From 63795eaa5cba78cc0a92d95fccd8db5523d7f050 Mon Sep 17 00:00:00 2001 From: fz Date: Thu, 3 Sep 2026 16:03:59 +0800 Subject: [PATCH 01/58] feat(minimal): remove str_replace_editor from minimal profiles --- ...cutable-sdk-runtime-distribution.i18n.yaml | 4 +- ...ile-executable-sdk-runtime-distribution.md | 2 +- ...-executable-sdk-runtime-distribution.zh.md | 2 +- ...3-python-sdk-dsh-profile-runtime.i18n.yaml | 4 +- ...26-08-23-python-sdk-dsh-profile-runtime.md | 2 +- ...08-23-python-sdk-dsh-profile-runtime.zh.md | 2 +- ...3-python-sdk-windows-x64-runtime.i18n.yaml | 4 +- ...26-08-23-python-sdk-windows-x64-runtime.md | 2 +- ...08-23-python-sdk-windows-x64-runtime.zh.md | 2 +- ...4-standalone-sdk-minimal-profile.i18n.yaml | 4 +- ...26-08-24-standalone-sdk-minimal-profile.md | 4 +- ...08-24-standalone-sdk-minimal-profile.zh.md | 4 +- ...nimal-preset-owns-rl-composition.i18n.yaml | 4 +- ...8-10-minimal-preset-owns-rl-composition.md | 10 +- ...0-minimal-preset-owns-rl-composition.zh.md | 10 +- ...rsistent-bash-str-replace-editor.i18n.yaml | 4 +- ...7-29-persistent-bash-str-replace-editor.md | 2 +- ...9-persistent-bash-str-replace-editor.zh.md | 2 +- ...31-even-out-shipped-tool-rosters.i18n.yaml | 4 +- ...026-07-31-even-out-shipped-tool-rosters.md | 2 +- ...-07-31-even-out-shipped-tool-rosters.zh.md | 2 +- ...l-profiles-bare-two-tool-runtime.i18n.yaml | 4 +- ...-minimal-profiles-bare-two-tool-runtime.md | 14 +- ...nimal-profiles-bare-two-tool-runtime.zh.md | 14 +- ...10-default-presets-single-editor.i18n.yaml | 4 +- ...026-08-10-default-presets-single-editor.md | 8 +- ...-08-10-default-presets-single-editor.zh.md | 8 +- ...l-profiles-persistent-shell-only.i18n.yaml | 6 + ...-minimal-profiles-persistent-shell-only.md | 33 ++ ...nimal-profiles-persistent-shell-only.zh.md | 33 ++ ...n-minimal-model-visible-snapshot.i18n.yaml | 4 +- ...3-python-minimal-model-visible-snapshot.md | 8 +- ...ython-minimal-model-visible-snapshot.zh.md | 8 +- apps/cli/reference/README.i18n.yaml | 4 +- apps/cli/reference/README.md | 4 +- apps/cli/reference/README.zh.md | 4 +- apps/cli/tests/built-bin.e2e.ts | 2 - apps/cli/tests/web-agent-presets.e2e.ts | 25 +- apps/web/tests/agent-preset-authoring.e2e.ts | 2 +- .../created.expected.md | 4 +- .../damaged.expected.md | 2 +- .../section.expected.md | 2 +- .../agent-preset-selection/menu.expected.md | 2 +- apps/web/tests/minimal-preset.snapshot.ts | 23 +- docs/user/guide/python-sdk.i18n.yaml | 4 +- docs/user/guide/python-sdk.md | 5 +- docs/user/guide/python-sdk.zh.md | 5 +- packages/bundle/sdk-minimal/README.i18n.yaml | 4 +- packages/bundle/sdk-minimal/README.md | 10 +- packages/bundle/sdk-minimal/README.zh.md | 10 +- packages/bundle/sdk-minimal/cordis.patch.yml | 12 - packages/bundle/sdk-minimal/package.json | 4 +- .../sdk-minimal/tests/sdk-minimal.spec.ts | 4 +- .../ui-agent-preset/src/client/locales.ts | 4 +- .../presets/minimal/agent.cordis.yml | 26 +- .../agent-presets/presets/minimal/preset.yml | 2 +- pnpm-lock.yaml | 6 - python/sdk/README.i18n.yaml | 4 +- python/sdk/README.md | 2 +- python/sdk/README.zh.md | 2 +- python/sdk/examples/README.i18n.yaml | 4 +- python/sdk/examples/README.md | 5 +- python/sdk/examples/README.zh.md | 5 +- scripts/smoke-python-runtime.py | 50 +- .../minimal/model-visible.json | 433 +----------------- .../minimal/win-x64/model-visible.json | 433 +----------------- .../minimal-preset/tool-schemas.expected.json | 85 ---- 67 files changed, 222 insertions(+), 1191 deletions(-) create mode 100644 .agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml create mode 100644 .agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md create mode 100644 .agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml index cba3cd1c5c..0fade92b07 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md -2026-07-10-single-file-executable-sdk-runtime-distribution.md: ab09691b48684b8ebae269df4d585b499423e671 -2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: d986105b087357bd8301251dca14736a5b427bbb +2026-07-10-single-file-executable-sdk-runtime-distribution.md: 4f3cec7951c92100fdaaf1375742cf3139587c64 +2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: fd766c98dcb5beff49b64952be92e09c60c08495 diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md index ab09691b48..4f3cec7951 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md @@ -66,7 +66,7 @@ The Python client launches the packaged `dsh` command with the selected profile ## Testing -The verification surface has three tiers. Mechanism tier: the measured conclusions for the `--sea` chain are embedded in the Decision sections (ESM dynamic import inside the VFS, single cordis instance, fail-loud config chain, `node:sqlite`, macOS ad-hoc signing runs). SDK tier: the complete keyless pytest suite covers the client protocol against a fake runtime peer, subprocess cleanup, absolute cwd propagation, dual-carrier launch, and carrier resolution; root CI runs it on Python 3.10. End-to-end tier: every platform build installs both wheels into a clean venv outside the checkout, proves matching versions and installed module/executable locations, then completes turns against a mock endpoint through the default SDK path, a custom config, the checked-in standalone minimal composition, and the direct binary protocol, with final text and JSONL checked. The minimal run asserts its exact system prompt and two-tool catalog, retains Bash state across calls, and invokes the editor. The custom config additionally drives `run_code` and a zero-agent `workflow` through their real worker files inside the packaged VFS. The filesystem-search scenario requires the model to call both `glob` and `grep` through the target-native `-rg` sidecar. The spawn-node scenario drives the platform shell tool through a command starting with `node` and requires the machine's own Node version in the tool result with no `PKG_EXECPATH` in the child environment, pinning the packaged runtime against a pkg upgrade that re-records the child-process patch. The MCP scenario starts a temporary external stdio server, deliberately delays its initial `tools/list` response, then immediately starts the first SDK prompt; the prompt must see and call the discovered tool, proving that `initialize` is a real Loader-settlement readiness boundary rather than a timing sleep. The same installed run compares a committed executable-specific snapshot through the Python SDK: a keyless scripted model mounts a Cordis plugin that registers a tool, invokes that tool from `run_code`, runs a direct spawn subagent and a workflow that starts a second spawn child, then unmounts the plugin. The fixture explicitly disables its unused bundled Bash and local skill discovery so its tool set does not depend on repository-external state, and the comparison normalizes opaque message, agent, workflow-run, and session IDs across the SDK result and notification stream plus the parent and two child JSONL logs. Trusted pull requests add a real-provider two-turn file write/read whose external bytes, tool calls, completed reasons, and persisted log must agree. This harness stays separate from ACP's `pnpm run test:snapshot` because the protocols and build artifacts differ. +The verification surface has three tiers. Mechanism tier: the measured conclusions for the `--sea` chain are embedded in the Decision sections (ESM dynamic import inside the VFS, single cordis instance, fail-loud config chain, `node:sqlite`, macOS ad-hoc signing runs). SDK tier: the complete keyless pytest suite covers the client protocol against a fake runtime peer, subprocess cleanup, absolute cwd propagation, dual-carrier launch, and carrier resolution; root CI runs it on Python 3.10. End-to-end tier: every platform build installs both wheels into a clean venv outside the checkout, proves matching versions and installed module/executable locations, then completes turns against a mock endpoint through the default SDK path, a custom config, the checked-in standalone minimal composition, and the direct binary protocol, with final text and JSONL checked. The minimal run asserts its exact system prompt and single shell-tool catalog and retains shell state across calls. The custom config additionally drives `run_code` and a zero-agent `workflow` through their real worker files inside the packaged VFS. The filesystem-search scenario requires the model to call both `glob` and `grep` through the target-native `-rg` sidecar. The spawn-node scenario drives the platform shell tool through a command starting with `node` and requires the machine's own Node version in the tool result with no `PKG_EXECPATH` in the child environment, pinning the packaged runtime against a pkg upgrade that re-records the child-process patch. The MCP scenario starts a temporary external stdio server, deliberately delays its initial `tools/list` response, then immediately starts the first SDK prompt; the prompt must see and call the discovered tool, proving that `initialize` is a real Loader-settlement readiness boundary rather than a timing sleep. The same installed run compares a committed executable-specific snapshot through the Python SDK: a keyless scripted model mounts a Cordis plugin that registers a tool, invokes that tool from `run_code`, runs a direct spawn subagent and a workflow that starts a second spawn child, then unmounts the plugin. The fixture explicitly disables its unused bundled Bash and local skill discovery so its tool set does not depend on repository-external state, and the comparison normalizes opaque message, agent, workflow-run, and session IDs across the SDK result and notification stream plus the parent and two child JSONL logs. Trusted pull requests add a real-provider two-turn file write/read whose external bytes, tool calls, completed reasons, and persisted log must agree. This harness stays separate from ACP's `pnpm run test:snapshot` because the protocols and build artifacts differ. Manual-driving caveat: the bin treats stdin EOF as "the client is gone" and disposes immediately, so a short-lived pipe aborts an in-flight turn — pipe-driven runs must keep stdin open until the turn ends. diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md index d986105b08..fd766c98dc 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md @@ -66,7 +66,7 @@ exe 内支持 `dsh-workflow-worker-thread` 与 `dsh-code-runtime-worker-thread` ## 测试 -验证面分三层。机制层:`--sea` 链路的实测结论内嵌在「决策」各节(VFS 内 ESM 动态 `import()`、单一 Cordis 实例、明确报错的配置链路、`node:sqlite`、macOS ad-hoc 签名可运行)。SDK 层:完整的无密钥 pytest 套件以 mock 运行时对端覆盖客户端协议、子进程清理、绝对 `cwd` 传递、双载体启动与载体解析;根 CI 在 Python 3.10 上运行全部用例。端到端层:每个平台构建都会把两个 wheel 包安装进 checkout 外的干净 venv,证明版本相同以及已安装模块/可执行文件的位置,再通过默认 SDK 路径、自定义配置、仓库内置的独立 minimal 组合和直接二进制协议,对 mock 端点完成轮次,并校验最终文本与 JSONL。minimal 运行会断言其精确系统提示词与双工具目录,跨调用保留 Bash 状态,并调用编辑器。自定义配置还会通过打包进 VFS 的真实工作线程文件执行 `run_code` 和不启动 agent 的 `workflow`。文件系统搜索场景要求模型通过目标平台的 `-rg` 伴随文件调用 `glob` 与 `grep`。spawn-node 场景驱动平台 shell 工具执行以 `node` 开头的命令,要求工具结果给出机器自身的 Node 版本且子进程环境中无 `PKG_EXECPATH`,把打包运行时钉死在「pkg 升级重录 child_process 补丁也不得回归」的行为上。MCP 场景会启动临时外部 stdio server,刻意延迟首次 `tools/list` 响应,随后立即启动第一个 SDK 提示词;该提示词必须看到并调用已发现的工具,从而证明 `initialize` 是真正以 Loader 插件树完全稳定为准的就绪边界,而不是依赖定时 sleep。同一项安装后运行还会经 Python SDK 比较一组检入的 exe 专用快照:无密钥脚本化模型挂载一个会注册工具的 Cordis 插件,从 `run_code` 调用该工具,运行一个直接 spawn 的 subagent 和一个会通过 spawn 启动第二个 subagent 的工作流,随后卸载该插件。该 fixture(测试前置数据)会显式禁用组合包中未使用的 Bash 和本地 skill(技能)发现,使其工具集不依赖仓库外部状态;比较时会规范化 SDK 结果与通知流,以及父会话和两个子会话 JSONL 日志中不透明的消息、agent、工作流运行与会话 ID。可信拉取请求会增加真实提供方双轮文件写入/读取,并要求外部字节、工具调用、已完成原因与持久化日志一致。该 harness 与 ACP 的 `pnpm run test:snapshot` 保持独立,因为二者的协议和构建产物不同。 +验证面分三层。机制层:`--sea` 链路的实测结论内嵌在「决策」各节(VFS 内 ESM 动态 `import()`、单一 Cordis 实例、明确报错的配置链路、`node:sqlite`、macOS ad-hoc 签名可运行)。SDK 层:完整的无密钥 pytest 套件以 mock 运行时对端覆盖客户端协议、子进程清理、绝对 `cwd` 传递、双载体启动与载体解析;根 CI 在 Python 3.10 上运行全部用例。端到端层:每个平台构建都会把两个 wheel 包安装进 checkout 外的干净 venv,证明版本相同以及已安装模块/可执行文件的位置,再通过默认 SDK 路径、自定义配置、仓库内置的独立 minimal 组合和直接二进制协议,对 mock 端点完成轮次,并校验最终文本与 JSONL。minimal 运行会断言其精确系统提示词与单个 shell 工具目录,并跨调用保留 shell 状态。自定义配置还会通过打包进 VFS 的真实工作线程文件执行 `run_code` 和不启动 agent 的 `workflow`。文件系统搜索场景要求模型通过目标平台的 `-rg` 伴随文件调用 `glob` 与 `grep`。spawn-node 场景驱动平台 shell 工具执行以 `node` 开头的命令,要求工具结果给出机器自身的 Node 版本且子进程环境中无 `PKG_EXECPATH`,把打包运行时钉死在「pkg 升级重录 child_process 补丁也不得回归」的行为上。MCP 场景会启动临时外部 stdio server,刻意延迟首次 `tools/list` 响应,随后立即启动第一个 SDK 提示词;该提示词必须看到并调用已发现的工具,从而证明 `initialize` 是真正以 Loader 插件树完全稳定为准的就绪边界,而不是依赖定时 sleep。同一项安装后运行还会经 Python SDK 比较一组检入的 exe 专用快照:无密钥脚本化模型挂载一个会注册工具的 Cordis 插件,从 `run_code` 调用该工具,运行一个直接 spawn 的 subagent 和一个会通过 spawn 启动第二个 subagent 的工作流,随后卸载该插件。该 fixture(测试前置数据)会显式禁用组合包中未使用的 Bash 和本地 skill(技能)发现,使其工具集不依赖仓库外部状态;比较时会规范化 SDK 结果与通知流,以及父会话和两个子会话 JSONL 日志中不透明的消息、agent、工作流运行与会话 ID。可信拉取请求会增加真实提供方双轮文件写入/读取,并要求外部字节、工具调用、已完成原因与持久化日志一致。该 harness 与 ACP 的 `pnpm run test:snapshot` 保持独立,因为二者的协议和构建产物不同。 手工驱动注意:`bin` 将 stdin EOF 视为「客户端已离开」并立即 dispose,生命周期较短的管道会中止进行中的轮次——管道驱动必须保持 stdin 打开,直到轮次结束。 diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml index e02c19ea8c..9547152e59 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md -2026-08-23-python-sdk-dsh-profile-runtime.md: 4af7812db6818b65c754a43ec1a7f973d1cbcbf9 -2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 155e7d2ae0b0ba3e4163dd85a31de90bef9d588a +2026-08-23-python-sdk-dsh-profile-runtime.md: 17e15e8bffcb22e7bfc7a868b86163ea127f1a5b +2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 311d553dedd1c7290367f1e99f31b85eaecd7e8c diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md index 4af7812db6..17e15e8bff 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md @@ -24,7 +24,7 @@ Every Python launch requires either explicit `dsh_home` or a non-empty `DSH_HOME Persistent SDK customization uses the same profile interfaces as direct CLI use. `dsh plugin --profile ...` manages external dependencies and bundle order, `$DSH_HOME/profiles//cordis.patch.yml` owns persistent row changes, the home patch applies machine-local changes across profiles, and Python `patches` supplies invocation-specific overlays. A selected profile is valid only when it retains an SDK server row. Missing profiles, bundles, server rows, and invalid patches fail without a complete-config fallback; a profile that remains alive without serving JSON-RPC fails the independently bounded initialization handshake with a diagnostic naming that profile. -The [standalone sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.md) lists one repository-owned bundle that inserts its complete explicit tree without `dsh-base`. Its persistent Bash and string-replace editor are present by composition; the shared JSON-RPC server exposes no root-agent tool filter. Dynamic runtime context, workspace instructions, settings, managed credentials, telemetry, compaction, and every other base row are absent. The same runtime still packages the full `sdk` and `web` profiles as separate choices. +The [standalone sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.md) lists one repository-owned bundle that inserts its complete explicit tree without `dsh-base`. Its platform-selected persistent shell is present by composition, while filesystem tools and providers are absent; the shared JSON-RPC server exposes no root-agent tool filter. Dynamic runtime context, workspace instructions, settings, managed credentials, telemetry, compaction, and every other base row are absent. The same runtime still packages the full `sdk` and `web` profiles as separate choices. The runtime wheel installs a `dsh` console command. Ordinary profile and SDK execution remains Node-free; external package management requires a caller-installed `pnpm`. diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md index 155e7d2ae0..311d553ded 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md @@ -24,7 +24,7 @@ Python SDK 分发一个私有 Node 应用,直接启动完整外部 `cordis.yml 持久 SDK 自定义使用与直接 CLI 相同的 profile 接口。`dsh plugin --profile ...` 管理外部依赖与 bundle 顺序,`$DSH_HOME/profiles//cordis.patch.yml` 负责持久配置项变更,home patch 对所有 profile 应用机器本地变更,Python `patches` 则提供单次启动 overlay。所选 profile 只有保留 SDK server 配置项时才有效。缺失 profile、bundle、server 配置项或非法 patch 都会直接失败,不存在完整配置回退;保持运行却不提供 JSON-RPC 服务的 profile 会在独立有界的初始化握手中失败,诊断会指明该 profile。 -[独立 sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.zh.md)只列出一个仓库自有组合包,该组合包会插入不含 `dsh-base` 的完整显式配置树。持久 Bash 与字符串替换 editor 通过组合存在;共享 JSON-RPC server 不暴露根 agent 工具筛选器。动态运行时上下文、workspace 指令、settings、托管凭据、遥测、compaction 与其他所有 base 配置项均不存在。同一运行时仍会把完整 `sdk` 与 `web` profile 作为独立选择打包。 +[独立 sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.zh.md)只列出一个仓库自有组合包,该组合包会插入不含 `dsh-base` 的完整显式配置树。按平台选择的持久 shell 通过组合存在,而文件系统工具与提供方缺席;共享 JSON-RPC server 不暴露根 agent 工具筛选器。动态运行时上下文、workspace 指令、settings、托管凭据、遥测、compaction 与其他所有 base 配置项均不存在。同一运行时仍会把完整 `sdk` 与 `web` profile 作为独立选择打包。 运行时 wheel 安装 `dsh` 控制台命令。普通 profile 与 SDK 运行仍不需要 Node;外部包管理要求调用方自行安装 `pnpm`。 diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-windows-x64-runtime.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-windows-x64-runtime.i18n.yaml index 3db66f3a7c..270722cea2 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-windows-x64-runtime.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-windows-x64-runtime.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-23-python-sdk-windows-x64-runtime.md -2026-08-23-python-sdk-windows-x64-runtime.md: 59a46d99f9e7ed411aeffbb541bbe3bb0c752078 -2026-08-23-python-sdk-windows-x64-runtime.zh.md: 3ab972aabb8135c8bc6285d129ba7bc9335eb11f +2026-08-23-python-sdk-windows-x64-runtime.md: 106a20838b1ad30b6470daf3a9651cd5df58e5c2 +2026-08-23-python-sdk-windows-x64-runtime.zh.md: 48e0d1ff5075644ef1674a3ada346d9583d70924 diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-windows-x64-runtime.md b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-windows-x64-runtime.md index 59a46d99f9..106a20838b 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-windows-x64-runtime.md +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-windows-x64-runtime.md @@ -30,7 +30,7 @@ The public Python client gives the initial profile handshake an independent 30-s After a successful shutdown response, the Python client closes stdin and waits within the configured shutdown timeout for the `dsh` context to exit and flush durable session state before terminating it. A failed shutdown retains immediate bounded termination. `shutdown_timeout_seconds` bounds each of the shutdown request, EOF grace, and termination-confirmation phases, so a pathological close can approach three times that value before the final kill. This distinction preserves the final accepted turn on Windows, where `terminate()` force-kills the process rather than delivering a catchable signal. -The minimal blackbox uses persistent `pwsh` plus `str_replace_editor` on Windows and owns `minimal/win-x64/model-visible.json`; Linux and macOS retain persistent Bash and the shared `minimal/model-visible.json`. The advanced process/subagent snapshot and restart/durable-log snapshot remain shared across all targets. The shipped [`sdk-minimal` bundle](../../../../packages/bundle/sdk-minimal/README.md) selects the same platform shell pair for the runnable Python tutorial. +The minimal blackbox uses only persistent `pwsh` on Windows and owns `minimal/win-x64/model-visible.json`; Linux and macOS use only persistent Bash and the shared `minimal/model-visible.json`. The advanced process/subagent snapshot and restart/durable-log snapshot remain shared across all targets. The shipped [`sdk-minimal` bundle](../../../../packages/bundle/sdk-minimal/README.md) selects the same platform shell pair for the runnable Python tutorial. ## Existing decisions and supersession diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-windows-x64-runtime.zh.md b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-windows-x64-runtime.zh.md index 3ab972aabb..48e0d1ff50 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-windows-x64-runtime.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-windows-x64-runtime.zh.md @@ -30,7 +30,7 @@ Windows lane 会创建干净的 Windows 虚拟环境,安装版本精确匹配 成功收到 shutdown 响应后,Python 客户端会关闭 stdin,并在已配置的 shutdown 超时内等待 `dsh` 上下文退出及刷写持久 session 状态,然后才回退到终止进程。Shutdown 失败时仍立即执行有界终止。`shutdown_timeout_seconds` 会分别限制 shutdown 请求、EOF 宽限与终止确认阶段,因此异常关闭在最终 kill 前可能接近该值的三倍。该区别会保留 Windows 上最后一个已接受轮次;该平台的 `terminate()` 会强制结束进程,而不是发送可捕获信号。 -极简黑盒测试在 Windows 上使用持久 `pwsh` 与 `str_replace_editor`,并由 `minimal/win-x64/model-visible.json` 固定预期;Linux 与 macOS 保留持久 Bash 和共享的 `minimal/model-visible.json`。高级进程/subagent 快照与重启/持久日志快照继续由所有目标共享。随附的 [`sdk-minimal` 组合包](../../../../packages/bundle/sdk-minimal/README.zh.md)为可运行 Python 教程选择同一组平台 shell。 +极简黑盒测试在 Windows 上仅使用持久 `pwsh`,并由 `minimal/win-x64/model-visible.json` 固定预期;Linux 与 macOS 仅使用持久 Bash 和共享的 `minimal/model-visible.json`。高级进程/subagent 快照与重启/持久日志快照继续由所有目标共享。随附的 [`sdk-minimal` 组合包](../../../../packages/bundle/sdk-minimal/README.zh.md)为可运行 Python 教程选择同一组平台 shell。 ## Existing decisions and supersession diff --git a/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.i18n.yaml index 549a861394..52d6e57170 100644 --- a/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.md -2026-08-24-standalone-sdk-minimal-profile.md: 3c53ea86479742e5bfddd7c13f22d370af90f0e4 -2026-08-24-standalone-sdk-minimal-profile.zh.md: c6f4d705d9e2c3d7c4acd404f94aebc1e83b77f7 +2026-08-24-standalone-sdk-minimal-profile.md: fe13c4e8a09a9fc3186b2f223cf625e00620497d +2026-08-24-standalone-sdk-minimal-profile.zh.md: 9e5d0b654c571238f15c6b6eac1b46cd79bf7b40 diff --git a/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.md b/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.md index 3c53ea8647..fe13c4e8a0 100644 --- a/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.md +++ b/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.md @@ -22,7 +22,7 @@ The bundle reuses `@deepseek-ai/dsh-sdk-app` for command help, stdin EOF, and bo ### Explicit composition -The bundle owns one DeepSeek adapter, SDK JSON-RPC serving, the explicit agent core, local subprocess and unrestricted filesystem providers, a platform-selected persistent shell, the string-replace editor, and uncompressed JSONL sessions under `$DSH_HOME/sessions`. Linux and macOS mount Bash; Windows mounts PowerShell. The SDK initialization request owns the model id; `DSH_CONTEXT_WINDOW` supplies fallback capacity for models outside the adapter's advisory catalog. The persona comes from `DSH_SYSTEM_PROMPT`, and the credential from `DEEPSEEK_API_KEY`. +The bundle owns one DeepSeek adapter, SDK JSON-RPC serving, the explicit agent core, a local subprocess provider, a platform-selected persistent shell, and uncompressed JSONL sessions under `$DSH_HOME/sessions`. Linux and macOS mount Bash; Windows mounts PowerShell. Filesystem tools and providers are absent under the later [persistent-shell-only decision](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.md). The SDK initialization request owns the model id; `DSH_CONTEXT_WINDOW` supplies fallback capacity for models outside the adapter's advisory catalog. The persona comes from `DSH_SYSTEM_PROMPT`, and the credential from `DEEPSEEK_API_KEY`. Harness identity, runtime context, workspace instructions, skills, model-facing job controls, compaction, settings, managed credentials, telemetry, Web tools, subagents, and every other base row are absent rather than hidden. The profile pins `danger-full-access`, `maxTokensAsSuccess: false`, and startup-only patch loading. @@ -40,7 +40,7 @@ It also supersedes the minimal-overlay realization in [Python SDK runtime throug ## Verification -The bundle test pins the exact row and dependency roster. Profile-template and config-dump tests pin the one-bundle manifest, startup-only lifecycle, absence of `dsh-base`, and absence of module HMR. The keyless source test boots the real `dsh --profile sdk-minimal` process, completes a turn, and asserts the generated manifest. The installed-wheel minimal scenario owns the complete system prompt and two advertised tools in its committed model-visible snapshot while exercising persistent shell state, editor effects, and JSONL persistence through the packaged executable. +The bundle test pins the exact row and dependency roster, including the absence of filesystem dependencies. Profile-template and config-dump tests pin the one-bundle manifest, startup-only lifecycle, absence of `dsh-base`, and absence of module HMR. The keyless source test boots the real `dsh --profile sdk-minimal` process, completes a turn, and asserts the generated manifest. The installed-wheel minimal scenario owns the complete system prompt and single advertised shell in its committed model-visible snapshot while exercising persistent shell state and JSONL persistence through the packaged executable. ## Alternatives considered diff --git a/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md b/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md index c6f4d705d9..9e5d0b654c 100644 --- a/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md @@ -22,7 +22,7 @@ Status: implemented ### 显式组合 -该组合包拥有一个 DeepSeek 适配器、SDK JSON-RPC 服务、显式 agent 核心、本地子进程与不受限文件系统提供方、按平台选择的持久 shell、字符串替换 editor,以及位于 `$DSH_HOME/sessions` 的未压缩 JSONL 会话。Linux 与 macOS 挂载 Bash,Windows 挂载 PowerShell。SDK 初始化请求拥有模型 id;`DSH_CONTEXT_WINDOW` 为不在适配器建议目录中的模型提供后备容量。Persona 来自 `DSH_SYSTEM_PROMPT`,凭据来自 `DEEPSEEK_API_KEY`。 +该组合包拥有一个 DeepSeek 适配器、SDK JSON-RPC 服务、显式 agent 核心、本地子进程提供方、按平台选择的持久 shell,以及位于 `$DSH_HOME/sessions` 的未压缩 JSONL 会话。Linux 与 macOS 挂载 Bash,Windows 挂载 PowerShell。后续的[仅持久 shell 决策](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md)要求文件系统工具与提供方缺席。SDK 初始化请求拥有模型 id;`DSH_CONTEXT_WINDOW` 为不在适配器建议目录中的模型提供后备容量。Persona 来自 `DSH_SYSTEM_PROMPT`,凭据来自 `DEEPSEEK_API_KEY`。 Harness 身份、运行时上下文、workspace 指令、skills、面向模型的 job 控制、compaction、settings、托管凭据、遥测、Web 工具、subagent 与其他所有 base 配置项均不存在,而不是被隐藏。该 profile 固定使用 `danger-full-access`、`maxTokensAsSuccess: false` 与仅启动时 patch 加载。 @@ -40,7 +40,7 @@ Python 运行时继续打包 `dsh-web-app` 与前端产物。`dsh web` 会从已 ## 验证 -组合包测试固定确切配置项与依赖清单。Profile 模板与配置 dump 测试固定单组合包 manifest、仅启动时生命周期、`dsh-base` 缺席与模块 HMR 缺席。Keyless 源码测试启动真实 `dsh --profile sdk-minimal` 进程、完成一个回合并断言生成的 manifest。Installed-wheel 极简场景通过已提交的模型可见快照固定完整系统提示词和两个对外公布的工具,同时经由打包可执行程序验证持久 shell 状态、editor 文件效果与 JSONL 持久化。 +组合包测试固定确切配置项与依赖清单,包括文件系统依赖缺席。Profile 模板与配置 dump 测试固定单组合包 manifest、仅启动时生命周期、`dsh-base` 缺席与模块 HMR 缺席。Keyless 源码测试启动真实 `dsh --profile sdk-minimal` 进程、完成一个回合并断言生成的 manifest。Installed-wheel 极简场景通过已提交的模型可见快照固定完整系统提示词和单个对外公布的 shell,同时经由打包可执行程序验证持久 shell 状态与 JSONL 持久化。 ## 考虑过的替代方案 diff --git a/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.i18n.yaml index 38476183a5..14d3016876 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md -2026-08-10-minimal-preset-owns-rl-composition.md: 4c296ed4af5df7a48bdfba6ff1972321cc2e54cf -2026-08-10-minimal-preset-owns-rl-composition.zh.md: 545f0a32fe9c7726d6fc910d0598174e7d7a3ec1 +2026-08-10-minimal-preset-owns-rl-composition.md: 9176deaea24875eefd828e23e79d1d0a71682464 +2026-08-10-minimal-preset-owns-rl-composition.zh.md: ab1dd86c224e3f0090e32a5ffe32aa2261fcbbba diff --git a/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md index 4c296ed4af..9176deaea2 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md +++ b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md @@ -12,17 +12,17 @@ The split also hid other drift. The preset mounted one-shot Bash rather than the ## Decision -The shipped Web `minimal` preset is the sole Web owner of the RL agent composition. It declares an entry-local PTY registry and local backend, persistent `bash` with the RL environment description and 300-second timeout, and `str_replace_editor`. Tool presentation remains a deployment choice. The later [bare two-tool runtime decision](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md) supersedes this note's original compaction and filesystem-provider choices: the current preset mounts an entry-local `fs-local` provider and no compaction backend. The editor accepts no `requireAbsolutePath` setting because absolute paths are its unconditional contract. +The shipped Web `minimal` preset is the sole Web owner of the RL agent composition. It declares an entry-local PTY registry and local backend plus persistent `bash` with the RL environment description and 300-second timeout. Tool presentation remains a deployment choice. The later [bare-runtime decision](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md) supersedes this note's original compaction choice, and the [persistent-shell-only decision](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.md) removes `str_replace_editor` and its otherwise-unused entry-local `fs-local` provider. The current preset therefore mounts no compaction or filesystem backend. The preset persona is exactly `You are a helpful software engineer assistant.`, sets `complete: true`, and suppresses runtime context for its agent scope. A complete `PromptSection` participates in ordinary assembly so tools, variables, and cooperative listeners still resolve; after the `system-prompt/assemble` waterfall, the prompt registry restores a detached copy of that section as the sole system-prompt section and discards every dynamic context contribution. Multiple effective complete sections reject assembly. These final registry constraints prevent harness identity, Web orientation, tool guidance, an assembly listener, sandbox policy, approval policy, delegation, or another dynamic context provider from adding model input. -The process-wide `core-web.cordis.yml` patch is absent. Browser UI, workspace attachment, persistence, subprocess, sandbox, permission, model routing, and other cross-session services remain host-owned. Selecting `minimal` changes one agent's model-facing composition and shadows the host filesystem provider only for that agent, without changing other sessions in the Web process. +The process-wide `core-web.cordis.yml` patch is absent. Browser UI, workspace attachment, persistence, subprocess, sandbox, permission, model routing, and other cross-session services remain host-owned. Selecting `minimal` changes one agent's model-facing composition without changing other sessions in the Web process. ## Verification -System-prompt and persona package tests prove final complete-section and runtime-context suppression, including waterfall mutation and duplicate rejection. The shipped-preset composition test asserts the exact prompt, Bash description, absolute editor schema, and two-tool catalog under the default native presentation. The keyless Web replay sends a real request through a `minimal` agent while global identity, Web-orientation text, dynamic policy contexts, and a test section are registered, asserts that no runtime-context snapshot exists, the entry-local filesystem is bare, and compaction is absent, then executes two persistent Bash calls to prove environment and cwd state survive and executes the editor through an absolute path. +System-prompt and persona package tests prove final complete-section and runtime-context suppression, including waterfall mutation and duplicate rejection. The shipped-preset composition test asserts the exact prompt, Bash description, single-tool catalog, and absence of a filesystem service under the default native presentation. The keyless Web replay sends a real request through a `minimal` agent while global identity, Web-orientation text, dynamic policy contexts, and a test section are registered, asserts that no runtime-context snapshot or filesystem service exists and compaction is absent, then executes two persistent Bash calls to prove environment and cwd state survive. -The standalone [`sdk-minimal` bundle](../../../../packages/bundle/sdk-minimal/README.md) is the complete two-tool composition for `dsh --profile sdk-minimal`. The [bare two-tool runtime decision](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md) owns its launch-specific environment configuration, bare filesystem, and absence of compaction; the [standalone-profile decision](../architecture/2026-08-24-standalone-sdk-minimal-profile.md) owns its launcher and bundle placement. Its keyless SDK process test asserts the assembled system prompt and two-tool catalog, and the installed-wheel scenario executes persistent Bash across calls and exercises the editor; the Python SDK tutorial provides the runnable entry point. +The standalone [`sdk-minimal` bundle](../../../../packages/bundle/sdk-minimal/README.md) is the complete single-tool composition for `dsh --profile sdk-minimal`. The [bare-runtime decision](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md) owns its launch-specific environment configuration and absence of compaction; the [standalone-profile decision](../architecture/2026-08-24-standalone-sdk-minimal-profile.md) owns its launcher and bundle placement. Its keyless SDK process test asserts the assembled system prompt and single-tool catalog, and the installed-wheel scenario executes the persistent shell across calls; the Python SDK tutorial provides the runnable entry point. ## Alternatives considered @@ -36,4 +36,4 @@ The standalone [`sdk-minimal` bundle](../../../../packages/bundle/sdk-minimal/RE ## Consequences -The Web RL prompt is fixed rather than environment-overridable; the standalone JSON-RPC prompt is deployment-selected. The Web preset and `sdk-minimal` profile share persistent-shell-plus-editor behavior for their respective launch paths; `sdk-minimal` selects PowerShell on Windows. Shell state is per agent and disappears with that agent. The Web preset pays for its own PTY and bare filesystem service instances, while other presets pay nothing for them. The Web preset's Bash backend requires the supported POSIX terminal substrate, so that preset does not support Windows agents. +The Web RL prompt is fixed rather than environment-overridable; the standalone JSON-RPC prompt is deployment-selected. The Web preset and `sdk-minimal` profile expose only their respective persistent shells; `sdk-minimal` selects PowerShell on Windows. Shell state is per agent and disappears with that agent. The Web preset pays for its own PTY service instance, while other presets pay nothing for it. The Web preset's Bash backend requires the supported POSIX terminal substrate, so that preset does not support Windows agents. diff --git a/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md index 545f0a32fe..ab1dd86c22 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md @@ -12,17 +12,17 @@ Status: implemented ## 决策 -随附的 Web `minimal` preset 是 RL agent 组合在 Web 中的唯一所有者。它声明 entry 本地的 PTY 注册表与本地后端、带 RL 环境描述且超时为 300 秒的持久 `bash`,以及 `str_replace_editor`。工具呈现仍由部署选择。后续的[裸双工具运行时决策](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md)取代了本记录最初的压缩与文件系统提供方选择:当前 preset 挂载 entry 本地的 `fs-local` 提供方,不挂载压缩后端。编辑器不接受 `requireAbsolutePath` 设置,因为要求绝对路径是它的无条件约定。 +随附的 Web `minimal` preset 是 RL agent 组合在 Web 中的唯一所有者。它声明 entry 本地的 PTY 注册表与本地后端,以及带 RL 环境描述且超时为 300 秒的持久 `bash`。工具呈现仍由部署选择。后续的[裸运行时决策](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md)取代了本记录最初的压缩选择,而[仅持久 shell 决策](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md)移除了 `str_replace_editor` 及其原本除此之外无人使用的 entry 本地 `fs-local` 提供方。因此当前 preset 不挂载压缩或文件系统后端。 preset persona 恰好是 `You are a helpful software engineer assistant.`,它设置 `complete: true`,并为其 agent 作用域抑制 runtime context。complete `PromptSection` 参与常规组装,因此工具、变量和协作式监听器仍会解析;`system-prompt/assemble` waterfall(瀑布式事件)结束后,提示词注册表会将该段落的独立副本恢复为唯一的系统提示词段落,并丢弃每个动态上下文贡献。存在多个有效 complete 段时,组装会被拒绝。这些最终注册表约束可防止 harness 身份、Web 定位、工具引导、组装监听器、沙箱策略、批准策略、委派或其他动态上下文提供方添加模型输入。 -进程级 `core-web.cordis.yml` patch 缺席。浏览器 UI、workspace 附加、持久化、子进程、沙箱、权限、模型路由及其他跨会话服务仍由宿主持有。选择 `minimal` 会改变一个 agent 面向模型的组合,并且仅为该 agent 遮蔽宿主文件系统提供方,不会改变 Web 进程中的其他会话。 +进程级 `core-web.cordis.yml` patch 缺席。浏览器 UI、workspace 附加、持久化、子进程、沙箱、权限、模型路由及其他跨会话服务仍由宿主持有。选择 `minimal` 会改变一个 agent 面向模型的组合,但不会改变 Web 进程中的其他会话。 ## 验证 -系统提示词与 persona 包测试证明了 complete 段最终约束与 runtime-context 抑制,包括 waterfall 修改与重复项拒绝。交付 preset 组合测试在默认原生呈现下断言精确的提示词、Bash 描述、要求绝对路径的编辑器 schema 和双工具目录。无密钥 Web 回放通过 `minimal` agent 发送一个真实请求,同时注册全局身份、Web 定位文本、动态策略上下文和一个测试段落;它断言不存在 runtime-context 快照、entry 本地文件系统是裸后端且压缩不存在,随后执行两次持久 Bash 调用,证明环境与 cwd 状态能够保留,并通过绝对路径执行编辑器。 +系统提示词与 persona 包测试证明了 complete 段最终约束与 runtime-context 抑制,包括 waterfall 修改与重复项拒绝。交付 preset 组合测试在默认原生呈现下断言精确的提示词、Bash 描述、单工具目录,以及文件系统服务缺席。无密钥 Web 回放通过 `minimal` agent 发送一个真实请求,同时注册全局身份、Web 定位文本、动态策略上下文和一个测试段落;它断言不存在 runtime-context 快照与文件系统服务且压缩不存在,随后执行两次持久 Bash 调用,证明环境与 cwd 状态能够保留。 -独立的 [`sdk-minimal` 组合包](../../../../packages/bundle/sdk-minimal/README.zh.md)是 `dsh --profile sdk-minimal` 的完整双工具组合。[裸双工具运行时决策](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md)说明其启动方式专属的环境配置、裸文件系统和无 compaction 选择;[独立 profile 决策](../architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md)负责其 launcher 与组合包位置。其无密钥 SDK 进程测试会断言组装后的系统提示词与双工具目录,installed-wheel 场景会跨调用执行持久 Bash 并使用编辑器;Python SDK 教程提供可运行入口。 +独立的 [`sdk-minimal` 组合包](../../../../packages/bundle/sdk-minimal/README.zh.md)是 `dsh --profile sdk-minimal` 的完整单工具组合。[裸运行时决策](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md)说明其启动方式专属的环境配置和无 compaction 选择;[独立 profile 决策](../architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md)负责其 launcher 与组合包位置。其无密钥 SDK 进程测试会断言组装后的系统提示词与单工具目录,installed-wheel 场景会跨调用执行持久 shell;Python SDK 教程提供可运行入口。 ## 考虑过的替代方案 @@ -36,4 +36,4 @@ preset persona 恰好是 `You are a helpful software engineer assistant.`,它 ## 后果 -Web RL 提示词固定不变,不能通过环境覆盖;独立 JSON-RPC 提示词由部署选择。Web preset 与 `sdk-minimal` profile 在各自启动路径共享持久 shell 加 editor 的行为;`sdk-minimal` 在 Windows 上选择 PowerShell。Shell 状态按 agent 隔离,并随该 agent 一并消失。Web preset 为自身的 PTY 与裸文件系统服务实例承担开销,其他 preset 无需承担。Web preset 的 Bash 后端需要受支持的 POSIX 终端基础环境,因此该 preset 不支持 Windows agent。 +Web RL 提示词固定不变,不能通过环境覆盖;独立 JSON-RPC 提示词由部署选择。Web preset 与 `sdk-minimal` profile 分别只暴露各自的持久 shell;`sdk-minimal` 在 Windows 上选择 PowerShell。Shell 状态按 agent 隔离,并随该 agent 一并消失。Web preset 为自身的 PTY 服务实例承担开销,其他 preset 无需承担。Web preset 的 Bash 后端需要受支持的 POSIX 终端基础环境,因此该 preset 不支持 Windows agent。 diff --git a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.i18n.yaml b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.i18n.yaml index 5a32ed9ae3..9ba413c09f 100644 --- a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md -2026-07-29-persistent-bash-str-replace-editor.md: e6265ca0f8eb5430ba490b9d046a726f152a2892 -2026-07-29-persistent-bash-str-replace-editor.zh.md: 2fb8a3b3cadefd101d90c52a5ed01e8244a58f07 +2026-07-29-persistent-bash-str-replace-editor.md: 02ad2bfa8ab8051eff2c408c8dbbca7acca2daaa +2026-07-29-persistent-bash-str-replace-editor.zh.md: beb2e17d2d342e621e0f1f829910ecf48e35ac80 diff --git a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md index e6265ca0f8..02ad2bfa8a 100644 --- a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md +++ b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md @@ -18,7 +18,7 @@ Some deployments need a one-call Bash schema whose shell state survives across m Both plugins are included in the Python runtime closure. The persistent Bash closure also includes the PTY service/local backend and the sandbox services required by that backend. Because `node-pty` executes a native `spawn-helper` on macOS, each packaged macOS runtime executable ships with a `-spawn-helper` sibling; Linux uses `forkpty` directly. A pinned `node-pty` patch checks `DSH_NODE_PTY_SPAWN_HELPER` first, so it remains a true override for a current external consumer that supplies a non-sibling helper. When the override is unset, the patch resolves the packaged executable sibling if present and otherwise preserves upstream lookup in ordinary Node runs. The macOS builders fail before publication when the helper is absent or not executable. -The shipped [`minimal` agent preset](../../../../packages/preset/agent-presets/presets/minimal/agent.cordis.yml) composes both plugins for the Claude SWE-compatible RL contract. Its entry-local PTY realm carries the registry, local backend, and persistent Bash tool; the editor registers beside that realm against the host filesystem. The preset fixes the complete system prompt, follows the deployment tool-presentation mode, omits every other model-facing consumer, and leaves browser, Workspace, persistence, sandbox, and permission services on the shared Web host. The local PTY backend resolves the effective session sandbox mode when it creates the shell. While that owner has an open shell or a spawn in progress, a different permission mode is rejected before its session event commits; the editor continues through the Web filesystem sandbox. The [minimal-preset decision](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md) owns this composition boundary. +The shipped [`minimal` agent preset](../../../../packages/preset/agent-presets/presets/minimal/agent.cordis.yml) composes only the persistent-shell plugin for the minimal model contract. Its entry-local PTY realm carries the registry, local backend, and persistent Bash or PowerShell tool. The preset fixes the complete system prompt, follows the deployment tool-presentation mode, omits every other model-facing consumer, and leaves browser, Workspace, persistence, sandbox, and permission services on the shared Web host. The local PTY backend resolves the effective session sandbox mode when it creates the shell. While that owner has an open shell or a spawn in progress, a different permission mode is rejected before its session event commits. The [persistent-shell-only decision](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.md) owns the editor's absence from minimal compositions; the editor remains independently mountable elsewhere. ## Alternatives considered diff --git a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.zh.md b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.zh.md index 2fb8a3b3ca..beb2e17d2d 100644 --- a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.zh.md +++ b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.zh.md @@ -18,7 +18,7 @@ Status: implemented 两个插件都进入 Python runtime 闭包。持久 Bash 的闭包还包含 PTY 服务/本地后端,以及该后端要求的沙箱服务。由于 `node-pty` 在 macOS 上会执行原生 `spawn-helper`,每个打包后的 macOS 运行时可执行文件都会携带一个 `-spawn-helper` 伴随文件;Linux 直接使用 `forkpty`。固定版本的 `node-pty` 补丁会先检查 `DSH_NODE_PTY_SPAWN_HELPER`,因此对当前提供非伴随 helper 的外部消费方而言,该变量仍是真正的覆盖项。未设置该覆盖时,补丁会在打包可执行文件的伴随文件存在时解析它,否则在普通 Node 运行中保留上游查找方式。若 helper 缺失或不可执行,macOS 构建器会在发布前失败。 -随附的 [`minimal` agent preset](../../../../packages/preset/agent-presets/presets/minimal/agent.cordis.yml) 会组合这两个插件,以满足与 Claude SWE 兼容的 RL 约定。其 entry 本地 PTY realm 持有注册表、本地后端和持久 Bash 工具;编辑器在该 realm 旁注册,并使用宿主文件系统。preset 会固定完整系统提示词、跟随部署的工具呈现模式,省略其他所有面向模型的消费方,并将浏览器、Workspace、持久化、沙箱与权限服务留在共享 Web 宿主上。本地 PTY 后端会在创建 shell 时解析会话的有效沙箱模式。只要该所有者仍有打开的 shell 或仍在进行中的 spawn,另一种权限模式就会在对应的会话事件提交前遭到拒绝;编辑器则继续经由 Web 文件系统沙箱运行。这一组合边界由 [minimal-preset 决策](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md)负责说明。 +随附的 [`minimal` agent preset](../../../../packages/preset/agent-presets/presets/minimal/agent.cordis.yml)只为极简模型约定组合持久 shell 插件。其 entry 本地 PTY realm 持有注册表、本地后端和持久 Bash 或 PowerShell 工具。Preset 会固定完整系统提示词、跟随部署的工具呈现模式,省略其他所有面向模型的消费方,并将浏览器、Workspace、持久化、沙箱与权限服务留在共享 Web 宿主上。本地 PTY 后端会在创建 shell 时解析会话的有效沙箱模式。只要该所有者仍有打开的 shell 或仍在进行中的 spawn,另一种权限模式就会在对应的会话事件提交前遭到拒绝。[仅持久 shell 决策](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md)负责极简组合不包含 editor 的事实;其他组合仍可独立挂载 editor。 ## 考虑过的替代方案 diff --git a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml index 2f2c4740b0..33e4ac93a8 100644 --- a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md -2026-07-31-even-out-shipped-tool-rosters.md: 4e6b4b0e0ad89ce95d731dac21a8b55a24663be5 -2026-07-31-even-out-shipped-tool-rosters.zh.md: 4be6678cc2f728c6f69cbf97462089a6b45c736a +2026-07-31-even-out-shipped-tool-rosters.md: 4fea5fb30860f3254aec6178068158b4e7756493 +2026-07-31-even-out-shipped-tool-rosters.zh.md: 32c188f6419b9ede74838215848d77f139280cde diff --git a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md index 4e6b4b0e0a..4fea5fb308 100644 --- a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md +++ b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md @@ -12,7 +12,7 @@ The result was a user-visible difference nobody had decided: the same model, ask ## Decision -The rows that are not surface-specific move into [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml), and three more join them: `tool-session-query`, `tool-str-replace-editor`, and `repeat-tool-reminder`. Web search moves there too; its [deployment decision](2026-07-31-web-default-search.md) owns the security boundary while the shared base owns its surface-neutral mount. Both surfaces assemble the same roster, including fixed `glob` and `grep` members because `dsh-tool-fs-search` spawns the [packaged ripgrep binary](../architecture/2026-08-01-packaged-ripgrep-search.md). Two later decisions narrow that roster: the [session-search decision](2026-08-02-session-search-not-shipped-default.md) keeps `tool-session-query` opt-in, and the [single-editor decision](../simplification/2026-08-10-default-presets-single-editor.md) keeps `tool-str-replace-editor` out of the general-purpose presets while retaining it in `minimal`. +The rows that are not surface-specific move into [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml), and three more join them: `tool-session-query`, `tool-str-replace-editor`, and `repeat-tool-reminder`. Web search moves there too; its [deployment decision](2026-07-31-web-default-search.md) owns the security boundary while the shared base owns its surface-neutral mount. Both surfaces assemble the same roster, including fixed `glob` and `grep` members because `dsh-tool-fs-search` spawns the [packaged ripgrep binary](../architecture/2026-08-01-packaged-ripgrep-search.md). Later decisions narrow that roster: the [session-search decision](2026-08-02-session-search-not-shipped-default.md) keeps `tool-session-query` opt-in, the [single-editor decision](../simplification/2026-08-10-default-presets-single-editor.md) removes `tool-str-replace-editor` from general-purpose presets, and the [persistent-shell-only decision](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.md) removes it from the minimal compositions. Two rows stay surface-specific. `tmux-context` is TUI-only because a browser surface has no terminal multiplexer to describe. `session-reference` is TUI-only because it drives the shared session-query index from the launcher's process-local path, and the browser sidebar reconciles that index on its own first search. diff --git a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md index 4be6678cc2..32c188f641 100644 --- a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md +++ b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md @@ -12,7 +12,7 @@ Status: implemented ## 决策 -那些并非 surface 专属的行移入 [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml),另有三行加入:`tool-session-query`、`tool-str-replace-editor` 和 `repeat-tool-reminder`。Web 搜索也一并移入;其[部署决策](2026-07-31-web-default-search.zh.md)负责安全边界,共享 base 则负责与 surface 无关的挂载。两个 surface 组装同一份清单,其中 `glob` 和 `grep` 是固定成员,因为 `dsh-tool-fs-search` 直接 spawn [打包的 ripgrep 二进制](../architecture/2026-08-01-packaged-ripgrep-search.zh.md)。之后有两项决策收窄这份清单:[session-search 决策](2026-08-02-session-search-not-shipped-default.zh.md)让 `tool-session-query` 保持需显式启用,[单一编辑器决策](../simplification/2026-08-10-default-presets-single-editor.zh.md)让通用 preset 不提供 `tool-str-replace-editor`,但在 `minimal` 中保留它。 +那些并非 surface 专属的行移入 [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml),另有三行加入:`tool-session-query`、`tool-str-replace-editor` 和 `repeat-tool-reminder`。Web 搜索也一并移入;其[部署决策](2026-07-31-web-default-search.zh.md)负责安全边界,共享 base 则负责与 surface 无关的挂载。两个 surface 组装同一份清单,其中 `glob` 和 `grep` 是固定成员,因为 `dsh-tool-fs-search` 直接 spawn [打包的 ripgrep 二进制](../architecture/2026-08-01-packaged-ripgrep-search.zh.md)。后续决策收窄了这份清单:[session-search 决策](2026-08-02-session-search-not-shipped-default.zh.md)让 `tool-session-query` 保持需显式启用,[单一 editor 决策](../simplification/2026-08-10-default-presets-single-editor.zh.md)从通用 preset 移除 `tool-str-replace-editor`,[仅持久 shell 决策](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md)则从极简组合移除它。 有两行仍是 surface 专属。`tmux-context` 只在 TUI,因为浏览器 surface 没有终端复用器可描述。`session-reference` 只在 TUI,因为它以 launcher 的进程本地路径驱动共享的 session-query 索引,而浏览器侧边栏会在自己的首次搜索里重建该索引。 diff --git a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.i18n.yaml b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.i18n.yaml index 575a930f99..3cc9d383ee 100644 --- a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md -2026-08-11-minimal-profiles-bare-two-tool-runtime.md: ffe8e799e1ba5a71939e3a17d484afd209b4014f -2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md: 587ba696382575244a75f23fe69c14252caf7544 +2026-08-11-minimal-profiles-bare-two-tool-runtime.md: 1bca0261484da867fcffd33b90535cc014903e95 +2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md: cecf5e2ff5a334f18e09e9eacb7639e1d3bf5d60 diff --git a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md index ffe8e799e1..1bca026148 100644 --- a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md +++ b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md @@ -1,4 +1,4 @@ -# Agent Note: Minimal profiles use the bare two-tool runtime +# Agent Note: Minimal profiles use a bare runtime Status: implemented @@ -12,23 +12,23 @@ The two launch paths also have different configuration owners. Web mounts a per- ## Decision -The shipped Web minimal preset exposes persistent `bash` and `str_replace_editor`; the standalone profile exposes persistent `bash` on Linux/macOS or `pwsh` on Windows, plus the same editor. Both mount no context-compaction provider, suppress every `dsh-system-prompt` runtime-context contribution for fresh sessions, and run the editor against `@deepseek-ai/dsh-fs-local`. The Web preset isolates `ctx.fs` inside the agent entry and mounts `fs-local` beside the editor, so other Web agents retain the host filesystem provider. Its persona remains the fixed complete prompt owned by the earlier [minimal-preset composition decision](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md) and applies runtime-context suppression only to that agent scope. The standalone spine forwards the same setting to its process-owned system-prompt service. The Web host retains its sandbox and approval services; the standalone profile mounts a danger-full-access sandbox policy and no approval service. Neither contributes model-facing policy context. +The shipped Web minimal preset exposes persistent `bash`; the standalone profile exposes persistent `bash` on Linux/macOS or `pwsh` on Windows. Both mount no context-compaction or filesystem provider and suppress every `dsh-system-prompt` runtime-context contribution for fresh sessions. The [persistent-shell-only decision](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.md) removes the editor and its otherwise-unused `fs-local` provider from both compositions. The Web preset's persona remains the fixed complete prompt owned by the earlier [minimal-preset composition decision](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md) and applies runtime-context suppression only to that agent scope. The standalone spine forwards the same setting to its process-owned system-prompt service. The Web host retains its sandbox and approval services; the standalone profile mounts a danger-full-access sandbox policy and no approval service. Neither contributes model-facing policy context. -The standalone [`@deepseek-ai/dsh-sdk-minimal` bundle](../../../../packages/bundle/sdk-minimal/README.md) remains a complete JSON-RPC process composition behind `dsh --profile sdk-minimal`. It mounts SDK startup and JSON-RPC serving, the local PTY and subprocess services required by the platform-selected persistent shell, `fs-local`, that shell's tool consumer, the editor, and uncompressed JSONL persistence under `$DSH_HOME/sessions`. It does not mount `token-meter`, `compaction-basic`, `fs-sandbox`, or `fs-observation-policy`. The persistent shell consumes the profile's danger-full-access sandbox policy; the editor is not confined by that policy. The [standalone-profile decision](../architecture/2026-08-24-standalone-sdk-minimal-profile.md) owns this bundle placement and its separation from `dsh-base`. +The standalone [`@deepseek-ai/dsh-sdk-minimal` bundle](../../../../packages/bundle/sdk-minimal/README.md) remains a complete JSON-RPC process composition behind `dsh --profile sdk-minimal`. It mounts SDK startup and JSON-RPC serving, the local PTY and subprocess services required by the platform-selected persistent shell, that shell's tool consumer, and uncompressed JSONL persistence under `$DSH_HOME/sessions`. It does not mount `token-meter`, `compaction-basic`, `fs-local`, `fs-sandbox`, `fs-observation-policy`, or a filesystem tool. The persistent shell consumes the profile's danger-full-access sandbox policy. The [standalone-profile decision](../architecture/2026-08-24-standalone-sdk-minimal-profile.md) owns this bundle placement and its separation from `dsh-base`. `DSH_SYSTEM_PROMPT` selects the standalone persona, and `DSH_CONTEXT_WINDOW` supplies fallback capacity for a model without exact catalog metadata. The SDK client's JSON-RPC `initialize` request is the sole runtime model selection. [`minimal.py`](../../../../python/sdk/examples/minimal.py) may read `DSH_MODEL` only as the command's default `model` argument; an explicit `--model` needs no matching child environment value. Endpoint and credential variables stay owned by the DeepSeek adapter's existing environment-resolution path. ## Verification -The Web replay boots the complete Web host, creates the agent through the preset service, and asserts that the scoped filesystem is bare, no scoped compaction service exists, no system-prompt-owned runtime-context message was appended, and the assembled request contains exactly the fixed prompt and two tools. It then executes persistent Bash and the editor against the real scoped services. +The Web replay boots the complete Web host, creates the agent through the preset service, and asserts that no scoped filesystem or compaction service exists, no system-prompt-owned runtime-context message was appended, and the assembled request contains exactly the fixed prompt and persistent Bash. It then executes persistent Bash against the real scoped services. -The SDK keyless source test boots real `dsh --profile sdk-minimal`, completes a turn with an environment-selected prompt, and asserts the generated one-bundle manifest. The Python SDK bundled-runtime snapshot owns the assembled prompt, exact two-tool catalog, and absence of every system-prompt-owned runtime-context message. Packaged-runtime coverage initializes the standalone profile through each available carrier with environment-selected model, model capacity, and prompt values, then executes the selected persistent shell and editor. Cordis validation checks that both configurations resolve their declared plugins and configuration fields. +The SDK keyless source test boots real `dsh --profile sdk-minimal`, completes a turn with an environment-selected prompt, and asserts the generated one-bundle manifest. The Python SDK bundled-runtime snapshot owns the assembled prompt, exact single-tool catalog, and absence of every system-prompt-owned runtime-context message. Packaged-runtime coverage initializes the standalone profile through each available carrier with environment-selected model, model capacity, and prompt values, then executes the selected persistent shell. Cordis validation checks that both configurations resolve their declared plugins and configuration fields. ## Alternatives considered **Keep `compaction-basic` mounted with a high threshold.** Rejected because even an inert-for-short-tests provider permits history replacement in longer sessions and leaves the minimal composition dependent on model-capacity metadata and the token meter. -**Keep `fs-sandbox` in danger-full-access mode.** Rejected because the sandboxed provider still makes confinement and escalation part of the editor capability. The target runtime requires the bare local provider, whose lack of `sandboxMode` is composition truth. +**Keep `fs-local` after removing the editor.** Rejected because neither minimal composition has another filesystem consumer. Retaining the provider would increase the runtime roster without adding a model-visible capability. **Use one Cordis leaf for Web and Python SDK startup.** Rejected because a Web preset contributes agent-scoped services to an existing multi-session host, while the Python SDK must launch a complete process containing the JSON-RPC server and its process-wide dependencies. @@ -36,4 +36,4 @@ The SDK keyless source test boots real `dsh --profile sdk-minimal`, completes a ## Consequences -Minimal sessions never summarize or replace earlier history and never add a runtime-context snapshot; callers must keep turns within the selected model's context capacity and must not rely on model-visible narration of standing sandbox or approval policy. The editor can address any absolute path visible to the runtime process, independently of the persistent shell's sandbox policy. The two launch paths share their model-facing tool, no-context, and no-compaction guarantees while retaining different prompt and model configuration appropriate to their owners. The Python SDK path communicates only through the bundled `dsh` stdio JSON-RPC profile. +Minimal sessions never summarize or replace earlier history and never add a runtime-context snapshot; callers must keep turns within the selected model's context capacity and must not rely on model-visible narration of standing sandbox or approval policy. The two launch paths share their single-tool, no-filesystem, no-context, and no-compaction guarantees while retaining different prompt and model configuration appropriate to their owners. The Python SDK path communicates only through the bundled `dsh` stdio JSON-RPC profile. diff --git a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md index 587ba69638..cecf5e2ff5 100644 --- a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md +++ b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md @@ -1,4 +1,4 @@ -# Agent Note: minimal profile 使用裸双工具运行时 +# Agent Note: minimal profile 使用裸运行时 Status: implemented @@ -12,23 +12,23 @@ Web `minimal` preset 与独立 JSON-RPC minimal 组合对外提供持久 `bash` ## 决策 -随附 Web minimal preset 对外提供持久 `bash` 与 `str_replace_editor`;独立 profile 在 Linux/macOS 上提供持久 `bash`,在 Windows 上提供 `pwsh`,并提供相同 editor。两者都不挂载上下文压缩提供方,为新建会话抑制每个 `dsh-system-prompt` runtime-context 贡献,并让编辑器使用 `@deepseek-ai/dsh-fs-local`。Web preset 在 agent entry 内隔离 `ctx.fs`,将 `fs-local` 与编辑器一起挂载,因此其他 Web agent 仍使用宿主文件系统提供方。其 persona 继续采用较早的 [minimal preset 组合决策](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md)所拥有的固定 complete 提示词,并仅为该 agent 作用域实施 runtime-context 抑制。独立 spine 将同一设置转发给其进程拥有的 system-prompt 服务。Web 宿主保留沙箱与批准服务;独立 profile 挂载 danger-full-access 沙箱策略,不挂载批准服务。两者都不贡献面向模型的策略上下文。 +随附 Web minimal preset 对外提供持久 `bash`;独立 profile 在 Linux/macOS 上提供持久 `bash`,在 Windows 上提供 `pwsh`。两者都不挂载上下文压缩或文件系统提供方,并为新建会话抑制每个 `dsh-system-prompt` runtime-context 贡献。[仅持久 shell 决策](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md)从两份组合中移除了编辑器及其原本除此之外无人使用的 `fs-local` 提供方。Web preset 的 persona 继续采用较早的 [minimal preset 组合决策](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md)所拥有的固定 complete 提示词,并仅为该 agent 作用域实施 runtime-context 抑制。独立 spine 将同一设置转发给其进程拥有的 system-prompt 服务。Web 宿主保留沙箱与批准服务;独立 profile 挂载 danger-full-access 沙箱策略,不挂载批准服务。两者都不贡献面向模型的策略上下文。 -独立的 [`@deepseek-ai/dsh-sdk-minimal` 组合包](../../../../packages/bundle/sdk-minimal/README.zh.md)仍是 `dsh --profile sdk-minimal` 后面的完整 JSON-RPC 进程组合。它挂载 SDK 启动与 JSON-RPC 服务、按平台选择的持久 shell 所需的本地 PTY 和子进程服务、`fs-local`、该 shell 的工具消费方、editor,以及位于 `$DSH_HOME/sessions` 的未压缩 JSONL 持久化。它不挂载 `token-meter`、`compaction-basic`、`fs-sandbox` 或 `fs-observation-policy`。持久 shell 消费该 profile 的 danger-full-access 沙箱策略;编辑器不受该策略限制。[独立 profile 决策](../architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md)负责该组合包的位置及其与 `dsh-base` 的分离。 +独立的 [`@deepseek-ai/dsh-sdk-minimal` 组合包](../../../../packages/bundle/sdk-minimal/README.zh.md)仍是 `dsh --profile sdk-minimal` 后面的完整 JSON-RPC 进程组合。它挂载 SDK 启动与 JSON-RPC 服务、按平台选择的持久 shell 所需的本地 PTY 和子进程服务、该 shell 的工具消费方,以及位于 `$DSH_HOME/sessions` 的未压缩 JSONL 持久化。它不挂载 `token-meter`、`compaction-basic`、`fs-local`、`fs-sandbox`、`fs-observation-policy` 或文件系统工具。持久 shell 消费该 profile 的 danger-full-access 沙箱策略。[独立 profile 决策](../architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md)负责该组合包的位置及其与 `dsh-base` 的分离。 `DSH_SYSTEM_PROMPT` 选择独立组合的 persona,`DSH_CONTEXT_WINDOW` 为没有确切目录元数据的模型提供后备容量。SDK 客户端的 JSON-RPC `initialize` 请求是唯一运行时模型选择。[`minimal.py`](../../../../python/sdk/examples/minimal.py)可以只把 `DSH_MODEL` 读作命令的默认 `model` 参数;显式 `--model` 不需要匹配的子进程环境值。端点与凭据变量继续由 DeepSeek 适配器现有的环境解析路径持有。 ## 验证 -Web 回放会启动完整 Web 宿主,通过 preset 服务创建 agent,并断言作用域文件系统为裸后端、不存在作用域压缩服务、没有追加 system-prompt 拥有的 runtime-context 消息,而且组装请求只包含固定提示词与两个工具。随后,它通过真实作用域服务执行持久 Bash 和编辑器。 +Web 回放会启动完整 Web 宿主,通过 preset 服务创建 agent,并断言不存在作用域文件系统或压缩服务、没有追加 system-prompt 拥有的 runtime-context 消息,而且组装请求只包含固定提示词与持久 Bash。随后,它通过真实作用域服务执行持久 Bash。 -SDK keyless 源码测试启动真实 `dsh --profile sdk-minimal`,使用环境选择的提示词完成一个回合,并断言生成的单组合包 manifest。Python SDK 打包运行时快照固定组装提示词、精确双工具目录,并固定不存在任何 system-prompt 所拥有的 runtime-context 消息。打包运行时覆盖会通过每种可用载体,使用环境选择的模型、模型容量和提示词值初始化独立 profile,然后执行所选持久 shell 与 editor。Cordis 校验会检查两份配置能否解析声明的插件和配置字段。 +SDK keyless 源码测试启动真实 `dsh --profile sdk-minimal`,使用环境选择的提示词完成一个回合,并断言生成的单组合包 manifest。Python SDK 打包运行时快照固定组装提示词、精确单工具目录,并固定不存在任何 system-prompt 所拥有的 runtime-context 消息。打包运行时覆盖会通过每种可用载体,使用环境选择的模型、模型容量和提示词值初始化独立 profile,然后执行所选持久 shell。Cordis 校验会检查两份配置能否解析声明的插件和配置字段。 ## 考虑过的替代方案 **以较高阈值保留 `compaction-basic`。** 不予采用,因为即便提供方在短测试中未触发,较长会话仍允许替换历史记录,而且 minimal 组合仍会依赖模型容量元数据与 token meter。 -**在 danger-full-access 模式下保留 `fs-sandbox`。** 不予采用,因为沙箱提供方仍会使限权与提权成为编辑器能力的一部分。目标运行时要求裸本地提供方,而其不具备 `sandboxMode` 正是组合事实。 +**移除编辑器后保留 `fs-local`。** 不予采用,因为两份 minimal 组合都没有其他文件系统消费方。保留该提供方只会扩大运行时清单,不会新增模型可见能力。 **为 Web 与 Python SDK 启动使用同一个 Cordis leaf。** 不予采用,因为 Web preset 向现有多会话宿主贡献 agent 作用域服务,而 Python SDK 必须启动包含 JSON-RPC 服务器及其进程级依赖的完整进程。 @@ -36,4 +36,4 @@ SDK keyless 源码测试启动真实 `dsh --profile sdk-minimal`,使用环境 ## 后果 -Minimal 会话不会摘要或替换较早历史,也不会添加 runtime-context 快照;调用方必须让会话轮次保持在所选模型的上下文容量内,且不得依赖模型可见的常驻沙箱或批准策略说明。编辑器可以访问运行时进程可见的任何绝对路径,且不受持久 shell 沙箱策略影响。两条启动路径共享面向模型的工具、无上下文与无压缩保证,同时保留适合各自所有者的不同提示词和模型配置。Python SDK 路径只通过内置 `dsh` stdio JSON-RPC profile 通信。 +Minimal 会话不会摘要或替换较早历史,也不会添加 runtime-context 快照;调用方必须让会话轮次保持在所选模型的上下文容量内,且不得依赖模型可见的常驻沙箱或批准策略说明。两条启动路径共享单工具、无文件系统、无上下文与无压缩保证,同时保留适合各自所有者的不同提示词和模型配置。Python SDK 路径只通过内置 `dsh` stdio JSON-RPC profile 通信。 diff --git a/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.i18n.yaml b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.i18n.yaml index 4e47e34616..5d39c528e2 100644 --- a/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.i18n.yaml +++ b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.md -2026-08-10-default-presets-single-editor.md: 3d1c1dea4f9fcc676a8146033f7785dfd3aaf481 -2026-08-10-default-presets-single-editor.zh.md: 5212156a6a3c11e16165eca04bd141337380e172 +2026-08-10-default-presets-single-editor.md: ef18f8d7dd2242c8af53af970df0a06b8d7d09d3 +2026-08-10-default-presets-single-editor.zh.md: c9b57e1ab3fa8d40ac587590f374f1f3747b7e7f diff --git a/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.md b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.md index 3d1c1dea4f..ef18f8d7dd 100644 --- a/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.md +++ b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.md @@ -6,11 +6,11 @@ English | [中文](2026-08-10-default-presets-single-editor.zh.md) ## Problem -The `standard`, `code`, and `cordis` presets exposed both the `read`/`write`/`edit` filesystem tools and `str_replace_editor`. The two interfaces overlap for ordinary file inspection and editing, so every request carried an additional tool schema without adding a distinct default capability. The `minimal` preset has a different composition contract: its exact two-tool roster intentionally includes `str_replace_editor` beside persistent `bash`. +The `standard`, `code`, and `cordis` presets exposed both the `read`/`write`/`edit` filesystem tools and `str_replace_editor`. The two interfaces overlap for ordinary file inspection and editing, so every request carried an additional tool schema without adding a distinct default capability. The `minimal` preset originally retained `str_replace_editor` beside persistent `bash` as a separate composition exception. ## Decision -The `standard`, `code`, and `cordis` preset configurations mount `dsh-tool-fs` and `dsh-tool-fs-search`, but do not mount `dsh-tool-str-replace-editor`. PTC mode therefore omits `str_replace_editor` from both its registry and generated SDK. The `minimal` preset continues to mount `dsh-tool-str-replace-editor`, and deployments or user-authored presets may still mount the plugin explicitly. +The `standard`, `code`, and `cordis` preset configurations mount `dsh-tool-fs` and `dsh-tool-fs-search`, but do not mount `dsh-tool-str-replace-editor`. PTC mode therefore omits `str_replace_editor` from both its registry and generated SDK. The later [persistent-shell-only decision](2026-09-03-minimal-profiles-persistent-shell-only.md) removes the editor from the shipped `minimal` and `sdk-minimal` compositions. Deployments and user-authored presets may still mount the plugin explicitly. This decision narrows the preset roster rather than removing the tool package or its Python runtime support. The earlier [shared-roster decision](../feature/2026-07-31-even-out-shipped-tool-rosters.md) continues to own why surface-neutral tools live in preset composition; this note owns the editor exception. @@ -18,8 +18,8 @@ This decision narrows the preset roster rather than removing the tool package or **Keep both editing interfaces in the general-purpose presets.** Rejected because the overlapping model-visible schemas increase tool choice without supplying a separate default operation. -**Remove `str_replace_editor` from every shipped composition.** Rejected because the `minimal` preset intentionally exposes that schema as one of its two tools, and explicit deployments remain valid consumers of the standalone plugin. +**Remove the `str_replace_editor` package from the distribution.** Rejected because explicit deployments remain valid consumers of the standalone plugin. ## Consequences -General-purpose agents use `read`, `write`, and `edit` for filesystem mutations, while the minimal agent retains `str_replace_editor`. Preset composition tests pin its absence from the standard roster, the Cordis roster, and the PTC mode SDK, while the minimal assertions continue to pin its presence. +General-purpose agents use `read`, `write`, and `edit` for filesystem mutations, while minimal agents use their persistent shell. Preset composition tests pin `str_replace_editor`'s absence from every shipped preset and the PTC mode SDK. diff --git a/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.zh.md b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.zh.md index 5212156a6a..c9b57e1ab3 100644 --- a/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.zh.md +++ b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.zh.md @@ -6,11 +6,11 @@ Status: implemented ## 问题 -`standard`、`code` 和 `cordis` preset 同时提供 `read`/`write`/`edit` 文件系统工具与 `str_replace_editor`。两套接口在常规文件查看和编辑上重叠,导致每次请求都携带额外的工具 schema,却没有增加独立的默认能力。`minimal` preset 具有不同的组合约定:它固定的双工具清单有意在持久 `bash` 之外提供 `str_replace_editor`。 +`standard`、`code` 和 `cordis` preset 同时提供 `read`/`write`/`edit` 文件系统工具与 `str_replace_editor`。两套接口在常规文件查看和编辑上重叠,导致每次请求都携带额外的工具 schema,却没有增加独立的默认能力。`minimal` preset 最初把持久 `bash` 之外的 `str_replace_editor` 保留为单独的组合例外。 ## 决策 -`standard`、`code` 和 `cordis` preset 配置挂载 `dsh-tool-fs` 与 `dsh-tool-fs-search`,但不挂载 `dsh-tool-str-replace-editor`。因此 PTC mode 的注册表和生成的 SDK 均不包含 `str_replace_editor`。`minimal` preset 继续挂载 `dsh-tool-str-replace-editor`,部署配置或用户自定义 preset 仍可显式挂载该插件。 +`standard`、`code` 和 `cordis` preset 配置挂载 `dsh-tool-fs` 与 `dsh-tool-fs-search`,但不挂载 `dsh-tool-str-replace-editor`。因此 PTC mode 的注册表和生成的 SDK 均不包含 `str_replace_editor`。后续的[仅持久 shell 决策](2026-09-03-minimal-profiles-persistent-shell-only.zh.md)会从随附的 `minimal` 与 `sdk-minimal` 组合中移除 editor。部署配置与用户自定义 preset 仍可显式挂载该插件。 此决策收窄 preset 工具清单,不移除工具包及其 Python 运行时支持。较早的[共享清单决策](../feature/2026-07-31-even-out-shipped-tool-rosters.zh.md)继续说明与 surface 无关的工具为何归 preset 组合所有;本记录说明编辑器例外。 @@ -18,8 +18,8 @@ Status: implemented **在通用 preset 中保留两套编辑接口。** 不予采用,因为重叠的模型可见 schema 增加了工具选择,却没有提供不同的默认操作。 -**从所有交付组合中移除 `str_replace_editor`。** 不予采用,因为 `minimal` preset 有意将该 schema 作为两个工具之一,显式部署仍是该独立插件的有效消费方。 +**从发行物中删除 `str_replace_editor` 包。** 不予采用,因为显式部署仍是该独立插件的有效消费方。 ## 后果 -通用 agent 使用 `read`、`write` 和 `edit` 完成文件系统修改,minimal agent 保留 `str_replace_editor`。preset 组合测试固定其不会出现在 standard 清单、Cordis 清单及 PTC mode SDK 中,同时 minimal 断言继续固定其存在。 +通用 agent 使用 `read`、`write` 与 `edit` 完成文件系统修改,minimal agent 则使用持久 shell。Preset 组合测试会固定每份随附 preset 及 PTC mode SDK 都不包含 `str_replace_editor`。 diff --git a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml new file mode 100644 index 0000000000..077fec32c1 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md +2026-09-03-minimal-profiles-persistent-shell-only.md: fbbdd63c3bd8928be49e9eb78f76c5c2824637a0 +2026-09-03-minimal-profiles-persistent-shell-only.zh.md: 1a7cc625a2ce279da96b1d765151f17e63ce4201 diff --git a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md new file mode 100644 index 0000000000..fbbdd63c3b --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md @@ -0,0 +1,33 @@ +# Agent Note: Minimal profiles expose only a persistent shell + +Status: implemented + +English | [中文](2026-09-03-minimal-profiles-persistent-shell-only.zh.md) + +## Problem + +The shipped Web `minimal` preset and standalone `sdk-minimal` profile exposed `str_replace_editor` beside their persistent shell. The editor added a second file-mutation interface and its complete schema to every minimal model request, although the shell already provides file inspection and mutation. It also required a dedicated `fs-local` service that no other row in either minimal composition consumed. + +The next-generation model's minimal runtime requires one model-facing tool. Leaving the editor mounted but hidden through a presentation filter would preserve an inactive capability that could reappear when presentation configuration changes. + +## Decision + +The shipped minimal compositions expose exactly one platform-selected persistent shell: `bash` on Linux and macOS, or `pwsh` on Windows. Neither composition mounts `@deepseek-ai/dsh-tool-str-replace-editor`, a filesystem tool, or the `fs-local` service that supported the editor. The fixed complete persona, absence of runtime context and compaction, shell timeout, and launch-specific host services remain unchanged. + +The standalone editor package and its consumers outside the two minimal compositions remain supported. A trusted user-authored preset or higher profile patch can mount it explicitly; the shipped `minimal` and `sdk-minimal` defaults never insert it. + +Exact composition tests assert the single tool and the absence of a preset-local filesystem service. The `sdk-minimal` bundle test and built config dump assert that its row and dependency allowlists contain neither `fs-local` nor `dsh-tool-str-replace-editor`. Web and packaged-Python model-visible snapshots pin the one-tool schema roster. + +This decision partially supersedes the minimal exceptions in [one editor family in general-purpose presets](2026-08-10-default-presets-single-editor.md), [the minimal preset composition](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md), [the bare minimal runtime](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md), and [the standalone sdk-minimal profile](../architecture/2026-08-24-standalone-sdk-minimal-profile.md). Those notes retain authority for the independent editor package, prompt ownership, no-compaction behavior, profile launch, and bundle layering. + +## Alternatives considered + +**Keep the editor row and hide its schema.** Rejected because a presentation or restriction layer would leave the capability in the minimal composition and make its absence depend on another setting. + +**Remove the editor package from the distribution.** Rejected because full profiles and explicit custom compositions remain valid consumers. The requirement concerns the two shipped minimal defaults. + +**Keep the editor only in `sdk-minimal`.** Rejected because the two minimal paths would present different tool contracts to the same model class, and the packaged SDK path would retain the schema cost and unused filesystem service. + +## Consequences + +Minimal agents inspect and modify files through their persistent shell. Their model requests carry one tool schema, and their compositions own no filesystem service. Full profiles, the editor package, and explicit custom compositions keep their existing behavior. diff --git a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md new file mode 100644 index 0000000000..1a7cc625a2 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md @@ -0,0 +1,33 @@ +# Agent Note: 极简 profile 只提供持久 shell + +Status: implemented + +[English](2026-09-03-minimal-profiles-persistent-shell-only.md) | 中文 + +## 问题 + +随附 Web `minimal` preset 与独立 `sdk-minimal` profile 在持久 shell 之外还提供 `str_replace_editor`。Shell 已经可以检查和修改文件,editor 仍会为每个极简模型请求增加第二种文件修改接口及其完整 schema。它还要求挂载一个专用 `fs-local` 服务,而两份极简组合中的其他配置项都不使用该服务。 + +下一代模型的极简运行时要求只提供一个面向模型的工具。如果保留 editor 的挂载,只通过呈现层过滤隐藏它,那么呈现配置变化时,该能力仍可能重新出现。 + +## 决策 + +随附的极简组合只提供一个按平台选择的持久 shell:Linux 与 macOS 使用 `bash`,Windows 使用 `pwsh`。两份组合都不挂载 `@deepseek-ai/dsh-tool-str-replace-editor`、文件系统工具或支撑 editor 的 `fs-local` 服务。固定的 complete persona、运行时上下文与 compaction 的缺失、shell 超时和各启动路径的宿主服务保持不变。 + +独立 editor 包及其在两份极简组合之外的消费方继续受支持。受信任的用户自定义 preset 或更高优先级的 profile patch 可以显式挂载它;随附的 `minimal` 与 `sdk-minimal` 默认组合不会插入它。 + +精确组合测试会断言单工具清单以及 preset 内不存在文件系统服务。`sdk-minimal` bundle 测试与构建后配置转储会断言配置项和依赖 allowlist 都不含 `fs-local` 或 `dsh-tool-str-replace-editor`。Web 与打包 Python 的模型可见快照会固定单工具 schema 清单。 + +该决策部分取代[通用 preset 使用一种 editor](2026-08-10-default-presets-single-editor.zh.md)、[minimal preset 组合](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md)、[极简裸运行时](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md)与[独立 sdk-minimal profile](../architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md)中的极简例外。这些 Agent Note 继续负责独立 editor 包、提示词所有权、无 compaction 行为、profile 启动与 bundle 分层。 + +## 考虑过的替代方案 + +**保留 editor 配置项并隐藏其 schema。** 不予采用,因为呈现层或限制层会让该能力继续留在极简组合中,并使其缺失依赖另一项设置。 + +**从发行物中删除 editor 包。** 不予采用,因为完整 profile 与显式自定义组合仍是有效消费方。本需求只涉及两份随附的极简默认组合。 + +**只在 `sdk-minimal` 中保留 editor。** 不予采用,因为两条极简路径会向同类模型提供不同的工具约定,而且打包 SDK 路径仍会承担 schema 成本和未被其他配置项使用的文件系统服务。 + +## 后果 + +极简 agent 通过持久 shell 检查和修改文件。模型请求只携带一个工具 schema,组合不拥有文件系统服务。完整 profile、editor 包与显式自定义组合保持现有行为。 diff --git a/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.i18n.yaml b/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.i18n.yaml index 534ca917cc..6e53b407d9 100644 --- a/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.i18n.yaml +++ b/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.md -2026-08-13-python-minimal-model-visible-snapshot.md: 1cef57c440ddd2209628016ab550174b448587bc -2026-08-13-python-minimal-model-visible-snapshot.zh.md: fbbb3bd6f4c3edadb61a02eebbae3dfaa97cb8e7 +2026-08-13-python-minimal-model-visible-snapshot.md: becfbae8add8b1ac5b1b08b31294d30ed2e40ba8 +2026-08-13-python-minimal-model-visible-snapshot.zh.md: c2e959d5974f897220fc7be1f96917c7bf43d704 diff --git a/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.md b/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.md index 1cef57c440..becfbae8ad 100644 --- a/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.md +++ b/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.md @@ -10,22 +10,22 @@ The Python lane needs an exact record of what the standalone minimal profile sho ## Decision -The `sdk-minimal` scenario in [the packaged-runtime smoke](../../../../scripts/smoke-python-runtime.py) boots the shipped profile and records `scripts/snapshots/python-sdk-single-exe/minimal/model-visible.json`: for every model request of the turn, the advertised tool schemas verbatim and the message list. System and user messages keep their full text with the scenario's temporary directory tokenized; assistant and tool messages keep only call identity, because their PTY and filesystem text differs across replay platforms. The profile omits dynamic runtime context, so every message it emits is compared. +The `sdk-minimal` scenario in [the packaged-runtime smoke](../../../../scripts/smoke-python-runtime.py) boots the shipped profile and records `scripts/snapshots/python-sdk-single-exe/minimal/model-visible.json`: for every model request of the turn, the advertised tool schemas verbatim and the message list. System and user messages keep their full text with the scenario's temporary directory tokenized; assistant and tool messages keep only call identity, because their PTY text differs across replay platforms. The profile omits dynamic runtime context, so every message it emits is compared. The snapshot, rather than inline mock-model assertions, owns the minimal scenario's tools and system prompts and reports their complete diff. Snapshot comparison takes its directory and file set as arguments, so the `minimal` and `advanced` expected outputs use one implementation, and `--update-snapshots` accepts `sdk-minimal`. ## Alternatives considered -**Snapshot the minimal session log, like the advanced scenario.** The minimal turn drives a real PTY and editor, so persisted tool results carry platform-dependent text. The expected output would go red for reasons unrelated to model-visible assembly, and normalizing that text away leaves the log carrying little the model-visible file does not. +**Snapshot the minimal session log, like the advanced scenario.** The minimal turn drives a real PTY, so persisted tool results carry platform-dependent text. The expected output would go red for reasons unrelated to model-visible assembly, and normalizing that text away leaves the log carrying little the model-visible file does not. **Extend the mock model's inline assertions.** Every new model-visible contribution would need another hand-written expectation, and a failure names one mismatch rather than the whole surface. Tool descriptions would also be duplicated from the composition into the script. -**Rely on the TypeScript SDK snapshot.** Its `persistent-tools` scenario pins a similar two-tool composition through replayed model responses and a source or `lib` runtime, in a different required job. It cannot show what the deployed executable's shipped profile assembles for a Python caller. +**Rely on the TypeScript SDK snapshot.** Its `persistent-tools` scenario pins a broader tool composition through replayed model responses and a source or `lib` runtime, in a different required job. It cannot show what the deployed executable's shipped profile assembles for a Python caller. ## Consequences A change to the minimal composition's model-visible surface — a system section, a tool, a tool description, or an added user message — now fails `python-runtime` with the exact diff, and landing it means rerunning `--scenario sdk-minimal --update-snapshots` and reviewing that diff. The minimal composition's tool descriptions become reviewed expected output. -Assistant and tool message text is not compared. The scenario's own assertions continue to own persistent-shell state, editor output, and the final response; the snapshot owns every model-visible message the profile emits. +Assistant and tool message text is not compared. The scenario's own assertions continue to own persistent-shell state and the final response; the snapshot owns every model-visible message the profile emits. [AGENTS.md](../../../../AGENTS.md) and [the testing policy](../../../../docs/testing.md) now name both SDKs as independent projections of the agent loop, session lifecycle, and `SessionEventMap`, so a change to any of those carries updating both expected outputs rather than only the one a contributor happens to run. diff --git a/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.zh.md b/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.zh.md index fbbb3bd6f4..c2e959d597 100644 --- a/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.zh.md +++ b/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.zh.md @@ -10,22 +10,22 @@ Python 通道需要精确记录独立极简 profile 实际展示给模型的内 ## 决策 -[打包运行时冒烟测试](../../../../scripts/smoke-python-runtime.py)的 `sdk-minimal` 场景会启动随附 profile,并录制 `scripts/snapshots/python-sdk-single-exe/minimal/model-visible.json`:对该回合的每个模型请求,逐字记录对外公布的工具 schema 与消息列表。system 与 user 消息保留全文,仅将场景的临时目录替换为占位符;assistant 与 tool 消息只保留调用标识,因为它们的 PTY 与文件系统文本在各回放平台上并不相同。该 profile 省略动态运行时上下文,因此它发出的每条消息都会参与比对。 +[打包运行时冒烟测试](../../../../scripts/smoke-python-runtime.py)的 `sdk-minimal` 场景会启动随附 profile,并录制 `scripts/snapshots/python-sdk-single-exe/minimal/model-visible.json`:对该回合的每个模型请求,逐字记录对外公布的工具 schema 与消息列表。system 与 user 消息保留全文,仅将场景的临时目录替换为占位符;assistant 与 tool 消息只保留调用标识,因为它们的 PTY 文本在各回放平台上并不相同。该 profile 省略动态运行时上下文,因此它发出的每条消息都会参与比对。 极简场景的工具与系统提示词由快照拥有,而不是由 mock 模型内联断言;快照会给出其完整差异。快照比对以目录与文件集合为参数,因此 `minimal` 与 `advanced` 两份期望输出共用一套实现,且 `--update-snapshots` 接受 `sdk-minimal`。 ## 曾考虑的替代方案 -**像进阶场景那样对极简会话日志做快照。** 极简回合驱动真实 PTY 与编辑器,持久化的工具结果带有平台相关文本。期望输出会因与模型可见组装无关的原因变红;而把这些文本归一化掉之后,日志所承载的内容也就所剩无几。 +**像进阶场景那样对极简会话日志做快照。** 极简回合驱动真实 PTY,持久化的工具结果带有平台相关文本。期望输出会因与模型可见组装无关的原因变红;而把这些文本归一化掉之后,日志所承载的内容也就所剩无几。 **扩展 mock 模型中的内联断言。** 每新增一项模型可见贡献都要再手写一条期望,且失败只会指出一处不匹配而非整个面。工具描述还会从组合复制进脚本,形成重复。 -**依赖 TypeScript SDK 快照。** 其 `persistent-tools` 场景通过重放模型响应与 source 或 `lib` 运行时固定一套相似的双工具组合,且位于另一个必需任务中。它无法体现已部署可执行文件的随附 profile 为 Python 调用方组装出什么。 +**依赖 TypeScript SDK 快照。** 其 `persistent-tools` 场景通过重放模型响应与 source 或 `lib` 运行时固定一套更广的工具组合,且位于另一个必需任务中。它无法体现已部署可执行文件的随附 profile 为 Python 调用方组装出什么。 ## 后果 极简组合模型可见面的改动——系统分段、工具、工具描述或新增的 user 消息——现在会让 `python-runtime` 带着精确差异失败;要让它落地,就必须重新运行 `--scenario sdk-minimal --update-snapshots` 并审阅该差异。极简组合的工具描述由此成为经过审阅的期望输出。 -assistant 与 tool 消息文本不参与比对。持久 shell 状态、编辑器输出与最终响应仍由该场景自身的断言拥有;快照负责该 profile 发出的每条模型可见消息。 +assistant 与 tool 消息文本不参与比对。持久 shell 状态与最终响应仍由该场景自身的断言拥有;快照负责该 profile 发出的每条模型可见消息。 [AGENTS.md](../../../../AGENTS.md) 与[测试政策](../../../../docs/testing.zh.md)现已点明两个 SDK 都是 agent loop、会话生命周期与 `SessionEventMap` 的独立投影,因此改动其中任何一项都要连带更新两侧的期望输出,而不只是贡献者恰好会运行的那一侧。 diff --git a/apps/cli/reference/README.i18n.yaml b/apps/cli/reference/README.i18n.yaml index 37aef6b7ae..11c71484dd 100644 --- a/apps/cli/reference/README.i18n.yaml +++ b/apps/cli/reference/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write apps/cli/reference/README.md -README.md: 78328be4d614ebf647d6f00a6f0d66978919da3e -README.zh.md: c140c3ba528509611ae17712851c6d406c190972 +README.md: 9537920e02696f822945fa6f6a3b4dcd723c077f +README.zh.md: 2f58a9ed211810574e0657512b177fcda93940c9 diff --git a/apps/cli/reference/README.md b/apps/cli/reference/README.md index 78328be4d6..9537920e02 100644 --- a/apps/cli/reference/README.md +++ b/apps/cli/reference/README.md @@ -81,11 +81,11 @@ The production Web runner needs built package and frontend artifacts (`pnpm run Process shutdown gives the plugin tree up to five seconds to dispose. The first `SIGINT`/`SIGTERM` starts that graceful drain — `SIGTERM` is a supervisor's ordinary stop request and exits 0 on every surface, `SIGINT` reports 130; a second signal forces immediate exit. If one-shot normal completion is already stuck in disposal, the first `Ctrl+C` is the escalation and exits immediately instead of being swallowed. -The base-backed modes treat the invoking directory as the default workspace root, load applicable `AGENTS.md` or `CLAUDE.md` instructions with a 65,536-byte render budget, and use an in-memory SQLite session content index. The standalone `sdk-minimal` profile uses the invoking directory as its local filesystem and sandbox-policy root but intentionally omits instruction discovery and SQLite. A `patchReload: live` profile watches valid edits of both `cordis.patch.yml` layers (profile and home) and reapplies them transactionally; a `startup` profile applies them once. A one-shot surface exits through its bounded shutdown, which disposes any live watchers. +The base-backed modes treat the invoking directory as the default workspace root, load applicable `AGENTS.md` or `CLAUDE.md` instructions with a 65,536-byte render budget, and use an in-memory SQLite session content index. The standalone `sdk-minimal` profile uses the invoking directory as its sandbox-policy root but intentionally omits filesystem tools, instruction discovery, and SQLite. A `patchReload: live` profile watches valid edits of both `cordis.patch.yml` layers (profile and home) and reapplies them transactionally; a `startup` profile applies them once. A one-shot surface exits through its bounded shutdown, which disposes any live watchers. New sessions in base-backed profiles default to the `workspace-write` permission preset. Bash and filesystem mutations are restricted to the session workspace and platform temporary roots; reads and network access are not confined, while process visibility depends on the selected sandbox backend — bwrap runs commands in a private PID namespace that hides host processes, and Landlock and Seatbelt leave host process visibility unchanged. `DSH_PERMISSION_MODE` changes the process fallback. Stored General-settings permissions affect later Web sessions, not an already-open one. The standalone `sdk-minimal` tree instead pins `danger-full-access` and mounts no approval or permission-settings service. -`DSH_TOOLS_MODE` selects `native`, `ptc`, or `both` for the process; another value fails at boot. The shipped `minimal` agent preset keeps that deployment presentation, fixes the complete system prompt to `You are a helpful software engineer assistant.`, and composes only persistent `bash` plus `str_replace_editor`. Select 极简模式 when creating a Web session; every other prompt section and model-facing plugin remains absent from that agent while the shared browser, workspace, persistence, sandbox, and permission host stays in place. +`DSH_TOOLS_MODE` selects `native`, `ptc`, or `both` for the process; another value fails at boot. The shipped `minimal` agent preset keeps that deployment presentation, fixes the complete system prompt to `You are a helpful software engineer assistant.`, and composes only the platform-selected persistent shell. Select 极简模式 when creating a Web session; every other prompt section and model-facing plugin remains absent from that agent while the shared browser, workspace, persistence, sandbox, and permission host stays in place. ## Shared deployment behavior diff --git a/apps/cli/reference/README.zh.md b/apps/cli/reference/README.zh.md index c140c3ba52..2f58a9ed21 100644 --- a/apps/cli/reference/README.zh.md +++ b/apps/cli/reference/README.zh.md @@ -81,11 +81,11 @@ dsh web --help 进程关闭时,插件树最多有 5 秒完成 dispose。首次收到 `SIGINT` 或 `SIGTERM` 时会开始优雅排空:`SIGTERM` 是监督进程发出的常规停止请求,在所有运行模式下都以 0 退出;`SIGINT` 则报告 130。第二次收到信号时会立即强制退出。如果一次性运行在正常结束时已经卡在 dispose 阶段,第一次按下 `Ctrl+C` 就会直接升级为强制退出,而不会被忽略。 -基于 base 的模式都将运行命令时所在的目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md` 或 `CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。独立的 `sdk-minimal` profile 把运行命令时所在的目录作为本地文件系统与沙箱策略根目录,但刻意省略指令发现与 SQLite。`patchReload: live` profile 会监视 profile 与 home 两个 `cordis.patch.yml` 配置层的有效变更,并以事务方式重新应用;`startup` profile 则只应用一次。一次性运行模式通过有界关闭流程退出,该流程会 dispose(资源释放)所有实时监视器。 +基于 base 的模式都将运行命令时所在的目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md` 或 `CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。独立的 `sdk-minimal` profile 把运行命令时所在的目录作为沙箱策略根目录,但刻意省略文件系统工具、指令发现与 SQLite。`patchReload: live` profile 会监视 profile 与 home 两个 `cordis.patch.yml` 配置层的有效变更,并以事务方式重新应用;`startup` profile 则只应用一次。一次性运行模式通过有界关闭流程退出,该流程会 dispose(资源释放)所有实时监视器。 基于 base 的 profile 中,新会话默认使用 `workspace-write` 权限预设。Bash 和文件系统修改仅限于会话 workspace 与平台临时根目录;读取和网络访问不受限制,进程可见性则取决于所选沙箱后端——bwrap 在私有 PID 命名空间中运行命令并隐藏宿主进程,Landlock 与 Seatbelt 保持宿主进程可见性不变。`DSH_PERMISSION_MODE` 更改进程后备值。General settings 中存储的权限影响后续 Web 会话,不改变已打开的会话。独立的 `sdk-minimal` 配置树则固定为 `danger-full-access`,且不挂载 approval 或权限 settings 服务。 -`DSH_TOOLS_MODE` 为进程选择 `native`、`ptc` 或 `both`;其他值会导致启动失败。随附的 `minimal` agent preset 会保留该部署的呈现方式,将完整系统提示词固定为 `You are a helpful software engineer assistant.`,并且仅组合持久 `bash` 和 `str_replace_editor`。创建 Web 会话时请选择极简模式;该 agent 不包含任何其他提示词段落或面向模型的插件,而共享的浏览器、workspace、持久化、沙箱与权限宿主保持不变。 +`DSH_TOOLS_MODE` 为进程选择 `native`、`ptc` 或 `both`;其他值会导致启动失败。随附的 `minimal` agent preset 会保留该部署的呈现方式,将完整系统提示词固定为 `You are a helpful software engineer assistant.`,并且仅组合按平台选择的持久 shell。创建 Web 会话时请选择极简模式;该 agent 不包含任何其他提示词段落或面向模型的插件,而共享的浏览器、workspace、持久化、沙箱与权限宿主保持不变。 ## 共享部署行为 diff --git a/apps/cli/tests/built-bin.e2e.ts b/apps/cli/tests/built-bin.e2e.ts index b3a3ef6a52..6a065b05c4 100644 --- a/apps/cli/tests/built-bin.e2e.ts +++ b/apps/cli/tests/built-bin.e2e.ts @@ -956,7 +956,6 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', ['pty', '@deepseek-ai/dsh-terminal'], ['terminal-bash', '@deepseek-ai/dsh-terminal-bash'], ['terminal-pwsh', '@deepseek-ai/dsh-terminal-bash'], - ['fs-local', '@deepseek-ai/dsh-fs-local'], ['timer', '@deepseek-ai/cordis-plugin-timer'], ['llm', '@deepseek-ai/dsh-llm'], ['session', '@deepseek-ai/dsh-session'], @@ -974,7 +973,6 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', ['agent-loop', '@deepseek-ai/dsh-agent-loop'], ['persistent-bash', '@deepseek-ai/dsh-tool-bash-persistent'], ['persistent-pwsh', '@deepseek-ai/dsh-tool-pwsh-persistent'], - ['str-replace-editor', '@deepseek-ai/dsh-tool-str-replace-editor'], ['sessions', '@deepseek-ai/dsh-session-persistence-jsonl'], ]) expect(stdout).toContain('# == @deepseek-ai/dsh-sdk-minimal') diff --git a/apps/cli/tests/web-agent-presets.e2e.ts b/apps/cli/tests/web-agent-presets.e2e.ts index 6c2ac3db7b..482412b93a 100644 --- a/apps/cli/tests/web-agent-presets.e2e.ts +++ b/apps/cli/tests/web-agent-presets.e2e.ts @@ -179,8 +179,8 @@ describe('the shipped Web composition', () => { it('leaves the global tool layer empty', () => { // Every model-facing tool belongs to a preset, `ask_user_question` // included: a tool in the global layer reaches EVERY agent regardless of - // which preset composed it, so a two-tool benchmark surface would really - // present three. A regression here means an agent-plane row came back to + // which preset composed it, so a single-tool benchmark surface would really + // present two. A regression here means an agent-plane row came back to // the host composition. expect(toolNames(ctx)).toEqual([]) }) @@ -277,7 +277,7 @@ describe('the shipped Web composition', () => { } }) - it('composes the exact RL prompt and two tools from `minimal`', async () => { + it('composes the exact RL prompt and persistent shell from `minimal`', async () => { const handle = await ctx.agents.create({ sessionId: SessionId('preset-minimal'), setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined), @@ -287,11 +287,10 @@ describe('the shipped Web composition', () => { expect(assembly.sections).toEqual([ { name: 'deployment:persona', text: MINIMAL_PROMPT }, ]) - expect(assembly.tools.map(tool => tool.name)).toEqual(['bash', 'str_replace_editor']) + expect(assembly.tools.map(tool => tool.name)).toEqual(['bash']) expect(assembly.tools.find(tool => tool.name === 'bash')?.description).toBe(MINIMAL_BASH_DESCRIPTION) - expect(JSON.stringify(assembly.tools.find(tool => tool.name === 'str_replace_editor')?.parameters)) - .toContain('Absolute path') expect(ctx.commands.find(handle.agent, 'goal')).toBeUndefined() + expect(ctx.agentPresets.serviceFor(handle.agent, 'fs')).toBeUndefined() expect(ctx.agentPresets.serviceFor(handle.agent, 'compaction')).toBeUndefined() expect(handle.agent.ctx.get('compaction')).toBeUndefined() } finally { @@ -309,7 +308,7 @@ describe('the shipped Web composition', () => { setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined), }) try { - expect(toolNames(ctx, minimal.agent)).toEqual(['bash', 'str_replace_editor']) + expect(toolNames(ctx, minimal.agent)).toEqual(['bash']) expect(toolNames(ctx, full.agent).length).toBeGreaterThan(10) await minimal.dispose() @@ -460,7 +459,7 @@ describe('the shipped Web composition', () => { // stays the preset's choice — minimal mounts no `tool-skill`, so its // tool table has no loader even though the global layer is readable. expect((await ctx.skills.list({ scope: handle.agent })).map(skill => skill.name)).toContain('dsh-badge') - expect(toolNames(ctx, handle.agent)).toEqual(['bash', 'str_replace_editor']) + expect(toolNames(ctx, handle.agent)).toEqual(['bash']) } finally { await handle.dispose() } @@ -839,7 +838,7 @@ describe('authoring a preset on the shipped composition', () => { try { // The same tools the shipped `minimal` composes, from a directory copied // through the service into a root outside the installed harness. - expect(toolNames(authorCtx, handle.agent)).toEqual(['bash', 'str_replace_editor']) + expect(toolNames(authorCtx, handle.agent)).toEqual(['bash']) } finally { await handle.dispose() } @@ -874,9 +873,9 @@ describe('the default preset as a user setting', () => { setup: agentCtx => ctx.agentPresets.mount(agentCtx).then(() => undefined), }) try { - // `mount()` with no id resolves the effective default. Two tools, not + // `mount()` with no id resolves the effective default. One tool, not // `standard`'s catalog: the setting decided the composition. - expect(toolNames(ctx, handle.agent)).toEqual(['bash', 'str_replace_editor']) + expect(toolNames(ctx, handle.agent)).toEqual(['bash']) } finally { await handle.dispose() } @@ -901,7 +900,7 @@ describe('a session keeps the preset it was created with', () => { try { // The api-proxy guard reads exactly this: the header records what the // session runs, so naming anything else is a caller error rather than a - // switch. Its history was produced under `minimal`'s two tools. + // switch. Its history was produced under `minimal`'s single tool. expect(handle.agent.session.header.agentPreset).toBe('minimal') } finally { await handle.dispose() @@ -963,7 +962,7 @@ describe('a composition that configures its own preset roots', () => { setup: agentCtx => rootsCtx.agentPresets.mount(agentCtx, 'team-spec').then(() => undefined), }) try { - expect(toolNames(rootsCtx, handle.agent)).toEqual(['bash', 'str_replace_editor']) + expect(toolNames(rootsCtx, handle.agent)).toEqual(['bash']) } finally { await handle.dispose() } diff --git a/apps/web/tests/agent-preset-authoring.e2e.ts b/apps/web/tests/agent-preset-authoring.e2e.ts index 72b8317fe4..218e542090 100644 --- a/apps/web/tests/agent-preset-authoring.e2e.ts +++ b/apps/web/tests/agent-preset-authoring.e2e.ts @@ -159,7 +159,7 @@ describe('web e2e: agent-preset authoring is a host-side copy', () => { expect(composition).toBe(await readFile(join(SHIPPED_PRESETS, 'minimal', 'agent.cordis.yml'), 'utf8')) const metadata = await readFile(join(userRoot, 'my-agent', 'preset.yml'), 'utf8') expect(metadata).toContain('name: 我的模式') - expect(metadata).toContain('description: 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。') + expect(metadata).toContain('description: 仅提供持久 shell 的单工具编码 Agent。') expect(metadata).not.toContain('order:') }, 60_000) diff --git a/apps/web/tests/expected/agent-preset-authoring/created.expected.md b/apps/web/tests/expected/agent-preset-authoring/created.expected.md index 37dc43d9a6..f9daab0c26 100644 --- a/apps/web/tests/expected/agent-preset-authoring/created.expected.md +++ b/apps/web/tests/expected/agent-preset-authoring/created.expected.md @@ -43,7 +43,7 @@ - text: 复制 - listitem: - 'button "设为默认: 极简模式"': - - text: 极简模式 内置 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。 + - text: 极简模式 内置 仅提供持久 shell 的单工具编码 Agent。 - code: minimal - 'button "查看: 极简模式"': - img @@ -65,7 +65,7 @@ - list: - listitem: - 'button "设为默认: 我的模式"': - - text: 我的模式 自定义 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。 + - text: 我的模式 自定义 仅提供持久 shell 的单工具编码 Agent。 - code: my-agent - 'button "查看路径: 我的模式"': - img diff --git a/apps/web/tests/expected/agent-preset-authoring/damaged.expected.md b/apps/web/tests/expected/agent-preset-authoring/damaged.expected.md index 7f8537f392..cfae761ba3 100644 --- a/apps/web/tests/expected/agent-preset-authoring/damaged.expected.md +++ b/apps/web/tests/expected/agent-preset-authoring/damaged.expected.md @@ -43,7 +43,7 @@ - text: 复制 - listitem: - 'button "设为默认: 极简模式"': - - text: 极简模式 内置 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。 + - text: 极简模式 内置 仅提供持久 shell 的单工具编码 Agent。 - code: minimal - 'button "查看: 极简模式"': - img diff --git a/apps/web/tests/expected/agent-preset-authoring/section.expected.md b/apps/web/tests/expected/agent-preset-authoring/section.expected.md index e6c83143d7..7efe280eb5 100644 --- a/apps/web/tests/expected/agent-preset-authoring/section.expected.md +++ b/apps/web/tests/expected/agent-preset-authoring/section.expected.md @@ -43,7 +43,7 @@ - text: 复制 - listitem: - 'button "设为默认: 极简模式"': - - text: 极简模式 内置 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。 + - text: 极简模式 内置 仅提供持久 shell 的单工具编码 Agent。 - code: minimal - 'button "查看: 极简模式"': - img diff --git a/apps/web/tests/expected/agent-preset-selection/menu.expected.md b/apps/web/tests/expected/agent-preset-selection/menu.expected.md index c89bfd50a5..7c05a3332a 100644 --- a/apps/web/tests/expected/agent-preset-selection/menu.expected.md +++ b/apps/web/tests/expected/agent-preset-selection/menu.expected.md @@ -3,6 +3,6 @@ - text: Standard mode Full coding agent with file editing, shell, file and web search, skills, planning, goals, subagents, and workflows. - img - menuitem "PTC mode Full coding agent without the workflow tool; other tools are exposed through the PTC mode SDK so the model can combine multi-step operations in one TypeScript program." - - menuitem "Minimal mode Two-tool coding agent with persistent bash and str_replace_editor." + - menuitem "Minimal mode Single-tool coding agent with a persistent shell." - menuitem "Creator mode Built for creating custom agent presets, with all Standard mode capabilities plus runtime inspection, plugin experiments, and preset-authoring guidance." - menuitem "Refusing mode Resolves, then refuses to start." diff --git a/apps/web/tests/minimal-preset.snapshot.ts b/apps/web/tests/minimal-preset.snapshot.ts index 19f829d5ac..6c966bd3c3 100644 --- a/apps/web/tests/minimal-preset.snapshot.ts +++ b/apps/web/tests/minimal-preset.snapshot.ts @@ -1,4 +1,4 @@ -import { mkdir, writeFile } from 'node:fs/promises' +import { mkdir } from 'node:fs/promises' import { join } from 'node:path' import { fileURLToPath } from 'node:url' import type { Browser, Page } from 'playwright' @@ -69,15 +69,13 @@ describe('minimal agent preset', () => { if (failures.length > 1) throw new AggregateError(failures, 'minimal preset smoke teardown failed') }) - it('sends the exact RL prompt and schemas, then executes the persistent shell and editor', async () => { + it('sends the exact RL prompt and shell schema, then executes the persistent shell', async () => { const requestHeader = agentHandle.agent.session.requestHeader() if (requestHeader === undefined) throw new Error('the minimal agent issued no model request') expect(agentHandle.agent.session.snapshotEvents().some(event => event.type === 'user/message' && event.data.source.kind === 'plugin' && event.data.source.plugin === '@deepseek-ai/dsh-system-prompt')).toBe(false) - const presetFileSystem = scaffold.ctx.agentPresets.serviceFor(agentHandle.agent, 'fs') - expect(presetFileSystem).toBeDefined() - expect(presetFileSystem?.sandboxMode).toBeUndefined() + expect(scaffold.ctx.agentPresets.serviceFor(agentHandle.agent, 'fs')).toBeUndefined() expect(scaffold.ctx.agentPresets.serviceFor(agentHandle.agent, 'compaction')).toBeUndefined() const stateDir = join(scaffold.workspaceCwd, 'persistent-state') @@ -97,16 +95,6 @@ describe('minimal agent preset', () => { arguments: { command: 'printf \'%s:%s\n\' "$DSH_MINIMAL_STATE" "$PWD"' }, agent: agentHandle.agent, }) - const seedPath = join(scaffold.workspaceCwd, 'preset-smoke.txt') - await writeFile(seedPath, 'MINIMAL_EDITOR_OK\n') - const editor = await scaffold.ctx.tools.execute({ - signal, - callId: ToolCallId('minimal-editor-smoke'), - name: 'str_replace_editor', - arguments: { command: 'view', path: seedPath }, - agent: agentHandle.agent, - }) - const text = (result: typeof bash): string => result.content .filter(block => block.type === 'text') .map(block => block.text) @@ -119,18 +107,13 @@ describe('minimal agent preset', () => { tools: requestHeader.tools?.map(tool => tool.name), goalCommand: scaffold.ctx.commands.find(agentHandle.agent, 'goal') !== undefined, bash: text(bash), - editor: text(editor), }).toMatchInlineSnapshot(` { "bash": "PERSISTED:{{cwd}}/persistent-state", - "editor": "Here's the content of {{cwd}}/preset-smoke.txt with line numbers (which has a total of 2 lines): - 1 MINIMAL_EDITOR_OK - 2", "goalCommand": false, "prompt": "You are a helpful software engineer assistant.", "tools": [ "bash", - "str_replace_editor", ], } `) diff --git a/docs/user/guide/python-sdk.i18n.yaml b/docs/user/guide/python-sdk.i18n.yaml index 04933034ac..1634954baf 100644 --- a/docs/user/guide/python-sdk.i18n.yaml +++ b/docs/user/guide/python-sdk.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/guide/python-sdk.md -python-sdk.md: 88cf32f60c285a4ce7c09e424532a6d9b7b00890 -python-sdk.zh.md: e443ec811a3b42034c75162b759408f1acd3486f +python-sdk.md: ee187ac2ef0156c56b6cdea8a2e0fddc32ac8ec2 +python-sdk.zh.md: 961ff2f9f0170cf924fa10e6f0825b5c47e427ef diff --git a/docs/user/guide/python-sdk.md b/docs/user/guide/python-sdk.md index 88cf32f60c..ee187ac2ef 100644 --- a/docs/user/guide/python-sdk.md +++ b/docs/user/guide/python-sdk.md @@ -135,13 +135,12 @@ Another `profile` is valid when it includes `@deepseek-ai/dsh-sdk-app` or anothe |---|---| | System prompt | `DSH_SYSTEM_PROMPT`, falling back to `You are a helpful software engineer assistant.` | | Model in `minimal.py` | `--model`, then `DSH_MODEL`, then `deepseek-v4-flash` | -| Model-facing tools | Persistent `bash` on Linux/macOS or `pwsh` on Windows, plus `str_replace_editor` | +| Model-facing tool | Persistent `bash` on Linux/macOS or `pwsh` on Windows | | Shell timeout | 300 seconds | -| Editor output limit | 16,000 characters | | Runtime context and compaction | Absent | | Session persistence | Uncompressed JSONL under `/sessions` | -The profile's sole bundle inserts the complete tree over an empty root and does not include `dsh-base`; later base-profile tools therefore cannot appear implicitly. It contains the SDK protocol, one environment-configured DeepSeek adapter, local execution, and persistence, while settings, managed credentials, telemetry, Web tools, subagents, local instruction discovery, and compaction are absent. It pins `danger-full-access`, so the platform-selected persistent shell and editor can modify any path visible to the runtime; use a disposable checkout or container. +The profile's sole bundle inserts the complete tree over an empty root and does not include `dsh-base`; later base-profile tools therefore cannot appear implicitly. It contains the SDK protocol, one environment-configured DeepSeek adapter, local execution, and persistence, while filesystem tools, settings, managed credentials, telemetry, Web tools, subagents, local instruction discovery, and compaction are absent. It pins `danger-full-access`, so the platform-selected persistent shell can modify any path visible to the runtime; use a disposable checkout or container. The installed wheel still packages the full `web` profile and frontend assets. Run `dsh web` against an explicit `DSH_HOME` when a Python SDK deployment also needs the browser application; `web` is a separate CLI application and cannot serve a Python SDK client. diff --git a/docs/user/guide/python-sdk.zh.md b/docs/user/guide/python-sdk.zh.md index e443ec811a..961ff2f9f0 100644 --- a/docs/user/guide/python-sdk.zh.md +++ b/docs/user/guide/python-sdk.zh.md @@ -135,13 +135,12 @@ dsh plugin --profile sdk-minimal add file:C:/work/my-plugin-bundle |---|---| | 系统提示词 | `DSH_SYSTEM_PROMPT`,未设置时为 `You are a helpful software engineer assistant.` | | `minimal.py` 的模型 | `--model`,然后是 `DSH_MODEL`,最后是 `deepseek-v4-flash` | -| 面向模型的工具 | Linux/macOS 上的持久 `bash` 或 Windows 上的 `pwsh`,以及 `str_replace_editor` | +| 面向模型的工具 | Linux/macOS 上的持久 `bash` 或 Windows 上的 `pwsh` | | Shell 超时 | 300 秒 | -| Editor 输出上限 | 16,000 字符 | | 运行时上下文与 compaction | 不存在 | | 会话持久化 | `/sessions` 下的未压缩 JSONL | -该 profile 的唯一组合包会在空根之上插入完整配置树,且不包含 `dsh-base`,因此基础 profile 以后新增的工具不会隐式出现。它包含 SDK 协议、一个由环境配置的 DeepSeek 适配器、本地执行与持久化;settings、托管凭据、遥测、Web 工具、subagent、本地指令发现和 compaction 均不存在。它固定使用 `danger-full-access`,因此按平台选择的持久 shell 与 editor 可以修改运行时可见的任何路径;应使用一次性 checkout 或容器。 +该 profile 的唯一组合包会在空根之上插入完整配置树,且不包含 `dsh-base`,因此基础 profile 以后新增的工具不会隐式出现。它包含 SDK 协议、一个由环境配置的 DeepSeek 适配器、本地执行与持久化;文件系统工具、settings、托管凭据、遥测、Web 工具、subagent、本地指令发现和 compaction 均不存在。它固定使用 `danger-full-access`,因此按平台选择的持久 shell 可以修改运行时可见的任何路径;应使用一次性 checkout 或容器。 已安装 wheel 仍会打包完整 `web` profile 与前端产物。如果 Python SDK 部署还需要浏览器应用,请针对显式 `DSH_HOME` 运行 `dsh web`;`web` 是独立 CLI 应用,不能为 Python SDK client 提供服务。 diff --git a/packages/bundle/sdk-minimal/README.i18n.yaml b/packages/bundle/sdk-minimal/README.i18n.yaml index cac1c4c770..b4894c1932 100644 --- a/packages/bundle/sdk-minimal/README.i18n.yaml +++ b/packages/bundle/sdk-minimal/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/sdk-minimal/README.md -README.md: 3be71c7e319f70bddda716c92ade14336db0a065 -README.zh.md: dff652f53d0a4fd6d50c216e564c46f81f4a188b +README.md: e67b1eb0637f870f47bd97ecff7133242e2f2210 +README.zh.md: 077d3ac0f603ce6781476b4850306d8f8486f82a diff --git a/packages/bundle/sdk-minimal/README.md b/packages/bundle/sdk-minimal/README.md index 3be71c7e31..e67b1eb063 100644 --- a/packages/bundle/sdk-minimal/README.md +++ b/packages/bundle/sdk-minimal/README.md @@ -1,5 +1,5 @@ --- -description: "Standalone two-tool SDK profile for users who need a minimal cross-platform coding agent without the shared base bundle." +description: "Standalone single-tool SDK profile for users who need a minimal cross-platform coding agent without the shared base bundle." kind: "package-bundle" --- @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -Use `dsh --profile sdk-minimal` when an SDK client needs a small, explicit coding-agent runtime. The profile advertises a platform-selected persistent shell and `str_replace_editor`, persists sessions as uncompressed JSONL, and selects the model from the SDK initialization request. It supplies a complete Cordis tree and deliberately excludes `dsh-base`, Web, settings, managed credentials, telemetry, compaction, workspace instructions, skills, jobs, and subagents. Its danger-full-access policy lets the shell and editor modify any path available to the process, so use it only with an isolated workspace. +Use `dsh --profile sdk-minimal` when an SDK client needs a small, explicit coding-agent runtime. The profile advertises only a platform-selected persistent shell, persists sessions as uncompressed JSONL, and selects the model from the SDK initialization request. It supplies a complete Cordis tree and deliberately excludes `dsh-base`, Web, settings, managed credentials, telemetry, compaction, filesystem tools, workspace instructions, skills, jobs, and subagents. Its danger-full-access policy lets the shell modify any path available to the process, so use it only with an isolated workspace. ## Table of Contents @@ -46,7 +46,7 @@ The profile mounts exactly one persistent shell stack: Bash on Linux and macOS,
Implementation internals — click to expand -The bundle's single insert is the complete application tree: SDK stdio startup and JSON-RPC serving, one environment-configured DeepSeek adapter, the explicit agent core, local subprocess and unrestricted filesystem providers, a platform-selected persistent shell PTY, the string-replace editor, and uncompressed JSONL persistence under `$DSH_HOME/sessions`. It does not inherit another bundle, so every extra row is an explicit profile change. +The bundle's single insert is the complete application tree: SDK stdio startup and JSON-RPC serving, one environment-configured DeepSeek adapter, the explicit agent core, local subprocess execution, a platform-selected persistent shell PTY, and uncompressed JSONL persistence under `$DSH_HOME/sessions`. It does not inherit another bundle, so every extra row is an explicit profile change. ### Source map @@ -77,11 +77,11 @@ The bundle's single insert is the complete application tree: SDK stdio startup a #### What the model sees -The system prompt is `DSH_SYSTEM_PROMPT` or `You are a helpful software engineer assistant.`. The only advertised tools are owner-scoped persistent `bash` on Linux/macOS or `pwsh` on Windows, plus `str_replace_editor`; runtime context, workspace instructions, skills, jobs controls, compaction, and Harness identity are absent. +The system prompt is `DSH_SYSTEM_PROMPT` or `You are a helpful software engineer assistant.`. The only advertised tool is owner-scoped persistent `bash` on Linux/macOS or `pwsh` on Windows; runtime context, filesystem tools, workspace instructions, skills, jobs controls, compaction, and Harness identity are absent. #### Token effect -One stable persona plus the two tool schemas. Tool results and ordinary conversation history grow with the session. +One stable persona plus one tool schema. Tool results and ordinary conversation history grow with the session. #### KV Cache effect diff --git a/packages/bundle/sdk-minimal/README.zh.md b/packages/bundle/sdk-minimal/README.zh.md index dff652f53d..077d3ac0f6 100644 --- a/packages/bundle/sdk-minimal/README.zh.md +++ b/packages/bundle/sdk-minimal/README.zh.md @@ -1,5 +1,5 @@ --- -description: "供需要不含共享 base bundle 的极简跨平台 coding agent 的用户使用的独立双工具 SDK profile。" +description: "供需要不含共享 base bundle 的极简跨平台 coding agent 的用户使用的独立单工具 SDK profile。" kind: "package-bundle" --- @@ -9,7 +9,7 @@ kind: "package-bundle" ## 概述 -当 SDK 客户端需要小型、显式的 coding agent 运行时时,请使用 `dsh --profile sdk-minimal`。该 profile 只公布按平台选择的持久 shell 与 `str_replace_editor`,把会话持久化为未压缩 JSONL,并从 SDK 初始化请求选择模型。它提供完整 Cordis 配置树,并刻意排除 `dsh-base`、Web、settings、托管凭据、遥测、compaction、workspace 指令、skills、jobs 与 subagent。其 danger-full-access 策略允许 shell 与编辑器修改进程可访问的任何路径,因此只能配合隔离 workspace 使用。 +当 SDK 客户端需要小型、显式的 coding agent 运行时时,请使用 `dsh --profile sdk-minimal`。该 profile 只公布按平台选择的持久 shell,把会话持久化为未压缩 JSONL,并从 SDK 初始化请求选择模型。它提供完整 Cordis 配置树,并刻意排除 `dsh-base`、Web、settings、托管凭据、遥测、compaction、文件系统工具、workspace 指令、skills、jobs 与 subagent。其 danger-full-access 策略允许 shell 修改进程可访问的任何路径,因此只能配合隔离 workspace 使用。 ## 目录 @@ -46,7 +46,7 @@ dsh --profile sdk-minimal
实现细节——点击展开 -该 bundle 的单个 insert 就是完整应用配置树:SDK stdio 启动与 JSON-RPC 服务、一个由环境配置的 DeepSeek 适配器、显式 agent 核心、本地子进程与不受限文件系统提供方、按平台选择的持久 shell PTY、字符串替换编辑器,以及位于 `$DSH_HOME/sessions` 的未压缩 JSONL 持久化。它不继承其他 bundle,因此每个额外配置项都是显式 profile 变更。 +该 bundle 的单个 insert 就是完整应用配置树:SDK stdio 启动与 JSON-RPC 服务、一个由环境配置的 DeepSeek 适配器、显式 agent 核心、本地子进程执行、按平台选择的持久 shell PTY,以及位于 `$DSH_HOME/sessions` 的未压缩 JSONL 持久化。它不继承其他 bundle,因此每个额外配置项都是显式 profile 变更。 ### 源码地图 @@ -77,11 +77,11 @@ dsh --profile sdk-minimal #### 模型看到的内容 -系统提示词取 `DSH_SYSTEM_PROMPT`,未设置时使用 `You are a helpful software engineer assistant.`。对外公布的工具只有 Linux/macOS 上 agent 所有的持久 `bash` 或 Windows 上的 `pwsh`,外加 `str_replace_editor`;运行时上下文、workspace 指令、skills、jobs 控制、compaction 与 Harness 身份均不存在。 +系统提示词取 `DSH_SYSTEM_PROMPT`,未设置时使用 `You are a helpful software engineer assistant.`。对外公布的唯一工具是 Linux/macOS 上 agent 所有的持久 `bash` 或 Windows 上的 `pwsh`;运行时上下文、文件系统工具、workspace 指令、skills、jobs 控制、compaction 与 Harness 身份均不存在。 #### Token 影响 -一个稳定 persona 加两个工具 schema。工具结果与普通对话历史随会话增长。 +一个稳定 persona 加一个工具 schema。工具结果与普通对话历史随会话增长。 #### KV Cache 影响 diff --git a/packages/bundle/sdk-minimal/cordis.patch.yml b/packages/bundle/sdk-minimal/cordis.patch.yml index 4375e2ab67..1b443a3881 100644 --- a/packages/bundle/sdk-minimal/cordis.patch.yml +++ b/packages/bundle/sdk-minimal/cordis.patch.yml @@ -63,13 +63,6 @@ shellDialect: pwsh timeoutMs: 300000 - # The editor uses the bare local filesystem; persistent Bash still consumes - # the shared danger-full-access sandbox policy above. - - id: fs-local - name: '@deepseek-ai/dsh-fs-local' - config: - cwd: !!js process.cwd() - # Keep the minimal agent kernel explicit: unlike dsh-base, this profile # owns each service row and omits every optional producer it does not use. - id: timer @@ -156,11 +149,6 @@ * Please avoid commands that may produce a very large amount of output. * Please run long lived commands in the background, e.g. 'Start-Job' or start a server with Start-Process. - - id: str-replace-editor - name: '@deepseek-ai/dsh-tool-str-replace-editor' - config: - maxOutputChars: 16000 - - id: sessions name: '@deepseek-ai/dsh-session-persistence-jsonl' config: diff --git a/packages/bundle/sdk-minimal/package.json b/packages/bundle/sdk-minimal/package.json index f6e7d53158..d28bcd2638 100644 --- a/packages/bundle/sdk-minimal/package.json +++ b/packages/bundle/sdk-minimal/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-sdk-minimal", - "description": "The standalone minimal SDK profile bundle: JSON-RPC, one DeepSeek adapter, persistent shell, editor, and JSONL sessions", + "description": "The standalone minimal SDK profile bundle: JSON-RPC, one DeepSeek adapter, persistent shell, and JSONL sessions", "version": "0.1.2-rc.1", "publishConfig": { "access": "public" @@ -38,7 +38,6 @@ "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-deepseek-llm-api-extensions": "workspace:^", - "@deepseek-ai/dsh-fs-local": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-jobs-local": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", @@ -61,7 +60,6 @@ "@deepseek-ai/dsh-terminal-bash": "workspace:^", "@deepseek-ai/dsh-tool-bash-persistent": "workspace:^", "@deepseek-ai/dsh-tool-pwsh-persistent": "workspace:^", - "@deepseek-ai/dsh-tool-str-replace-editor": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^" }, "peerDependencies": { diff --git a/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts b/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts index 062e0f7ba4..c98cf87881 100644 --- a/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts +++ b/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts @@ -39,7 +39,6 @@ describe('dsh-sdk-minimal bundle', () => { ['pty', '@deepseek-ai/dsh-terminal'], ['terminal-bash', '@deepseek-ai/dsh-terminal-bash'], ['terminal-pwsh', '@deepseek-ai/dsh-terminal-bash'], - ['fs-local', '@deepseek-ai/dsh-fs-local'], ['timer', '@deepseek-ai/cordis-plugin-timer'], ['llm', '@deepseek-ai/dsh-llm'], ['session', '@deepseek-ai/dsh-session'], @@ -57,7 +56,6 @@ describe('dsh-sdk-minimal bundle', () => { ['agent-loop', '@deepseek-ai/dsh-agent-loop'], ['persistent-bash', '@deepseek-ai/dsh-tool-bash-persistent'], ['persistent-pwsh', '@deepseek-ai/dsh-tool-pwsh-persistent'], - ['str-replace-editor', '@deepseek-ai/dsh-tool-str-replace-editor'], ['sessions', '@deepseek-ai/dsh-session-persistence-jsonl'], ]) expect(rows.find(row => row.id === 'sdk-app-startup')?.config).toEqual({ profile: 'sdk-minimal' }) @@ -83,6 +81,8 @@ describe('dsh-sdk-minimal bundle', () => { disabled: { __jsExpr: "process.platform !== 'win32'" }, config: { shellDialect: 'pwsh', timeoutMs: 300000 }, }) + expect(manifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-fs-local') + expect(manifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-tool-str-replace-editor') expect(Object.keys(manifest.dependencies ?? {}).sort()).toEqual( [...new Set(rows.map(row => row.name).filter((name): name is string => name !== undefined).map(packageName))].sort(), ) diff --git a/packages/client/ui-agent-preset/src/client/locales.ts b/packages/client/ui-agent-preset/src/client/locales.ts index a4dbdd949b..f14ef62142 100644 --- a/packages/client/ui-agent-preset/src/client/locales.ts +++ b/packages/client/ui-agent-preset/src/client/locales.ts @@ -39,7 +39,7 @@ export const en: Record = { 'Full coding agent without the workflow tool; other tools are 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.', + 'Single-tool coding agent with a persistent shell.', presetCordisName: 'Creator mode', presetCordisDescription: 'Built for creating custom agent presets, with all Standard mode capabilities plus runtime inspection, plugin experiments, and preset-authoring guidance.', @@ -98,7 +98,7 @@ export const zh: Record = { presetPtcName: 'PTC 模式', presetPtcDescription: '功能完整的编码 Agent,但默认不提供 workflow 工具;其他工具通过 PTC 模式 SDK 呈现,让模型用一个 TypeScript 程序组合多步操作。', presetMinimalName: '极简模式', - presetMinimalDescription: '仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。', + presetMinimalDescription: '仅提供持久 shell 的单工具编码 Agent。', presetCordisName: '创造模式', presetCordisDescription: '用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、插件实验和 preset 创作指导。', duplicate: '复制', diff --git a/packages/preset/agent-presets/presets/minimal/agent.cordis.yml b/packages/preset/agent-presets/presets/minimal/agent.cordis.yml index 0e1f2b0f5d..82b38775fa 100644 --- a/packages/preset/agent-presets/presets/minimal/agent.cordis.yml +++ b/packages/preset/agent-presets/presets/minimal/agent.cordis.yml @@ -1,10 +1,10 @@ -# The `minimal` agent preset: a fixed-prompt, two-tool coding-agent composition. +# The `minimal` agent preset: a fixed-prompt, single-tool coding-agent composition. # # The persona is the complete system prompt, so global identity, Web orientation, # tool guidance, and later assembly listeners cannot add prompt text. Runtime -# context snapshots are suppressed for this preset, and the model composes only -# the persistent shell (`bash` on POSIX, `pwsh` on win32) and -# `str_replace_editor`. Context compaction is absent. +# context snapshots are suppressed for this preset, and the model receives only +# the persistent shell (`bash` on POSIX, `pwsh` on win32). Context compaction is +# absent. - id: persona name: '@deepseek-ai/dsh-persona' @@ -68,21 +68,3 @@ * Use native Windows paths (C:\...) and $env:NAME variables; this is PowerShell, not bash. * Please avoid commands that may produce a very large amount of output. * Please run long lived commands in the background, e.g. 'Start-Job' or start a server with Start-Process. - -# The bare local filesystem shadows the host's sandboxed provider only for this -# preset. The editor shares that realm and requires absolute paths. -- id: filesystem - name: cordis:group - group: true - isolate: - fs: true - config: - - id: fs-local - name: '@deepseek-ai/dsh-fs-local' - config: - cwd: !!js process.env.DSH_CWD ?? process.cwd() - - - id: str-replace-editor - name: '@deepseek-ai/dsh-tool-str-replace-editor' - config: - maxOutputChars: 16000 diff --git a/packages/preset/agent-presets/presets/minimal/preset.yml b/packages/preset/agent-presets/presets/minimal/preset.yml index 97c714b72a..60d2f10c15 100644 --- a/packages/preset/agent-presets/presets/minimal/preset.yml +++ b/packages/preset/agent-presets/presets/minimal/preset.yml @@ -1,3 +1,3 @@ name: 极简模式 -description: 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。 +description: 仅提供持久 shell 的单工具编码 Agent。 order: 3 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1fc58b4611..4388263d21 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1368,9 +1368,6 @@ importers: '@deepseek-ai/dsh-deepseek-llm-api-extensions': specifier: workspace:^ version: link:../../llm/deepseek-llm-api-extensions - '@deepseek-ai/dsh-fs-local': - specifier: workspace:^ - version: link:../../fs/fs-local '@deepseek-ai/dsh-invariants': specifier: workspace:^ version: link:../../runtime-diagnostics/invariants @@ -1437,9 +1434,6 @@ importers: '@deepseek-ai/dsh-tool-pwsh-persistent': specifier: workspace:^ version: link:../../shell/tool-pwsh-persistent - '@deepseek-ai/dsh-tool-str-replace-editor': - specifier: workspace:^ - version: link:../../fs/tool-str-replace-editor '@deepseek-ai/dsh-tools': specifier: workspace:^ version: link:../../core/tools diff --git a/python/sdk/README.i18n.yaml b/python/sdk/README.i18n.yaml index a908083ed7..55e1dde449 100644 --- a/python/sdk/README.i18n.yaml +++ b/python/sdk/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write python/sdk/README.md -README.md: 77ae34e24f5aec0c767cbe6c367d6d44eefbbfef -README.zh.md: 41b343895aa3d03174098ad5f856cc65197f063b +README.md: bfca3b0dda6c97bbef255815f33ca81121832f4e +README.zh.md: 3824b6d35bcaa0bdec92584c53ce7036130d85c0 diff --git a/python/sdk/README.md b/python/sdk/README.md index 77ae34e24f..bfca3b0dda 100644 --- a/python/sdk/README.md +++ b/python/sdk/README.md @@ -59,7 +59,7 @@ with DeepSeekHarness( `provider` selects a provider route registered by the chosen Cordis composition; `model` is the model id resolved by that adapter. `reasoning_effort` is an optional non-empty adapter-owned identifier for that exact route; omission preserves the model's own default. `max_tokens` is an optional positive per-request output-token cap for the root agent and its in-process descendants; omission leaves the provider default in control. Initialization rejects a missing adapter, unavailable model, or unsupported effort before a prompt runs. Compaction summaries keep the separate limit configured by their compaction plugin. The bundled default composition registers `deepseek-official`. A custom composition can mount `llm-pi-ai`, configure provider-specific credentials/endpoints there, and select any provider/model present in pi-ai's installed catalog. -The shipped `sdk-minimal` profile is a standalone explicit tree rather than an overlay on `dsh-base`. Select it with `profile="sdk-minimal"`; the ordinary `model` argument is the sole runtime model selection, including for model ids outside the adapter's advisory catalog. It provides persistent Bash, the string-replace editor, local execution, and JSONL sessions; settings, managed credentials, telemetry, Web tools, and the full default tool roster remain available through the separate full `sdk` and `web` profiles. +The shipped `sdk-minimal` profile is a standalone explicit tree rather than an overlay on `dsh-base`. Select it with `profile="sdk-minimal"`; the ordinary `model` argument is the sole runtime model selection, including for model ids outside the adapter's advisory catalog. It provides only a platform-selected persistent shell, local execution, and JSONL sessions; filesystem tools, settings, managed credentials, telemetry, Web tools, and the full default tool roster remain available through the separate full `sdk` and `web` profiles. ## Results and notifications diff --git a/python/sdk/README.zh.md b/python/sdk/README.zh.md index 41b343895a..3824b6d35b 100644 --- a/python/sdk/README.zh.md +++ b/python/sdk/README.zh.md @@ -59,7 +59,7 @@ with DeepSeekHarness( `provider` 选择指定 Cordis 组合所注册的提供方路由;`model` 是该适配器解析出的模型 ID。`reasoning_effort` 是该确切路由可选的非空适配器自有标识符;省略时保留模型自身的默认值。`max_tokens` 是一个可选的正整数,用于限制根 agent 及其进程内后代在每次请求中输出的 token 数量;省略该参数时,由提供方的默认行为决定输出上限。缺少适配器、模型不可用或推理强度不受支持时,初始化会在提示词运行前拒绝。压缩摘要继续使用压缩插件单独配置的上限。内置默认组合注册 `deepseek-official`。自定义组合可以挂载 `llm-pi-ai`,在其中配置各提供方专属的凭据和端点,并选择 pi-ai 已安装 catalog 中存在的任意提供方/模型组合。 -随附的 `sdk-minimal` profile 是独立显式配置树,而不是 `dsh-base` 上的 overlay。使用 `profile="sdk-minimal"` 选择它;普通 `model` 参数是唯一运行时模型选择,也适用于不在适配器建议目录中的模型 id。它提供持久 Bash、字符串替换 editor、本地执行与 JSONL 会话;settings、托管凭据、遥测、Web 工具与完整默认工具清单仍由独立的完整 `sdk` 与 `web` profile 提供。 +随附的 `sdk-minimal` profile 是独立显式配置树,而不是 `dsh-base` 上的 overlay。使用 `profile="sdk-minimal"` 选择它;普通 `model` 参数是唯一运行时模型选择,也适用于不在适配器建议目录中的模型 id。它只提供按平台选择的持久 shell、本地执行与 JSONL 会话;文件系统工具、settings、托管凭据、遥测、Web 工具与完整默认工具清单仍由独立的完整 `sdk` 与 `web` profile 提供。 ## 结果与通知 diff --git a/python/sdk/examples/README.i18n.yaml b/python/sdk/examples/README.i18n.yaml index 12daa498e4..74236f416f 100644 --- a/python/sdk/examples/README.i18n.yaml +++ b/python/sdk/examples/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 python/sdk/examples/README.md -README.md: e79cb309318c1aa31d41bb50e52f5517b07ea485 -README.zh.md: 3208c5c1462a15a50a6990f5c989ef6b81da6ab4 +README.md: 82715c67cebe5d06decd3a6301cb1a8c245958c6 +README.zh.md: e241f9d7aa0174ce806e1d67b4183b9824a2f72b diff --git a/python/sdk/examples/README.md b/python/sdk/examples/README.md index e79cb30931..82715c67ce 100644 --- a/python/sdk/examples/README.md +++ b/python/sdk/examples/README.md @@ -22,11 +22,10 @@ Set `DEEPSEEK_BASE_URL` for a compatible proxy, `DSH_MODEL` for the script's def The shipped [`@deepseek-ai/dsh-sdk-minimal` bundle](../../../packages/bundle/sdk-minimal/README.md) is the complete explicit Cordis tree for this mode. It exposes exactly: - owner-scoped persistent `bash` on Linux/macOS or `pwsh` on Windows -- `str_replace_editor` with `view`, `create`, `str_replace`, and `insert` -The bundle does not include `dsh-base`, so every additional row is an explicit profile change. Runtime context, local instruction discovery, compaction, settings, managed credentials, telemetry, Web tools, subagents, and the full default tool roster are absent. The tree retains SDK startup and JSON-RPC serving, one environment-configured DeepSeek adapter, local execution, and JSONL persistence. +The bundle does not include `dsh-base`, so every additional row is an explicit profile change. Runtime context, filesystem tools, local instruction discovery, compaction, settings, managed credentials, telemetry, Web tools, subagents, and the full default tool roster are absent. The tree retains SDK startup and JSON-RPC serving, one environment-configured DeepSeek adapter, local execution, and JSONL persistence. -The persistent PTY and editor can modify any path available to the runtime process, so use a disposable checkout or container. +The persistent PTY can modify any path available to the runtime process, so use a disposable checkout or container. ## Add plugins diff --git a/python/sdk/examples/README.zh.md b/python/sdk/examples/README.zh.md index 3208c5c146..e241f9d7aa 100644 --- a/python/sdk/examples/README.zh.md +++ b/python/sdk/examples/README.zh.md @@ -22,11 +22,10 @@ python python/sdk/examples/minimal.py \ 随附的 [`@deepseek-ai/dsh-sdk-minimal` 组合包](../../../packages/bundle/sdk-minimal/README.zh.md)是该模式完整且显式的 Cordis 配置树。它只暴露: - Linux/macOS 上 agent 所有的持久 `bash`,或 Windows 上的 `pwsh` -- 支持 `view`、`create`、`str_replace` 与 `insert` 的 `str_replace_editor` -该组合包不包含 `dsh-base`,因此每一个新增配置项都是显式 profile 变更。运行时上下文、本地指令发现、compaction、settings、托管凭据、遥测、Web 工具、subagent 与完整默认工具清单均不存在。配置树保留 SDK 启动与 JSON-RPC 服务、一个由环境配置的 DeepSeek 适配器、本地执行和 JSONL 持久化。 +该组合包不包含 `dsh-base`,因此每一个新增配置项都是显式 profile 变更。运行时上下文、文件系统工具、本地指令发现、compaction、settings、托管凭据、遥测、Web 工具、subagent 与完整默认工具清单均不存在。配置树保留 SDK 启动与 JSON-RPC 服务、一个由环境配置的 DeepSeek 适配器、本地执行和 JSONL 持久化。 -持久 PTY 与 editor 可以修改运行时进程可访问的任何路径,因此只应在一次性 checkout 或容器中使用。 +持久 PTY 可以修改运行时进程可访问的任何路径,因此只应在一次性 checkout 或容器中使用。 ## 添加插件 diff --git a/scripts/smoke-python-runtime.py b/scripts/smoke-python-runtime.py index a0ab195b2e..b1969ab5b9 100644 --- a/scripts/smoke-python-runtime.py +++ b/scripts/smoke-python-runtime.py @@ -31,9 +31,8 @@ CODE_PROMPT = "Use run_code to compute the packaged worker smoke value." CODE_WORKER_TEXT = "code worker smoke ok" WORKFLOW_PROMPT = "Use workflow to compute the packaged worker smoke value without agents." WORKFLOW_WORKER_TEXT = "workflow worker smoke ok" -MINIMAL_PROMPT = "Exercise the packaged minimal agent's persistent shell and string-replacement editor." +MINIMAL_PROMPT = "Exercise the packaged minimal agent's persistent shell." MINIMAL_TEXT = "minimal agent smoke ok" -MINIMAL_EDITOR_PATH_PREFIX = "Editor path: " FS_SEARCH_PROMPT = "Exercise the packaged filesystem search tools." FS_SEARCH_TEXT = "filesystem search smoke ok" FS_SEARCH_MARKER = "PACKAGED_FS_SEARCH_OK" @@ -324,7 +323,7 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]: spawn_node = spawn_node_tool_followup(call_id, tool_name, tool_text) if spawn_node is not None: return spawn_node - minimal = minimal_tool_followup(body, call_id, tool_name, tool_text) + minimal = minimal_tool_followup(call_id, tool_name, tool_text) if minimal is not None: return minimal advanced = advanced_tool_followup(body, call_id, tool_name, tool_text) @@ -343,14 +342,7 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]: for message in reversed(messages) if isinstance(message, dict) and message.get("role") == "user" ] - minimal_prompt = next( - ( - prompt - for prompt in user_prompts - if prompt.startswith(f"{MINIMAL_PROMPT}\n{MINIMAL_EDITOR_PATH_PREFIX}") - ), - None, - ) + minimal_prompt = next((prompt for prompt in user_prompts if prompt == MINIMAL_PROMPT), None) # The minimal composition's assembled system prompt, advertised tool schemas, and # model-visible messages are pinned by its snapshot, not asserted here. if minimal_prompt is not None: @@ -523,12 +515,11 @@ def spawn_node_tool_followup( def minimal_tool_followup( - body: dict[str, object], call_id: str, tool_name: str, tool_text: str, ) -> list[dict[str, object]] | None: - """Verify the checked-in minimal composition's PTY and editor.""" + """Verify the checked-in minimal composition's persistent PTY.""" if not call_id.startswith("minimal-"): return None if call_id == "minimal-bash-1" and tool_name == MINIMAL_SHELL_TOOL: @@ -543,33 +534,6 @@ def minimal_tool_followup( expected = f"COUNT=2 CWD={MINIMAL_SHELL_SECOND_CWD}" if expected.lower() not in tool_text.lower(): raise AssertionError(f"persistent shell did not retain state: {tool_text}") - messages = body.get("messages") - if not isinstance(messages, list): - raise AssertionError("persistent editor smoke request has no messages") - editor_path = next( - ( - text.split(MINIMAL_EDITOR_PATH_PREFIX, 1)[1].strip() - for message in messages - if isinstance(message, dict) and message.get("role") == "user" - for text in [message_text(message.get("content"))] - if MINIMAL_EDITOR_PATH_PREFIX in text - ), - None, - ) - if editor_path is None: - raise AssertionError("persistent editor smoke prompt has no editor path") - return tool_call_chunks( - "minimal-editor", - "str_replace_editor", - { - "command": "create", - "path": editor_path, - "file_text": "created by packaged editor\n", - }, - ) - if call_id == "minimal-editor" and tool_name == "str_replace_editor": - if "New file created successfully" not in tool_text: - raise AssertionError(f"packaged editor did not create its file: {tool_text}") return text_chunks(MINIMAL_TEXT) raise AssertionError(f"unexpected minimal-agent follow-up: {call_id} {tool_name}: {tool_text}") @@ -1041,8 +1005,6 @@ def smoke_sdk_minimal(base_url: str, executable: Path, update_snapshots: bool) - first_request = len(MockModelHandler.requests) with tempfile.TemporaryDirectory(prefix="dsh-sdk-minimal-") as temporary: root = Path(temporary).resolve() - editor_path = root / "created.txt" - prompt = f"{MINIMAL_PROMPT}\n{MINIMAL_EDITOR_PATH_PREFIX}{editor_path}" dsh_home = root / "home" sessions = dsh_home / "sessions" with DeepSeekHarness( @@ -1056,13 +1018,11 @@ def smoke_sdk_minimal(base_url: str, executable: Path, update_snapshots: bool) - base_url=base_url, request_timeout_seconds=60, ) as harness: - result = harness.run(prompt, session_id="minimal-agent-smoke") + result = harness.run(MINIMAL_PROMPT, session_id="minimal-agent-smoke") event_text = json.dumps(result.events) if MINIMAL_TEXT not in event_text: raise AssertionError(f"minimal agent run emitted no final response: {result.events}") - if editor_path.read_text() != "created by packaged editor\n": - raise AssertionError(f"packaged editor wrote unexpected content: {editor_path.read_text()!r}") assert_session_log(sessions, root, MINIMAL_TEXT, "COUNT=1", "COUNT=2") files = build_minimal_snapshot_files(MockModelHandler.requests[first_request:], root) diff --git a/scripts/snapshots/python-sdk-single-exe/minimal/model-visible.json b/scripts/snapshots/python-sdk-single-exe/minimal/model-visible.json index 3cbc88cb08..81bb5f3b77 100644 --- a/scripts/snapshots/python-sdk-single-exe/minimal/model-visible.json +++ b/scripts/snapshots/python-sdk-single-exe/minimal/model-visible.json @@ -19,94 +19,6 @@ ] } } - }, - { - "type": "function", - "function": { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - } } ], "messages": [ @@ -116,7 +28,7 @@ }, { "role": "user", - "text": "Exercise the packaged minimal agent's persistent shell and string-replacement editor.\nEditor path: {{cwd}}/created.txt" + "text": "Exercise the packaged minimal agent's persistent shell." } ] }, @@ -140,94 +52,6 @@ ] } } - }, - { - "type": "function", - "function": { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - } } ], "messages": [ @@ -237,7 +61,7 @@ }, { "role": "user", - "text": "Exercise the packaged minimal agent's persistent shell and string-replacement editor.\nEditor path: {{cwd}}/created.txt" + "text": "Exercise the packaged minimal agent's persistent shell." }, { "role": "assistant", @@ -275,94 +99,6 @@ ] } } - }, - { - "type": "function", - "function": { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - } } ], "messages": [ @@ -372,7 +108,7 @@ }, { "role": "user", - "text": "Exercise the packaged minimal agent's persistent shell and string-replacement editor.\nEditor path: {{cwd}}/created.txt" + "text": "Exercise the packaged minimal agent's persistent shell." }, { "role": "assistant", @@ -403,168 +139,5 @@ "text": "{{tool-result}}" } ] - }, - { - "tools": [ - { - "type": "function", - "function": { - "name": "bash", - "description": "Run commands in a bash shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* You don't have access to the internet via this tool.\n* You do have access to a mirror of common linux and python packages via apt and pip.\n* State is persistent across command calls and discussions with the user.\n* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The bash command to run. Relative path is preferred in the command." - } - }, - "required": [ - "command" - ] - } - } - }, - { - "type": "function", - "function": { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - } - } - ], - "messages": [ - { - "role": "system", - "text": "You are a helpful software engineer assistant." - }, - { - "role": "user", - "text": "Exercise the packaged minimal agent's persistent shell and string-replacement editor.\nEditor path: {{cwd}}/created.txt" - }, - { - "role": "assistant", - "toolCalls": [ - { - "id": "minimal-bash-1", - "name": "bash" - } - ] - }, - { - "role": "tool", - "toolCallId": "minimal-bash-1", - "text": "{{tool-result}}" - }, - { - "role": "assistant", - "toolCalls": [ - { - "id": "minimal-bash-2", - "name": "bash" - } - ] - }, - { - "role": "tool", - "toolCallId": "minimal-bash-2", - "text": "{{tool-result}}" - }, - { - "role": "assistant", - "toolCalls": [ - { - "id": "minimal-editor", - "name": "str_replace_editor" - } - ] - }, - { - "role": "tool", - "toolCallId": "minimal-editor", - "text": "{{tool-result}}" - } - ] } ] diff --git a/scripts/snapshots/python-sdk-single-exe/minimal/win-x64/model-visible.json b/scripts/snapshots/python-sdk-single-exe/minimal/win-x64/model-visible.json index f8d606e8ea..728b73a1a0 100644 --- a/scripts/snapshots/python-sdk-single-exe/minimal/win-x64/model-visible.json +++ b/scripts/snapshots/python-sdk-single-exe/minimal/win-x64/model-visible.json @@ -19,94 +19,6 @@ ] } } - }, - { - "type": "function", - "function": { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - } } ], "messages": [ @@ -116,7 +28,7 @@ }, { "role": "user", - "text": "Exercise the packaged minimal agent's persistent shell and string-replacement editor.\nEditor path: {{cwd}}\\created.txt" + "text": "Exercise the packaged minimal agent's persistent shell." } ] }, @@ -140,94 +52,6 @@ ] } } - }, - { - "type": "function", - "function": { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - } } ], "messages": [ @@ -237,7 +61,7 @@ }, { "role": "user", - "text": "Exercise the packaged minimal agent's persistent shell and string-replacement editor.\nEditor path: {{cwd}}\\created.txt" + "text": "Exercise the packaged minimal agent's persistent shell." }, { "role": "assistant", @@ -275,94 +99,6 @@ ] } } - }, - { - "type": "function", - "function": { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - } } ], "messages": [ @@ -372,7 +108,7 @@ }, { "role": "user", - "text": "Exercise the packaged minimal agent's persistent shell and string-replacement editor.\nEditor path: {{cwd}}\\created.txt" + "text": "Exercise the packaged minimal agent's persistent shell." }, { "role": "assistant", @@ -403,168 +139,5 @@ "text": "{{tool-result}}" } ] - }, - { - "tools": [ - { - "type": "function", - "function": { - "name": "pwsh", - "description": "Run commands in a PowerShell shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* You don't have access to the internet via this tool.\n* State is persistent across command calls and discussions with the user.\n* Use native Windows paths (C:\\...) and $env:NAME variables; this is PowerShell, not bash.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'Start-Job' or start a server with Start-Process.", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The PowerShell command to run. Relative path is preferred in the command." - } - }, - "required": [ - "command" - ] - } - } - }, - { - "type": "function", - "function": { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - } - } - ], - "messages": [ - { - "role": "system", - "text": "You are a helpful software engineer assistant." - }, - { - "role": "user", - "text": "Exercise the packaged minimal agent's persistent shell and string-replacement editor.\nEditor path: {{cwd}}\\created.txt" - }, - { - "role": "assistant", - "toolCalls": [ - { - "id": "minimal-bash-1", - "name": "pwsh" - } - ] - }, - { - "role": "tool", - "toolCallId": "minimal-bash-1", - "text": "{{tool-result}}" - }, - { - "role": "assistant", - "toolCalls": [ - { - "id": "minimal-bash-2", - "name": "pwsh" - } - ] - }, - { - "role": "tool", - "toolCallId": "minimal-bash-2", - "text": "{{tool-result}}" - }, - { - "role": "assistant", - "toolCalls": [ - { - "id": "minimal-editor", - "name": "str_replace_editor" - } - ] - }, - { - "role": "tool", - "toolCallId": "minimal-editor", - "text": "{{tool-result}}" - } - ] } ] diff --git a/snapshots/web/minimal-preset/tool-schemas.expected.json b/snapshots/web/minimal-preset/tool-schemas.expected.json index 73234c5e49..9b8ce846b2 100644 --- a/snapshots/web/minimal-preset/tool-schemas.expected.json +++ b/snapshots/web/minimal-preset/tool-schemas.expected.json @@ -15,91 +15,6 @@ "command" ] } - }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } } ], "changes": [] From 79b3d7a59445fe56bb47457891dd7fa4f0ba28fc Mon Sep 17 00:00:00 2001 From: fz Date: Thu, 3 Sep 2026 17:02:37 +0800 Subject: [PATCH 02/58] refactor: remove str_replace_editor --- ...client-derived-tool-presentation.i18n.yaml | 4 +- ...-08-23-client-derived-tool-presentation.md | 7 - ...-23-client-derived-tool-presentation.zh.md | 7 - ...9-filesystem-absence-observation.i18n.yaml | 4 +- ...26-08-09-filesystem-absence-observation.md | 2 +- ...08-09-filesystem-absence-observation.zh.md | 2 +- ...rsistent-bash-str-replace-editor.i18n.yaml | 4 +- ...7-29-persistent-bash-str-replace-editor.md | 8 +- ...9-persistent-bash-str-replace-editor.zh.md | 8 +- ...31-even-out-shipped-tool-rosters.i18n.yaml | 4 +- ...026-07-31-even-out-shipped-tool-rosters.md | 4 +- ...-07-31-even-out-shipped-tool-rosters.zh.md | 4 +- ...6-07-31-web-workspace-file-links.i18n.yaml | 4 +- .../2026-07-31-web-workspace-file-links.md | 2 +- .../2026-07-31-web-workspace-file-links.zh.md | 2 +- ...10-default-presets-single-editor.i18n.yaml | 4 +- ...026-08-10-default-presets-single-editor.md | 8 +- ...-08-10-default-presets-single-editor.zh.md | 8 +- ...l-profiles-persistent-shell-only.i18n.yaml | 4 +- ...-minimal-profiles-persistent-shell-only.md | 10 +- ...nimal-profiles-persistent-shell-only.zh.md | 10 +- ...-09-03-remove-str-replace-editor.i18n.yaml | 6 + .../2026-09-03-remove-str-replace-editor.md | 33 + ...2026-09-03-remove-str-replace-editor.zh.md | 33 + apps/cli/composition.md | 3 - apps/cli/package.json | 1 - apps/cli/tests/web-agent-presets.e2e.ts | 3 - docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 18 - docs/config-catalog.zh.md | 18 - docs/event-producer-consumer.i18n.yaml | 4 +- docs/event-producer-consumer.md | 6 +- docs/event-producer-consumer.zh.md | 6 +- docs/module-graph.i18n.yaml | 4 +- docs/module-graph.md | 6 - docs/module-graph.zh.md | 6 - docs/subsystems/filesystem.i18n.yaml | 4 +- docs/subsystems/filesystem.md | 2 +- docs/subsystems/filesystem.zh.md | 2 +- docs/tool-catalog.i18n.yaml | 4 +- docs/tool-catalog.md | 107 --- docs/tool-catalog.zh.md | 109 --- packages/bundle/base/cordis.patch.yml | 5 - packages/bundle/base/package.json | 1 - .../sdk-minimal/tests/sdk-minimal.spec.ts | 1 - packages/bundle/web-app/cordis.patch.yml | 3 - .../client/ui-deliverables/README.i18n.yaml | 4 +- packages/client/ui-deliverables/README.md | 2 +- packages/client/ui-deliverables/README.zh.md | 2 +- .../src/client/turn-deliverables.ts | 35 +- .../tests/produced-files.client.spec.tsx | 73 +- packages/client/ui-tool/README.i18n.yaml | 4 +- packages/client/ui-tool/README.md | 2 +- packages/client/ui-tool/README.zh.md | 2 +- .../src/client/tool/models/diff-card-model.ts | 29 +- .../ui-tool/tests/diff-card.client.spec.tsx | 41 -- .../core/tools/tests/gen-tool-catalog.spec.ts | 2 +- packages/fs/README.i18n.yaml | 4 +- packages/fs/README.md | 1 - packages/fs/README.zh.md | 1 - .../tool-str-replace-editor/README.i18n.yaml | 6 - packages/fs/tool-str-replace-editor/README.md | 149 ---- .../fs/tool-str-replace-editor/README.zh.md | 149 ---- .../fs/tool-str-replace-editor/package.json | 53 -- .../fs/tool-str-replace-editor/src/index.ts | 531 --------------- .../tests/tools.spec.ts | 643 ------------------ .../fs/tool-str-replace-editor/tsconfig.json | 16 - pnpm-lock.yaml | 55 -- python/sdk-runtime/package.json | 1 - scripts/gen-tool-catalog.ts | 14 - scripts/smoke-python-runtime.py | 1 - .../sdk/bash-tool/tool-schemas.expected.json | 85 --- .../sdk/persistent-tools/cordis.snapshot.yml | 15 - snapshots/sdk/persistent-tools/cordis.yml | 158 ----- .../notifications.expected.jsonl | 89 --- .../sdk/persistent-tools/result.expected.json | 1 - snapshots/sdk/persistent-tools/session.jsonl | 88 --- snapshots/sdk/persistent-tools/snapshot.yml | 10 - .../system-prompt.expected.md | 1 - .../tool-schemas.expected.json | 106 --- .../workspace.expected/note.txt | 2 - snapshots/sdk/sdk.snapshot.ts | 20 - .../tool-schemas.1.expected.json | 85 --- .../tool-schemas.1.expected.json | 85 --- .../tool-schemas.expected.json | 85 --- .../tool-schemas.1.expected.json | 85 --- .../tool-schemas.expected.json | 85 --- .../tool-schemas.1.expected.json | 85 --- .../tool-schemas.1.expected.json | 85 --- .../sdk/text-turn/tool-schemas.expected.json | 85 --- .../tool-schemas.expected.json | 170 ----- .../both-mode-turn/system-prompt.expected.md | 18 - .../both-mode-turn/tool-schemas.expected.json | 85 --- .../tool-schemas.expected.json | 170 ----- .../system-prompt.expected.md | 18 - .../tool-schemas.expected.json | 85 --- .../tool-schemas.expected.json | 85 --- .../lsp-definition/tool-schemas.expected.json | 85 --- .../tool-schemas.expected.json | 85 --- .../tool-schemas.expected.json | 85 --- .../tool-schemas.expected.json | 85 --- .../ptc-python-turn/system-prompt.expected.md | 19 - .../ptc-read-image/system-prompt.expected.md | 18 - .../ptc-turn/system-prompt.expected.md | 18 - .../tool-schemas.expected.json | 85 --- .../ralph-loop/tool-schemas.1.expected.json | 85 --- .../ralph-loop/tool-schemas.2.expected.json | 85 --- .../tool-schemas.expected.json | 85 --- .../tool-schemas.expected.json | 85 --- .../tool-schemas.expected.json | 85 --- .../text-turn/tool-schemas.expected.json | 85 --- .../web-fetch/tool-schemas.expected.json | 85 --- tsconfig.base.json | 1 - tsconfig.host.json | 1 - 114 files changed, 159 insertions(+), 5123 deletions(-) create mode 100644 .agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.i18n.yaml create mode 100644 .agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.md create mode 100644 .agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.zh.md delete mode 100644 packages/fs/tool-str-replace-editor/README.i18n.yaml delete mode 100644 packages/fs/tool-str-replace-editor/README.md delete mode 100644 packages/fs/tool-str-replace-editor/README.zh.md delete mode 100644 packages/fs/tool-str-replace-editor/package.json delete mode 100644 packages/fs/tool-str-replace-editor/src/index.ts delete mode 100644 packages/fs/tool-str-replace-editor/tests/tools.spec.ts delete mode 100644 packages/fs/tool-str-replace-editor/tsconfig.json delete mode 100644 snapshots/sdk/persistent-tools/cordis.snapshot.yml delete mode 100644 snapshots/sdk/persistent-tools/cordis.yml delete mode 100644 snapshots/sdk/persistent-tools/notifications.expected.jsonl delete mode 100644 snapshots/sdk/persistent-tools/result.expected.json delete mode 100644 snapshots/sdk/persistent-tools/session.jsonl delete mode 100644 snapshots/sdk/persistent-tools/snapshot.yml delete mode 100644 snapshots/sdk/persistent-tools/system-prompt.expected.md delete mode 100644 snapshots/sdk/persistent-tools/tool-schemas.expected.json delete mode 100644 snapshots/sdk/persistent-tools/workspace.expected/note.txt diff --git a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml index dc2902d1ca..9c49645780 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md -2026-08-23-client-derived-tool-presentation.md: 5598c1fbc6073a71f63a20274cd5a791b6b5e6b3 -2026-08-23-client-derived-tool-presentation.zh.md: 5a87433377af58b1ca9d378b76393dada4ca4a1e +2026-08-23-client-derived-tool-presentation.md: a5d395a4afb7b880ed4401de86296b0b2c85b0c5 +2026-08-23-client-derived-tool-presentation.zh.md: 8784e28dcb53fe96ff3ef0a21cfde82d23e457f7 diff --git a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md index 5598c1fbc6..a5d395a4af 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md +++ b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md @@ -324,10 +324,7 @@ Standard and persistent providers sharing the same tool name are a special compa |---|---| | running `write` | intended added-only diff from `file_path` and `content` | | running `edit` | intended replacement diff from `file_path`, `old_string`, and `new_string` | -| running `str_replace_editor create` | intended added-only diff from `path` and `file_text` | -| running `str_replace_editor str_replace` | intended replacement diff from `path`, `old_str`, and `new_str` | | successful settled `write`/`edit` | applied contextual hunks from `meta.diffs` | -| settled `str_replace_editor` | Generic, because the tool defines no result presenter | | write create or missing/malformed/empty applied metadata | current argument fallback | | error, malformed arguments, edit with malformed metadata, or Code Dispatch child | Generic | @@ -375,8 +372,6 @@ The Deliverables Definition observes root `tool/call` and successful `tool/resul |---|---|---| | `write` | any successful call | `file_path` | | `edit` | any successful call | `file_path` | -| `str_replace_editor` | `create`, `str_replace`, or `insert` | `path` | -| `str_replace_editor` | `view` | produces no path | | Other | no current first-party mutation semantics | produces no path | Failures, interruptions, orphan results, missing paths, and malformed arguments produce no deliverable. Paths retain first-seen deduplication, and results settled after the closing Assistant seq remain excluded. @@ -532,8 +527,6 @@ This change does not promise to preserve differences expressed only through a Ho ### Deliverables - Successful write/edit calls produce `file_path`. -- str_replace_editor create/str_replace/insert calls produce `path`. -- str_replace_editor view produces no path. - failure, interruption, malformed input, and orphan results produce no path. - First-seen deduplication and the closing-seq cutoff remain unchanged. diff --git a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md index 5a87433377..8784e28dcb 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md @@ -324,10 +324,7 @@ TerminalBlock 的 ANSI、光标重放、宽字符、行数上限、展开、复 |---|---| | running `write` | 从 `file_path` 与 `content` 生成 intended added-only diff | | running `edit` | 从 `file_path`、`old_string`、`new_string` 生成 intended replacement diff | -| running `str_replace_editor create` | 从 `path` 与 `file_text` 生成 intended added-only diff | -| running `str_replace_editor str_replace` | 从 `path`、`old_str` 与 `new_str` 生成 intended replacement diff | | settled `write`/`edit` success | 从 `meta.diffs` 生成 applied contextual hunks | -| settled `str_replace_editor` | Generic,因为该工具没有 result presenter | | write create 或 applied metadata 缺失、畸形、为空 | 当前 args fallback | | error、畸形 args、edit 的 metadata 畸形、Code Dispatch child | Generic | @@ -375,8 +372,6 @@ Deliverables Definition 按 callId 观察 root `tool/call` 与成功 `tool/resul |---|---|---| | `write` | 任意成功调用 | `file_path` | | `edit` | 任意成功调用 | `file_path` | -| `str_replace_editor` | `create`、`str_replace`、`insert` | `path` | -| `str_replace_editor` | `view` | 不产生 path | | 其他 | 无当前第一方 mutation 语义 | 不产生 path | 失败、interrupted、orphan result、缺失 path 与畸形 args 不产生 deliverable。同一路径保持 first-seen 去重,closing Assistant seq 之后落定的结果继续排除。 @@ -532,8 +527,6 @@ Host registry 允许不同 scope 为同一 tool name 提供不同定义;Sessio ### Deliverables - write/edit 成功产生 `file_path`。 -- str_replace_editor create/str_replace/insert 产生 `path`。 -- str_replace_editor view 不产生 path。 - failure、interrupted、malformed 与 orphan 不产生 path。 - first-seen 去重与 closing seq cut 不变。 diff --git a/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.i18n.yaml index 2656ce9926..d2d1256907 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.md -2026-08-09-filesystem-absence-observation.md: 40338c4806607b262b93222b4a73a34538414e51 -2026-08-09-filesystem-absence-observation.zh.md: 4544424800844e61f839d5af1f92cef3b3655a27 +2026-08-09-filesystem-absence-observation.md: 5f34ec29c85424a9979bfe48bbb71fd0149f676d +2026-08-09-filesystem-absence-observation.zh.md: 1ea214ddeb72b25331e0a60157b28029e6e346a7 diff --git a/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.md b/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.md index 40338c4806..5f34ec29c8 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.md +++ b/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.md @@ -12,7 +12,7 @@ Treating a failed read as permission to create also exposes a second boundary. B ## Decision -`dsh-fs` owns an explicit observation union: `{ kind: 'present', version: FsVersion } | { kind: 'absent' }`. The `fs/observed` event carries that union. Successful reads and mutations emit present; a metadata miss from `read` or the `str_replace_editor` `view`, `str_replace`, or `insert` command emits absent synchronously before returning `FS_NOT_FOUND`. Other read failures do not manufacture absence. +`dsh-fs` owns an explicit observation union: `{ kind: 'present', version: FsVersion } | { kind: 'absent' }`. The `fs/observed` event carries that union. Successful reads and mutations emit present; a metadata miss from `read` emits absent synchronously before returning `FS_NOT_FOUND`. Other read failures do not manufacture absence. `dsh-fs-observation-policy` stores three logical states per owner and target without injecting or calling `ctx.fs`: missing map entry is unseen, `absent` is confirmed absence, and `present(version)` is a replacement/edit basis. Write maps unseen and absent to the existing `createIfAbsent` intent and present to `replaceIfVersion`. Edit maps unseen to `FS_NOT_OBSERVED`, absent to `FS_NOT_FOUND`, and present to its version guard. A successful create or mutation replaces absence with its produced present version. diff --git a/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.zh.md b/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.zh.md index 4544424800..1ea214ddeb 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.zh.md @@ -12,7 +12,7 @@ Status: implemented ## 决策 -`dsh-fs` 拥有一个显式观测联合类型:`{ kind: 'present', version: FsVersion } | { kind: 'absent' }`。`fs/observed` 事件携带该联合类型。成功的读取与变更发出存在观测;`read` 的元数据未命中,或 `str_replace_editor` 的 `view`、`str_replace`、`insert` 命令发生元数据未命中时,都会在返回 `FS_NOT_FOUND` 前同步发出缺失观测。其他读取失败不会产生缺失观测。 +`dsh-fs` 拥有一个显式观测联合类型:`{ kind: 'present', version: FsVersion } | { kind: 'absent' }`。`fs/observed` 事件携带该联合类型。成功的读取与变更发出存在观测;`read` 的元数据未命中时,会在返回 `FS_NOT_FOUND` 前同步发出缺失观测。其他读取失败不会产生缺失观测。 `dsh-fs-observation-policy` 按所有者与目标存储三种逻辑状态,既不注入也不调用 `ctx.fs`:映射中无条目即未见,`absent` 表示确认缺失,`present(version)` 是替换/编辑基准。写入把未见和缺失映射到现有 `createIfAbsent` 意图,把存在映射到 `replaceIfVersion`。编辑把未见映射到 `FS_NOT_OBSERVED`,把缺失映射到 `FS_NOT_FOUND`,把存在映射到其版本守卫。成功创建或变更后,系统会用其产生的存在版本取代缺失状态。 diff --git a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.i18n.yaml b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.i18n.yaml index 9ba413c09f..23fff50840 100644 --- a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md -2026-07-29-persistent-bash-str-replace-editor.md: 02ad2bfa8ab8051eff2c408c8dbbca7acca2daaa -2026-07-29-persistent-bash-str-replace-editor.zh.md: beb2e17d2d342e621e0f1f829910ecf48e35ac80 +2026-07-29-persistent-bash-str-replace-editor.md: a1289d3f2c7c42851ce6eb5d0ec071786e797614 +2026-07-29-persistent-bash-str-replace-editor.zh.md: 49e315fc789f0f031bf09dd96d2de035cf7dd609 diff --git a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md index 02ad2bfa8a..a1289d3f2c 100644 --- a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md +++ b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md @@ -14,11 +14,13 @@ Some deployments need a one-call Bash schema whose shell state survives across m `@deepseek-ai/dsh-tool-str-replace-editor` independently consumes `ctx.fs` and registers `str_replace_editor` with `view`, `create`, `str_replace`, and `insert`. It provides numbered text views, filtered two-level directory listings, unique literal replacement, canonical insertion boundaries, and bounded output. Paths are absolute; file views preserve content tabs so copied text remains valid literal replacement input; mutations preserve tabs outside the requested edit; and the public schema and failures use only `old_str`. Command-specific fields accept `null` placeholders: execution treats them as omitted when the selected command does not use them, preserves required-field checks, treats `view_range: null` as a full view, and rejects `str_replace.new_str: null` so only omission requests deletion. The plugin can compose with persistent Bash, one-shot Bash, sandboxed Bash, or no shell. +The [complete-removal decision](../simplification/2026-09-03-remove-str-replace-editor.md) later supersedes the preceding editor half and deletes that package. This note retains authority only for the persistent-shell behavior and the historical reason the capabilities were originally separate. + `dsh-system-prompt` accepts `includeHarnessIdentity: false`, and profile compositions own that row together with their selected shell-tool rows. `sdk-minimal` sets the value to `false` and mounts only its platform-selected persistent shell, so a deployment can own an exact persona without duplicate prompt or tool registrations. Existing defaults remain unchanged. -Both plugins are included in the Python runtime closure. The persistent Bash closure also includes the PTY service/local backend and the sandbox services required by that backend. Because `node-pty` executes a native `spawn-helper` on macOS, each packaged macOS runtime executable ships with a `-spawn-helper` sibling; Linux uses `forkpty` directly. A pinned `node-pty` patch checks `DSH_NODE_PTY_SPAWN_HELPER` first, so it remains a true override for a current external consumer that supplies a non-sibling helper. When the override is unset, the patch resolves the packaged executable sibling if present and otherwise preserves upstream lookup in ordinary Node runs. The macOS builders fail before publication when the helper is absent or not executable. +The persistent-shell plugin is included in the Python runtime closure together with the PTY service/local backend and the sandbox services required by that backend. Because `node-pty` executes a native `spawn-helper` on macOS, each packaged macOS runtime executable ships with a `-spawn-helper` sibling; Linux uses `forkpty` directly. A pinned `node-pty` patch checks `DSH_NODE_PTY_SPAWN_HELPER` first, so it remains a true override for a current external consumer that supplies a non-sibling helper. When the override is unset, the patch resolves the packaged executable sibling if present and otherwise preserves upstream lookup in ordinary Node runs. The macOS builders fail before publication when the helper is absent or not executable. -The shipped [`minimal` agent preset](../../../../packages/preset/agent-presets/presets/minimal/agent.cordis.yml) composes only the persistent-shell plugin for the minimal model contract. Its entry-local PTY realm carries the registry, local backend, and persistent Bash or PowerShell tool. The preset fixes the complete system prompt, follows the deployment tool-presentation mode, omits every other model-facing consumer, and leaves browser, Workspace, persistence, sandbox, and permission services on the shared Web host. The local PTY backend resolves the effective session sandbox mode when it creates the shell. While that owner has an open shell or a spawn in progress, a different permission mode is rejected before its session event commits. The [persistent-shell-only decision](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.md) owns the editor's absence from minimal compositions; the editor remains independently mountable elsewhere. +The shipped [`minimal` agent preset](../../../../packages/preset/agent-presets/presets/minimal/agent.cordis.yml) composes only the persistent-shell plugin for the minimal model contract. Its entry-local PTY realm carries the registry, local backend, and persistent Bash or PowerShell tool. The preset fixes the complete system prompt, follows the deployment tool-presentation mode, omits every other model-facing consumer, and leaves browser, Workspace, persistence, sandbox, and permission services on the shared Web host. The local PTY backend resolves the effective session sandbox mode when it creates the shell. While that owner has an open shell or a spawn in progress, a different permission mode is rejected before its session event commits. The [persistent-shell-only decision](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.md) owns the exact minimal composition; the complete-removal decision owns the editor package's absence from the distribution. ## Alternatives considered @@ -34,4 +36,4 @@ The shipped [`minimal` agent preset](../../../../packages/preset/agent-presets/p ## Consequences -Profiles can reproduce an external agent by configuring persona and descriptions while the underlying packages remain general. Persistent Bash requires an owning Agent and real PTY backend. Shell exit, timeout, or cancellation loses state. The editor delegates security and mutation policy to the mounted filesystem stack. Nullable branches increase the command-specific fields' schema cost so unused placeholders do not force retries; execution keeps the selected command's required and deletion semantics explicit. A minimal Web agent retains Web permissions but must close its persistent shell before changing modes. Runtime-wheel consumers still need no Node installation; Linux wheels contain one executable, while macOS wheels also contain its private native helper. +Profiles can reproduce an external agent by configuring persona and descriptions while the persistent-shell package remains general. Persistent Bash requires an owning Agent and real PTY backend. Shell exit, timeout, or cancellation loses state. A minimal Web agent retains Web permissions but must close its persistent shell before changing modes. Runtime-wheel consumers still need no Node installation; Linux wheels contain one executable, while macOS wheels also contain its private native helper. The editor-specific consequences no longer apply after its package removal. diff --git a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.zh.md b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.zh.md index beb2e17d2d..49e315fc78 100644 --- a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.zh.md +++ b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.zh.md @@ -14,11 +14,13 @@ Status: implemented `@deepseek-ai/dsh-tool-str-replace-editor` 独立消费 `ctx.fs`,注册包含 `view`、`create`、`str_replace` 与 `insert` 的 `str_replace_editor`。它提供带行号文本查看、过滤后的两层目录列表、唯一字面量替换、规范插入边界和有界输出。路径必须为绝对路径;文件查看会保留内容中的制表符,因此复制的文本仍可作为有效的字面量替换输入;变更会保留请求编辑范围之外的制表符;公开 schema 与错误则只使用 `old_str`。命令专属字段接受 `null` 占位参数:当前命令不使用该字段时,执行会将其视为未提供;必填检查保持不变;`view_range: null` 表示查看完整文件;`str_replace.new_str: null` 会被拒绝,只有省略该字段才表示删除。它可以与持久 Bash、一次性 Bash、沙箱 Bash 或无 shell 组合。 +后续的[完整移除决策](../simplification/2026-09-03-remove-str-replace-editor.zh.md)取代了前述 editor 部分并删除该包。本 note 只继续负责持久 shell 行为,以及两个能力最初分离的历史理由。 + `dsh-system-prompt` 接受 `includeHarnessIdentity: false`,profile 组合同时拥有该配置行与所选 shell 工具配置行。`sdk-minimal` 将该值设为 `false`,且只挂载按平台选择的持久 shell,因此部署可以拥有精确 persona,而不会重复注册提示词或工具。既有默认值不变。 -两个插件都进入 Python runtime 闭包。持久 Bash 的闭包还包含 PTY 服务/本地后端,以及该后端要求的沙箱服务。由于 `node-pty` 在 macOS 上会执行原生 `spawn-helper`,每个打包后的 macOS 运行时可执行文件都会携带一个 `-spawn-helper` 伴随文件;Linux 直接使用 `forkpty`。固定版本的 `node-pty` 补丁会先检查 `DSH_NODE_PTY_SPAWN_HELPER`,因此对当前提供非伴随 helper 的外部消费方而言,该变量仍是真正的覆盖项。未设置该覆盖时,补丁会在打包可执行文件的伴随文件存在时解析它,否则在普通 Node 运行中保留上游查找方式。若 helper 缺失或不可执行,macOS 构建器会在发布前失败。 +持久 shell 插件进入 Python runtime 闭包,并携带 PTY 服务/本地后端以及该后端要求的沙箱服务。由于 `node-pty` 在 macOS 上会执行原生 `spawn-helper`,每个打包后的 macOS 运行时可执行文件都会携带一个 `-spawn-helper` 伴随文件;Linux 直接使用 `forkpty`。固定版本的 `node-pty` 补丁会先检查 `DSH_NODE_PTY_SPAWN_HELPER`,因此对当前提供非伴随 helper 的外部消费方而言,该变量仍是真正的覆盖项。未设置该覆盖时,补丁会在打包可执行文件的伴随文件存在时解析它,否则在普通 Node 运行中保留上游查找方式。若 helper 缺失或不可执行,macOS 构建器会在发布前失败。 -随附的 [`minimal` agent preset](../../../../packages/preset/agent-presets/presets/minimal/agent.cordis.yml)只为极简模型约定组合持久 shell 插件。其 entry 本地 PTY realm 持有注册表、本地后端和持久 Bash 或 PowerShell 工具。Preset 会固定完整系统提示词、跟随部署的工具呈现模式,省略其他所有面向模型的消费方,并将浏览器、Workspace、持久化、沙箱与权限服务留在共享 Web 宿主上。本地 PTY 后端会在创建 shell 时解析会话的有效沙箱模式。只要该所有者仍有打开的 shell 或仍在进行中的 spawn,另一种权限模式就会在对应的会话事件提交前遭到拒绝。[仅持久 shell 决策](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md)负责极简组合不包含 editor 的事实;其他组合仍可独立挂载 editor。 +随附的 [`minimal` agent preset](../../../../packages/preset/agent-presets/presets/minimal/agent.cordis.yml)只为极简模型约定组合持久 shell 插件。其 entry 本地 PTY realm 持有注册表、本地后端和持久 Bash 或 PowerShell 工具。Preset 会固定完整系统提示词、跟随部署的工具呈现模式,省略其他所有面向模型的消费方,并将浏览器、Workspace、持久化、沙箱与权限服务留在共享 Web 宿主上。本地 PTY 后端会在创建 shell 时解析会话的有效沙箱模式。只要该所有者仍有打开的 shell 或仍在进行中的 spawn,另一种权限模式就会在对应的会话事件提交前遭到拒绝。[仅持久 shell 决策](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md)负责精确极简组合;完整移除决策负责发行物不再包含 editor 包。 ## 考虑过的替代方案 @@ -34,4 +36,4 @@ Status: implemented ## 后果 -Profile 可以通过配置 persona 和描述复现外部 Agent,而底层包保持通用。持久 Bash 需要拥有它的 Agent 与真实 PTY 后端;shell 退出、超时或取消会丢失状态。编辑器把安全与变更策略委托给挂载的文件系统栈。可为 `null` 的分支增加了命令专属字段的 schema 成本,使未使用的占位参数不会迫使模型重试;执行仍明确保留当前命令的必填与删除语义。minimal Web agent 保留 Web 权限,但必须先关闭持久 shell 才能更改权限模式。运行时 wheel 包的消费方仍无需安装 Node;Linux wheel 包包含一个可执行文件,macOS wheel 包还包含其私有原生 helper。 +Profile 可以通过配置 persona 和描述复现外部 Agent,而持久 shell 包保持通用。持久 Bash 需要拥有它的 Agent 与真实 PTY 后端;shell 退出、超时或取消会丢失状态。minimal Web agent 保留 Web 权限,但必须先关闭持久 shell 才能更改权限模式。运行时 wheel 包的消费方仍无需安装 Node;Linux wheel 包包含一个可执行文件,macOS wheel 包还包含其私有原生 helper。删除 editor 包后,其专属后果不再适用。 diff --git a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml index 33e4ac93a8..e8b141e733 100644 --- a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md -2026-07-31-even-out-shipped-tool-rosters.md: 4fea5fb30860f3254aec6178068158b4e7756493 -2026-07-31-even-out-shipped-tool-rosters.zh.md: 32c188f6419b9ede74838215848d77f139280cde +2026-07-31-even-out-shipped-tool-rosters.md: c16b8676f666f648f7e6ed87d24ebc9661cbc48a +2026-07-31-even-out-shipped-tool-rosters.zh.md: 2784ca5bdc86eb48729134b5fc8bf24fd865cda2 diff --git a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md index 4fea5fb308..c16b8676f6 100644 --- a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md +++ b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md @@ -12,11 +12,11 @@ The result was a user-visible difference nobody had decided: the same model, ask ## Decision -The rows that are not surface-specific move into [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml), and three more join them: `tool-session-query`, `tool-str-replace-editor`, and `repeat-tool-reminder`. Web search moves there too; its [deployment decision](2026-07-31-web-default-search.md) owns the security boundary while the shared base owns its surface-neutral mount. Both surfaces assemble the same roster, including fixed `glob` and `grep` members because `dsh-tool-fs-search` spawns the [packaged ripgrep binary](../architecture/2026-08-01-packaged-ripgrep-search.md). Later decisions narrow that roster: the [session-search decision](2026-08-02-session-search-not-shipped-default.md) keeps `tool-session-query` opt-in, the [single-editor decision](../simplification/2026-08-10-default-presets-single-editor.md) removes `tool-str-replace-editor` from general-purpose presets, and the [persistent-shell-only decision](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.md) removes it from the minimal compositions. +The rows that are not surface-specific move into [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml), and three more join them: `tool-session-query`, `tool-str-replace-editor`, and `repeat-tool-reminder`. Web search moves there too; its [deployment decision](2026-07-31-web-default-search.md) owns the security boundary while the shared base owns its surface-neutral mount. Both surfaces assemble the same roster, including fixed `glob` and `grep` members because `dsh-tool-fs-search` spawns the [packaged ripgrep binary](../architecture/2026-08-01-packaged-ripgrep-search.md). Later decisions narrow that roster: the [session-search decision](2026-08-02-session-search-not-shipped-default.md) keeps `tool-session-query` opt-in, the [single-editor decision](../simplification/2026-08-10-default-presets-single-editor.md) removes `tool-str-replace-editor` from general-purpose presets, the [persistent-shell-only decision](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.md) removes it from the minimal compositions, and the [complete-removal decision](../simplification/2026-09-03-remove-str-replace-editor.md) later deletes the package and shared-base row recorded here. Two rows stay surface-specific. `tmux-context` is TUI-only because a browser surface has no terminal multiplexer to describe. `session-reference` is TUI-only because it drives the shared session-query index from the launcher's process-local path, and the browser sidebar reconciles that index on its own first search. -**This roster decision added only at the time.** No tool row was removed from either surface when it landed, and a catalog comparison found additions and nothing else. The later session-search and single-editor decisions own their respective default-roster exceptions. The shared executors, sandbox composition, and access default are owned independently by the [workspace-write default decision](2026-07-31-workspace-write-surface-default.md). +**This roster decision added only at the time.** No tool row was removed from either surface when it landed, and a catalog comparison found additions and nothing else. The later session-search, single-editor, and complete-removal decisions own their respective current exceptions. The shared executors, sandbox composition, and access default are owned independently by the [workspace-write default decision](2026-07-31-workspace-write-surface-default.md). ### What stays unmounted, and why diff --git a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md index 32c188f641..2784ca5bdc 100644 --- a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md +++ b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md @@ -12,11 +12,11 @@ Status: implemented ## 决策 -那些并非 surface 专属的行移入 [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml),另有三行加入:`tool-session-query`、`tool-str-replace-editor` 和 `repeat-tool-reminder`。Web 搜索也一并移入;其[部署决策](2026-07-31-web-default-search.zh.md)负责安全边界,共享 base 则负责与 surface 无关的挂载。两个 surface 组装同一份清单,其中 `glob` 和 `grep` 是固定成员,因为 `dsh-tool-fs-search` 直接 spawn [打包的 ripgrep 二进制](../architecture/2026-08-01-packaged-ripgrep-search.zh.md)。后续决策收窄了这份清单:[session-search 决策](2026-08-02-session-search-not-shipped-default.zh.md)让 `tool-session-query` 保持需显式启用,[单一 editor 决策](../simplification/2026-08-10-default-presets-single-editor.zh.md)从通用 preset 移除 `tool-str-replace-editor`,[仅持久 shell 决策](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md)则从极简组合移除它。 +那些并非 surface 专属的行移入 [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml),另有三行加入:`tool-session-query`、`tool-str-replace-editor` 和 `repeat-tool-reminder`。Web 搜索也一并移入;其[部署决策](2026-07-31-web-default-search.zh.md)负责安全边界,共享 base 则负责与 surface 无关的挂载。两个 surface 组装同一份清单,其中 `glob` 和 `grep` 是固定成员,因为 `dsh-tool-fs-search` 直接 spawn [打包的 ripgrep 二进制](../architecture/2026-08-01-packaged-ripgrep-search.zh.md)。后续决策收窄了这份清单:[session-search 决策](2026-08-02-session-search-not-shipped-default.zh.md)让 `tool-session-query` 保持需显式启用,[单一 editor 决策](../simplification/2026-08-10-default-presets-single-editor.zh.md)从通用 preset 移除 `tool-str-replace-editor`,[仅持久 shell 决策](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md)从极简组合移除它,随后[完整移除决策](../simplification/2026-09-03-remove-str-replace-editor.zh.md)删除了本记录中的包与共享 base 行。 有两行仍是 surface 专属。`tmux-context` 只在 TUI,因为浏览器 surface 没有终端复用器可描述。`session-reference` 只在 TUI,因为它以 launcher 的进程本地路径驱动共享的 session-query 索引,而浏览器侧边栏会在自己的首次搜索里重建该索引。 -**本次工具清单决策当时只做加法。** 落地时两个 surface 均未移除任何工具行,目录对比只发现了新增,别无其他。后续的 session-search 与单一编辑器决策分别负责对应的默认清单例外。共享执行器、沙箱组合与访问默认值独立归属[workspace-write 默认值决策](2026-07-31-workspace-write-surface-default.zh.md)。 +**本次工具清单决策当时只做加法。** 落地时两个 surface 均未移除任何工具行,目录对比只发现了新增,别无其他。后续的 session-search、单一编辑器与完整移除决策分别负责当前对应的例外。共享执行器、沙箱组合与访问默认值独立归属[workspace-write 默认值决策](2026-07-31-workspace-write-surface-default.zh.md)。 ### 什么保持不挂,以及为什么 diff --git a/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.i18n.yaml index e8023fd069..b12f647e47 100644 --- a/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.md -2026-07-31-web-workspace-file-links.md: 944d7bef9b83eb34895c5f74e511600a713d786b -2026-07-31-web-workspace-file-links.zh.md: 71f0051204e0cb8b65995fc88a9edb230db8f5be +2026-07-31-web-workspace-file-links.md: ce0ca591cbe0ffc17d33e56f7a680912f3d86231 +2026-07-31-web-workspace-file-links.zh.md: e801c2e67a4e9c21f7d39f6644f0bf94fe8f1fa1 diff --git a/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.md b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.md index 944d7bef9b..ce0ca591cb 100644 --- a/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.md +++ b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.md @@ -14,7 +14,7 @@ Two distinct defects sat behind that. The transcript never said what a turn had ## Decision -**A finished turn ends with the files it produced.** The row is its own plugin, `@deepseek-ai/dsh-client-ui-deliverables`, registered into the `conversation.chat.turnTail` hole the chat view renders between a closing message's body and its IconActions — ui-conversation owns the hole and the owner currency (nodes, closing seq, `openFile`), the plugin owns every policy. `producedForClosing` reads the paths off the mutation tools' own follow-along `locations` — a diff card, or a generic card whose `kind` is `edit` (the shape `str_replace_editor`'s insert presents) — so a turn's output is listed whether or not the closing message named it, and a new mutation tool joins by declaring what it does rather than by being added to a list. Reads, deletes, and failed calls contribute nothing; a path appears once per turn in first-seen order; accumulation resets on the turn boundary, so a turn that mutates and then ends without content text cannot spill into the next turn's row. CSS container-width bands select a prefix of up to six paths and its matching `+ N files` label, while flexbox shrinks and ellipsizes the visible basenames; the component performs no JavaScript layout observation ([decision](../simplification/2026-09-01-css-produced-file-layout.md)). One cordis.yml line composes the surface in or out; the unregistered hole renders nothing. +**A finished turn ends with the files it produced.** The row is its own plugin, `@deepseek-ai/dsh-client-ui-deliverables`, registered into the `conversation.chat.turnTail` hole the chat view renders between a closing message's body and its IconActions — ui-conversation owns the hole and the owner currency (nodes, closing seq, `openFile`), the plugin owns every policy. `producedForClosing` reads the paths off the mutation tools' own follow-along `locations` — a diff card or a generic card whose `kind` is `edit` — so a turn's output is listed whether or not the closing message named it, and a new mutation tool joins by declaring what it does rather than by being added to a list. Reads, deletes, and failed calls contribute nothing; a path appears once per turn in first-seen order; accumulation resets on the turn boundary, so a turn that mutates and then ends without content text cannot spill into the next turn's row. CSS container-width bands select a prefix of up to six paths and its matching `+ N files` label, while flexbox shrinks and ellipsizes the visible basenames; the component performs no JavaScript layout observation ([decision](../simplification/2026-09-01-css-produced-file-layout.md)). One cordis.yml line composes the surface in or out; the unregistered hole renders nothing. **The path link reads as a link.** Underlined at rest, not only on hover. This is the smaller half of the diff and the larger half of the fix. diff --git a/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.zh.md b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.zh.md index 71f0051204..e801c2e67a 100644 --- a/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.zh.md +++ b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.zh.md @@ -14,7 +14,7 @@ Status: implemented ## 决策 -**完成的一轮以它产出的文件收尾。** 该行是独立插件 `@deepseek-ai/dsh-client-ui-deliverables`,注册进 chat 视图在收尾消息正文与其 IconActions 之间渲染的 `conversation.chat.turnTail` 空位——ui-conversation 拥有空位与 owner 通货(节点、收尾 seq、`openFile`),插件拥有全部策略。`producedForClosing` 从改写工具自身的跟随文件 `locations` 中读出路径——diff 卡片,或 `kind` 为 `edit` 的 generic 卡片(即 `str_replace_editor` 的 insert 所呈现的形状)——因此无论收尾消息是否点名,这一轮的产出都会被列出;新的改写工具靠声明自己做了什么加入,而不是靠被加进某张名单。read、删除与失败的调用不贡献任何条目;同一路径在一轮内按首见顺序只出现一次;累积在 turn 边界重置,因此一轮若先改写文件、随后没有正文内容就结束,不会溢进下一轮的行里。CSS 容器宽度档位会选择前六条路径中的一个前缀及其匹配的 `+ N 个文件` 标签,flexbox 则收缩可见的 basename 并用 ellipsis 省略;组件不执行 JavaScript 布局观察([决策](../simplification/2026-09-01-css-produced-file-layout.zh.md))。cordis.yml 中的一行即可把该交互面组合进来或去掉;未注册的空位什么也不渲染。 +**完成的一轮以它产出的文件收尾。** 该行是独立插件 `@deepseek-ai/dsh-client-ui-deliverables`,注册进 chat 视图在收尾消息正文与其 IconActions 之间渲染的 `conversation.chat.turnTail` 空位——ui-conversation 拥有空位与 owner 通货(节点、收尾 seq、`openFile`),插件拥有全部策略。`producedForClosing` 从改写工具自身的跟随文件 `locations` 中读出路径——diff 卡片或 `kind` 为 `edit` 的 generic 卡片——因此无论收尾消息是否点名,这一轮的产出都会被列出;新的改写工具靠声明自己做了什么加入,而不是靠被加进某张名单。read、删除与失败的调用不贡献任何条目;同一路径在一轮内按首见顺序只出现一次;累积在 turn 边界重置,因此一轮若先改写文件、随后没有正文内容就结束,不会溢进下一轮的行里。CSS 容器宽度档位会选择前六条路径中的一个前缀及其匹配的 `+ N 个文件` 标签,flexbox 则收缩可见的 basename 并用 ellipsis 省略;组件不执行 JavaScript 布局观察([决策](../simplification/2026-09-01-css-produced-file-layout.zh.md))。cordis.yml 中的一行即可把该交互面组合进来或去掉;未注册的空位什么也不渲染。 **路径链接读得出是链接。** 静止状态下就带下划线,而不只在悬停时。这是本次改动中更小的那一半,却是修复中更大的那一半。 diff --git a/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.i18n.yaml b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.i18n.yaml index 5d39c528e2..858227b5e5 100644 --- a/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.i18n.yaml +++ b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.md -2026-08-10-default-presets-single-editor.md: ef18f8d7dd2242c8af53af970df0a06b8d7d09d3 -2026-08-10-default-presets-single-editor.zh.md: c9b57e1ab3fa8d40ac587590f374f1f3747b7e7f +2026-08-10-default-presets-single-editor.md: 02d0ddddacbb045aa704890e01b9d710179f6eac +2026-08-10-default-presets-single-editor.zh.md: 0a4942d81a4bc58adcdf8c54e09b349cf2954377 diff --git a/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.md b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.md index ef18f8d7dd..02d0ddddac 100644 --- a/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.md +++ b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.md @@ -10,16 +10,16 @@ The `standard`, `code`, and `cordis` presets exposed both the `read`/`write`/`ed ## Decision -The `standard`, `code`, and `cordis` preset configurations mount `dsh-tool-fs` and `dsh-tool-fs-search`, but do not mount `dsh-tool-str-replace-editor`. PTC mode therefore omits `str_replace_editor` from both its registry and generated SDK. The later [persistent-shell-only decision](2026-09-03-minimal-profiles-persistent-shell-only.md) removes the editor from the shipped `minimal` and `sdk-minimal` compositions. Deployments and user-authored presets may still mount the plugin explicitly. +The `standard`, `code`, and `cordis` preset configurations mount `dsh-tool-fs` and `dsh-tool-fs-search`, but do not mount the standalone editor. PTC mode therefore omits that editor schema from both its registry and generated SDK. The later [persistent-shell-only decision](2026-09-03-minimal-profiles-persistent-shell-only.md) removes the editor from the shipped `minimal` and `sdk-minimal` compositions. The [complete-removal decision](2026-09-03-remove-str-replace-editor.md) subsequently deletes the standalone package and its runtime support. -This decision narrows the preset roster rather than removing the tool package or its Python runtime support. The earlier [shared-roster decision](../feature/2026-07-31-even-out-shipped-tool-rosters.md) continues to own why surface-neutral tools live in preset composition; this note owns the editor exception. +This decision originally narrowed only the preset roster. The later complete-removal decision owns the package and Python runtime deletion. The earlier [shared-roster decision](../feature/2026-07-31-even-out-shipped-tool-rosters.md) continues to own why surface-neutral tools live in preset composition; this note owns the earlier preset exception. ## Alternatives considered **Keep both editing interfaces in the general-purpose presets.** Rejected because the overlapping model-visible schemas increase tool choice without supplying a separate default operation. -**Remove the `str_replace_editor` package from the distribution.** Rejected because explicit deployments remain valid consumers of the standalone plugin. +**Remove the editor package from the distribution.** Rejected at the time because explicit deployments were treated as valid consumers. The later complete-removal decision accepts that compatibility break for v41. ## Consequences -General-purpose agents use `read`, `write`, and `edit` for filesystem mutations, while minimal agents use their persistent shell. Preset composition tests pin `str_replace_editor`'s absence from every shipped preset and the PTC mode SDK. +General-purpose agents use `read`, `write`, and `edit` for filesystem mutations, while minimal agents use their persistent shell. The complete-removal decision now guarantees the obsolete editor cannot be mounted from the distribution. diff --git a/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.zh.md b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.zh.md index c9b57e1ab3..0a4942d81a 100644 --- a/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.zh.md +++ b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.zh.md @@ -10,16 +10,16 @@ Status: implemented ## 决策 -`standard`、`code` 和 `cordis` preset 配置挂载 `dsh-tool-fs` 与 `dsh-tool-fs-search`,但不挂载 `dsh-tool-str-replace-editor`。因此 PTC mode 的注册表和生成的 SDK 均不包含 `str_replace_editor`。后续的[仅持久 shell 决策](2026-09-03-minimal-profiles-persistent-shell-only.zh.md)会从随附的 `minimal` 与 `sdk-minimal` 组合中移除 editor。部署配置与用户自定义 preset 仍可显式挂载该插件。 +`standard`、`code` 和 `cordis` preset 配置挂载 `dsh-tool-fs` 与 `dsh-tool-fs-search`,但不挂载独立 editor。因此 PTC mode 的注册表和生成 SDK 均不包含该 editor schema。后续的[仅持久 shell 决策](2026-09-03-minimal-profiles-persistent-shell-only.zh.md)从随附的 `minimal` 与 `sdk-minimal` 组合中移除 editor;再后的[完整移除决策](2026-09-03-remove-str-replace-editor.zh.md)删除独立包及其 runtime 支持。 -此决策收窄 preset 工具清单,不移除工具包及其 Python 运行时支持。较早的[共享清单决策](../feature/2026-07-31-even-out-shipped-tool-rosters.zh.md)继续说明与 surface 无关的工具为何归 preset 组合所有;本记录说明编辑器例外。 +此决策最初只收窄 preset 工具清单;后续完整移除决策负责删除包与 Python runtime。较早的[共享清单决策](../feature/2026-07-31-even-out-shipped-tool-rosters.zh.md)继续说明与 surface 无关的工具为何归 preset 组合所有;本记录说明早期的编辑器例外。 ## 曾考虑的替代方案 **在通用 preset 中保留两套编辑接口。** 不予采用,因为重叠的模型可见 schema 增加了工具选择,却没有提供不同的默认操作。 -**从发行物中删除 `str_replace_editor` 包。** 不予采用,因为显式部署仍是该独立插件的有效消费方。 +**从发行物中删除 editor 包。** 当时不予采用,因为显式部署被视为有效消费方。后续完整移除决策为 v41 接受了这一兼容性破坏。 ## 后果 -通用 agent 使用 `read`、`write` 与 `edit` 完成文件系统修改,minimal agent 则使用持久 shell。Preset 组合测试会固定每份随附 preset 及 PTC mode SDK 都不包含 `str_replace_editor`。 +通用 agent 使用 `read`、`write` 与 `edit` 完成文件系统修改,minimal agent 则使用持久 shell。完整移除决策现在保证发行物无法再挂载过时 editor。 diff --git a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml index 077fec32c1..f3fb2579cd 100644 --- a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml +++ b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md -2026-09-03-minimal-profiles-persistent-shell-only.md: fbbdd63c3bd8928be49e9eb78f76c5c2824637a0 -2026-09-03-minimal-profiles-persistent-shell-only.zh.md: 1a7cc625a2ce279da96b1d765151f17e63ce4201 +2026-09-03-minimal-profiles-persistent-shell-only.md: 8814cd94dc85f3b56d8ab55b8f7c00367ff3d0b6 +2026-09-03-minimal-profiles-persistent-shell-only.zh.md: 9b3ab9a756a9ef38410bbc2b1e9337070ef28583 diff --git a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md index fbbdd63c3b..8814cd94dc 100644 --- a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md +++ b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md @@ -14,20 +14,20 @@ The next-generation model's minimal runtime requires one model-facing tool. Leav The shipped minimal compositions expose exactly one platform-selected persistent shell: `bash` on Linux and macOS, or `pwsh` on Windows. Neither composition mounts `@deepseek-ai/dsh-tool-str-replace-editor`, a filesystem tool, or the `fs-local` service that supported the editor. The fixed complete persona, absence of runtime context and compaction, shell timeout, and launch-specific host services remain unchanged. -The standalone editor package and its consumers outside the two minimal compositions remain supported. A trusted user-authored preset or higher profile patch can mount it explicitly; the shipped `minimal` and `sdk-minimal` defaults never insert it. +The later [complete-removal decision](2026-09-03-remove-str-replace-editor.md) deletes the standalone editor package and all consumers. This note continues to own the exact one-shell composition of the shipped `minimal` and `sdk-minimal` defaults. -Exact composition tests assert the single tool and the absence of a preset-local filesystem service. The `sdk-minimal` bundle test and built config dump assert that its row and dependency allowlists contain neither `fs-local` nor `dsh-tool-str-replace-editor`. Web and packaged-Python model-visible snapshots pin the one-tool schema roster. +Exact composition tests assert the single tool and the absence of a preset-local filesystem service. The `sdk-minimal` bundle test and built config dump assert that their row and dependency allowlists contain no unused filesystem provider. Web and packaged-Python model-visible snapshots pin the one-tool schema roster. -This decision partially supersedes the minimal exceptions in [one editor family in general-purpose presets](2026-08-10-default-presets-single-editor.md), [the minimal preset composition](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md), [the bare minimal runtime](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md), and [the standalone sdk-minimal profile](../architecture/2026-08-24-standalone-sdk-minimal-profile.md). Those notes retain authority for the independent editor package, prompt ownership, no-compaction behavior, profile launch, and bundle layering. +This decision partially supersedes the minimal exceptions in [one editor family in general-purpose presets](2026-08-10-default-presets-single-editor.md), [the minimal preset composition](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md), [the bare minimal runtime](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md), and [the standalone sdk-minimal profile](../architecture/2026-08-24-standalone-sdk-minimal-profile.md). Those notes retain authority for prompt ownership, no-compaction behavior, profile launch, and bundle layering; the complete-removal decision owns the package deletion. ## Alternatives considered **Keep the editor row and hide its schema.** Rejected because a presentation or restriction layer would leave the capability in the minimal composition and make its absence depend on another setting. -**Remove the editor package from the distribution.** Rejected because full profiles and explicit custom compositions remain valid consumers. The requirement concerns the two shipped minimal defaults. +**Remove the editor package from the distribution.** Rejected at the time because explicit custom compositions were treated as valid consumers. The later complete-removal decision accepts the v41 compatibility break. **Keep the editor only in `sdk-minimal`.** Rejected because the two minimal paths would present different tool contracts to the same model class, and the packaged SDK path would retain the schema cost and unused filesystem service. ## Consequences -Minimal agents inspect and modify files through their persistent shell. Their model requests carry one tool schema, and their compositions own no filesystem service. Full profiles, the editor package, and explicit custom compositions keep their existing behavior. +Minimal agents inspect and modify files through their persistent shell. Their model requests carry one tool schema, and their compositions own no filesystem service. General-purpose profiles keep the native filesystem tools; the obsolete editor package no longer ships. diff --git a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md index 1a7cc625a2..9b3ab9a756 100644 --- a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md +++ b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md @@ -14,20 +14,20 @@ Status: implemented 随附的极简组合只提供一个按平台选择的持久 shell:Linux 与 macOS 使用 `bash`,Windows 使用 `pwsh`。两份组合都不挂载 `@deepseek-ai/dsh-tool-str-replace-editor`、文件系统工具或支撑 editor 的 `fs-local` 服务。固定的 complete persona、运行时上下文与 compaction 的缺失、shell 超时和各启动路径的宿主服务保持不变。 -独立 editor 包及其在两份极简组合之外的消费方继续受支持。受信任的用户自定义 preset 或更高优先级的 profile patch 可以显式挂载它;随附的 `minimal` 与 `sdk-minimal` 默认组合不会插入它。 +后续的[完整移除决策](2026-09-03-remove-str-replace-editor.zh.md)删除独立 editor 包及其全部消费方。本 note 继续负责随附 `minimal` 与 `sdk-minimal` 默认组合的精确单 shell 组成。 -精确组合测试会断言单工具清单以及 preset 内不存在文件系统服务。`sdk-minimal` bundle 测试与构建后配置转储会断言配置项和依赖 allowlist 都不含 `fs-local` 或 `dsh-tool-str-replace-editor`。Web 与打包 Python 的模型可见快照会固定单工具 schema 清单。 +精确组合测试会断言单工具清单以及 preset 内不存在文件系统服务。`sdk-minimal` bundle 测试与构建后配置转储会断言配置项和依赖 allowlist 都不含未使用的文件系统 provider。Web 与打包 Python 的模型可见快照会固定单工具 schema 清单。 -该决策部分取代[通用 preset 使用一种 editor](2026-08-10-default-presets-single-editor.zh.md)、[minimal preset 组合](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md)、[极简裸运行时](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md)与[独立 sdk-minimal profile](../architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md)中的极简例外。这些 Agent Note 继续负责独立 editor 包、提示词所有权、无 compaction 行为、profile 启动与 bundle 分层。 +该决策部分取代[通用 preset 使用一种 editor](2026-08-10-default-presets-single-editor.zh.md)、[minimal preset 组合](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md)、[极简裸运行时](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md)与[独立 sdk-minimal profile](../architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md)中的极简例外。这些 Agent Note 继续负责提示词所有权、无 compaction 行为、profile 启动与 bundle 分层;完整移除决策负责删除包。 ## 考虑过的替代方案 **保留 editor 配置项并隐藏其 schema。** 不予采用,因为呈现层或限制层会让该能力继续留在极简组合中,并使其缺失依赖另一项设置。 -**从发行物中删除 editor 包。** 不予采用,因为完整 profile 与显式自定义组合仍是有效消费方。本需求只涉及两份随附的极简默认组合。 +**从发行物中删除 editor 包。** 当时不予采用,因为显式自定义组合被视为有效消费方。后续完整移除决策接受了 v41 兼容性破坏。 **只在 `sdk-minimal` 中保留 editor。** 不予采用,因为两条极简路径会向同类模型提供不同的工具约定,而且打包 SDK 路径仍会承担 schema 成本和未被其他配置项使用的文件系统服务。 ## 后果 -极简 agent 通过持久 shell 检查和修改文件。模型请求只携带一个工具 schema,组合不拥有文件系统服务。完整 profile、editor 包与显式自定义组合保持现有行为。 +极简 agent 通过持久 shell 检查和修改文件。模型请求只携带一个工具 schema,组合不拥有文件系统服务。通用 profile 保留原生文件系统工具;过时 editor 包不再发布。 diff --git a/.agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.i18n.yaml b/.agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.i18n.yaml new file mode 100644 index 0000000000..e0a984690a --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.md +2026-09-03-remove-str-replace-editor.md: ac3f173d06af5db28f04839ae0551129ed09c586 +2026-09-03-remove-str-replace-editor.zh.md: 4f6ad91573e734dd463a1fcc5f3b0739ea1607be diff --git a/.agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.md b/.agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.md new file mode 100644 index 0000000000..ac3f173d06 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.md @@ -0,0 +1,33 @@ +# Agent Note: Remove the string-replacement editor + +Status: implemented + +English | [中文](2026-09-03-remove-str-replace-editor.zh.md) + +## Problem + +No shipped profile still needs the standalone `str_replace_editor` interface. General-purpose profiles already expose `read`, `write`, and `edit`, while the next-generation minimal contract exposes only its persistent shell. Keeping the dormant package would still ship its implementation, dependency closure, configuration surface, generated catalog entries, client presentation branches, and model-schema snapshots. + +A hidden or unmounted tool is not a complete removal. User-authored configuration could still mount it, and future composition changes could accidentally restore the schema. + +## Decision + +Delete `@deepseek-ai/dsh-tool-str-replace-editor` from the workspace and distribution. Remove its shared-base row, disabled Web override, CLI and Python runtime dependencies, TypeScript project aliases, tool-catalog registration, client-specific diff and produced-file handling, package documentation, tests, and recorded scenario. Regenerated configuration catalogs, module graphs, composition diagrams, and request snapshots must contain no package or model-visible tool entry. + +No compatibility alias, hidden registration, or replacement package is retained. General-purpose profiles keep `read`, `write`, `edit`, `glob`, and `grep`; minimal profiles keep their platform-selected persistent shell. Historical unknown tool events can use the existing generic client fallback, but the project does not advertise or test the removed contract. + +This decision supersedes the editor half of [the original persistent-tool decision](../feature/2026-07-29-persistent-bash-str-replace-editor.md), [the standalone-package exception](2026-08-10-default-presets-single-editor.md), and [the minimal-profile scope boundary](2026-09-03-minimal-profiles-persistent-shell-only.md). The original note continues to own persistent Bash behavior; the minimal-profile note continues to own the one-shell contract. + +## Alternatives considered + +**Keep the package for explicit custom compositions.** Rejected because there is no current shipped consumer, and an opt-in package still expands the supported and published surface. + +**Leave the registration in place and filter it from model requests.** Rejected because presentation settings could restore the capability and every runtime would continue carrying inactive code. + +**Provide an alias or compatibility shim over `read`/`write`/`edit`.** Rejected because the project is pre-release and a shim would preserve the schema and maintenance cost this change removes. + +## Consequences + +Existing user-authored configurations that name the deleted package fail loudly and must migrate to `read`, `write`, `edit`, or a shell. This is an accepted v41 pre-release compatibility break. + +The removed interface can return only through a new decision with a distinct current consumer, explicit composition owner, package and runtime closure, model-visible documentation, and tests. Until then, source, published manifests, generated catalogs, and current snapshots contain no `str_replace_editor` tool. diff --git a/.agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.zh.md b/.agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.zh.md new file mode 100644 index 0000000000..4f6ad91573 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.zh.md @@ -0,0 +1,33 @@ +# Agent Note:移除字符串替换编辑器 + +状态:已实现 + +[English](2026-09-03-remove-str-replace-editor.md) | 中文 + +## 问题 + +现有随附 profile 都不再需要独立的 `str_replace_editor` 接口。通用 profile 已提供 `read`、`write` 与 `edit`,下一代模型的 minimal contract 则只提供持久 shell。继续保留这个闲置包,仍会发布其实现、依赖闭包、配置面、生成目录条目、客户端展示分支和模型 schema 快照。 + +隐藏或不挂载工具不等于完整移除。用户自定义配置仍可挂载它,未来的组合改动也可能意外恢复该 schema。 + +## 决策 + +从 workspace 与发行物中删除 `@deepseek-ai/dsh-tool-str-replace-editor`。移除共享 base 行、已禁用的 Web override、CLI 与 Python runtime 依赖、TypeScript project alias、工具目录注册、客户端专用 diff 与 produced-file 处理、包文档、测试和录制场景。重新生成的配置目录、模块图、组合图和请求快照不得包含该包或面向模型的工具条目。 + +不保留兼容 alias、隐藏注册或替代包。通用 profile 保留 `read`、`write`、`edit`、`glob` 与 `grep`;minimal profile 保留按平台选择的持久 shell。历史中的未知工具事件可以使用现有客户端 generic fallback,但项目不再宣传或测试已移除的 contract。 + +本决策取代[原始持久工具决策](../feature/2026-07-29-persistent-bash-str-replace-editor.zh.md)中 editor 的部分、[独立包例外](2026-08-10-default-presets-single-editor.zh.md)以及[极简 profile 范围边界](2026-09-03-minimal-profiles-persistent-shell-only.zh.md)。原始 note 继续负责持久 Bash 行为;极简 profile note 继续负责单 shell contract。 + +## 考虑过的替代方案 + +**为显式自定义组合保留该包。** 不予采用,因为目前没有随附消费方,而 opt-in 包仍会扩大受支持与发布的表面。 + +**保留注册并从模型请求中过滤。** 不予采用,因为展示设置可能恢复该能力,而且每个 runtime 都会继续携带闲置代码。 + +**在 `read`/`write`/`edit` 上提供 alias 或兼容 shim。** 不予采用,因为项目仍处于发布前阶段,shim 会保留本次改动要删除的 schema 与维护成本。 + +## 后果 + +点名已删除包的用户自定义配置会明确失败,必须迁移到 `read`、`write`、`edit` 或 shell。这是 v41 发布前适配中接受的兼容性破坏。 + +只有新决策给出不同且当前存在的消费方、明确的组合 owner、包与 runtime 闭包、面向模型的文档和测试后,已移除接口才能重新引入。在此之前,源码、发布 manifest、生成目录与当前快照均不包含 `str_replace_editor` 工具。 diff --git a/apps/cli/composition.md b/apps/cli/composition.md index f0f2dadeab..406edb4557 100644 --- a/apps/cli/composition.md +++ b/apps/cli/composition.md @@ -156,8 +156,6 @@ flowchart LR cfg --> plugin_dsh_base_tool_goal plugin_dsh_base_tool_ralph["tool-ralph
@deepseek-ai/dsh-tool-ralph"] cfg --> plugin_dsh_base_tool_ralph - plugin_dsh_base_tool_str_replace_editor["tool-str-replace-editor
@deepseek-ai/dsh-tool-str-replace-editor"] - cfg --> plugin_dsh_base_tool_str_replace_editor plugin_dsh_base_repeat_tool_reminder["repeat-tool-reminder
@deepseek-ai/dsh-repeat-tool-reminder"] cfg --> plugin_dsh_base_repeat_tool_reminder plugin_dsh_base_web["web
@deepseek-ai/dsh-web"] @@ -256,7 +254,6 @@ flowchart LR | `tool-todo` | `@deepseek-ai/dsh-tool-todo` | | `tool-goal` | `@deepseek-ai/dsh-tool-goal` | | `tool-ralph` | `@deepseek-ai/dsh-tool-ralph` | -| `tool-str-replace-editor` | `@deepseek-ai/dsh-tool-str-replace-editor` | | `repeat-tool-reminder` | `@deepseek-ai/dsh-repeat-tool-reminder` | | `web` | `@deepseek-ai/dsh-web` | | `web-search-deepseek` | `@deepseek-ai/dsh-web-search-deepseek` | diff --git a/apps/cli/package.json b/apps/cli/package.json index 7a389053d4..4f88ba712f 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -84,7 +84,6 @@ "@deepseek-ai/dsh-tool-pwsh-persistent": "workspace:^", "@deepseek-ai/dsh-tool-ralph": "workspace:^", "@deepseek-ai/dsh-tool-skill": "workspace:^", - "@deepseek-ai/dsh-tool-str-replace-editor": "workspace:^", "@deepseek-ai/dsh-tool-subagent": "workspace:^", "@deepseek-ai/dsh-tool-subagent-control": "workspace:^", "@deepseek-ai/dsh-tool-todo": "workspace:^", diff --git a/apps/cli/tests/web-agent-presets.e2e.ts b/apps/cli/tests/web-agent-presets.e2e.ts index 482412b93a..cfd7abd500 100644 --- a/apps/cli/tests/web-agent-presets.e2e.ts +++ b/apps/cli/tests/web-agent-presets.e2e.ts @@ -335,7 +335,6 @@ describe('the shipped Web composition', () => { ])) // And it keeps the standard agent's own tools rather than replacing them. expect(tools).toEqual(expect.arrayContaining(['bash', 'read', 'edit', 'skill'])) - expect(tools).not.toContain('str_replace_editor') expect(ctx.commands.find(handle.agent, 'goal')).toBeDefined() // The preset's own authoring skill registers into ITS layer of the host @@ -363,10 +362,8 @@ describe('the shipped Web composition', () => { // the capabilities — so the assembly is what carries the claim. const assembly = await ctx.systemPrompt.assemble({ scope: coded.agent }) expect(assembly.tools.map(tool => tool.name)).toEqual(['run_code']) - expect(toolNames(ctx, coded.agent)).not.toContain('str_replace_editor') expect(ctx.commands.find(coded.agent, 'goal')).toBeDefined() const sdk = assembly.sections.find(section => section.name === 'tools:sdk')?.text ?? '' - expect(sdk).not.toContain('str_replace_editor') expect(sdk).toContain('web_search') // The presentation is this agent's alone: the deployment default is diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 55c4409121..fdbe59332d 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-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/config-catalog.md -config-catalog.md: 14aedbe0c0de0d0bc6779d3993c8a03135dc93e3 -config-catalog.zh.md: 4388de1391a273e1e088f1590d0ea1da98258927 +config-catalog.md: a7b98aac4695b5c1337cfa8e9d6266239a92f68f +config-catalog.zh.md: 82d6a5eeb76fbbfa35b01e41ea7af2b159a9c550 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 14aedbe0c0..a7b98aac46 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -2871,24 +2871,6 @@ export interface Config { Source: [`packages/skill/tool-skill/src/index.ts:61`](../packages/skill/tool-skill/src/index.ts) - - -## `@deepseek-ai/dsh-tool-str-replace-editor` - -Requires: `tools` · `fs` - -```ts config-catalog -/** Configuration for the string-replacement editor tool. */ -export interface Config { - /** Maximum returned view characters before clipping (default 16000). */ - maxOutputChars?: number - /** Model-facing tool description. */ - description?: string -} -``` - -Source: [`packages/fs/tool-str-replace-editor/src/index.ts:505`](../packages/fs/tool-str-replace-editor/src/index.ts) - ## `@deepseek-ai/dsh-tool-subagent` diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 4388de1391..82d6a5eeb7 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -2873,24 +2873,6 @@ export interface Config { 来源:[`packages/skill/tool-skill/src/index.ts:61`](../packages/skill/tool-skill/src/index.ts) - - -## `@deepseek-ai/dsh-tool-str-replace-editor` - -需要:`tools` · `fs` - -```ts config-catalog -/** Configuration for the string-replacement editor tool. */ -export interface Config { - /** Maximum returned view characters before clipping (default 16000). */ - maxOutputChars?: number - /** Model-facing tool description. */ - description?: string -} -``` - -来源:[`packages/fs/tool-str-replace-editor/src/index.ts:505`](../packages/fs/tool-str-replace-editor/src/index.ts) - ## `@deepseek-ai/dsh-tool-subagent` diff --git a/docs/event-producer-consumer.i18n.yaml b/docs/event-producer-consumer.i18n.yaml index 571af289d6..6b307d229d 100644 --- a/docs/event-producer-consumer.i18n.yaml +++ b/docs/event-producer-consumer.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/event-producer-consumer.md -event-producer-consumer.md: d04a37ee71756619b36de192011a0053e5ceccb8 -event-producer-consumer.zh.md: 226c368517d989cba2eeac515ee2c544151e8005 +event-producer-consumer.md: 02c3c268298d04cc372ef8343c6f5a071b86320b +event-producer-consumer.zh.md: a0de0add0d6777b3f830fe214d48ca3580d6aeae diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index d04a37ee71..02c3c26829 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -38,9 +38,9 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:102`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) | | `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:90`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`credentials`](../packages/credentials/credentials), `remotes` | | `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace), `workspace-controller` | -| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:66`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | -| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem) | -| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:58`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | +| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:66`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | +| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem) | +| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:58`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | | `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) | | `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` | | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:67`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index 226c368517..a0de0add0d 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -40,9 +40,9 @@ | `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:96`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) | | `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:84`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`credentials`](../packages/credentials/credentials), `remotes` | | `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace), `workspace-controller` | -| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:66`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | -| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem) | -| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:58`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | +| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:66`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | +| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem) | +| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:58`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | | `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) | | `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` | | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:67`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) | diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index 303dd0f885..11eee3a26b 100644 --- a/docs/module-graph.i18n.yaml +++ b/docs/module-graph.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/module-graph.md -module-graph.md: e6ca3e11a28b70310ecbd3118e1c736227626abf -module-graph.zh.md: 9025dc6cce64d89cceaded2e92e3e5b465f79c7a +module-graph.md: 1ad7b4348e6f79865c34d395d01ffb30367e8191 +module-graph.zh.md: d9e1bb8f9eab7a8ac8fdcaad3b2a292743b4cb03 diff --git a/docs/module-graph.md b/docs/module-graph.md index e6ca3e11a2..1ad7b4348e 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -54,7 +54,6 @@ flowchart TD pkg_fs_sandbox["fs-sandbox"] pkg_tool_fs["tool-fs"] pkg_tool_fs_search["tool-fs-search"] - pkg_tool_str_replace_editor["tool-str-replace-editor"] end subgraph group_skill["packages/skill"] pkg_skill["skill"] @@ -721,10 +720,6 @@ flowchart TD pkg_tool_fs_search --> pkg_system_prompt pkg_tool_fs_search --> pkg_timeout pkg_tool_fs_search --> pkg_tools - pkg_tool_str_replace_editor --> pkg_fs - pkg_tool_str_replace_editor --> pkg_sandbox - pkg_tool_str_replace_editor --> pkg_sandbox_policy - pkg_tool_str_replace_editor --> pkg_tools pkg_tool_skill --> pkg_agent pkg_tool_skill --> pkg_llm pkg_tool_skill --> pkg_skill @@ -1327,7 +1322,6 @@ flowchart TD | [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | -| [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) | | [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) | | [`tool-web`](../packages/web/tool-web) | `web` | [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) | | [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`tools`](../packages/core/tools) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index 9025dc6cce..d9e1bb8f9e 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -56,7 +56,6 @@ flowchart TD pkg_fs_sandbox["fs-sandbox"] pkg_tool_fs["tool-fs"] pkg_tool_fs_search["tool-fs-search"] - pkg_tool_str_replace_editor["tool-str-replace-editor"] end subgraph group_skill["packages/skill"] pkg_skill["skill"] @@ -723,10 +722,6 @@ flowchart TD pkg_tool_fs_search --> pkg_system_prompt pkg_tool_fs_search --> pkg_timeout pkg_tool_fs_search --> pkg_tools - pkg_tool_str_replace_editor --> pkg_fs - pkg_tool_str_replace_editor --> pkg_sandbox - pkg_tool_str_replace_editor --> pkg_sandbox_policy - pkg_tool_str_replace_editor --> pkg_tools pkg_tool_skill --> pkg_agent pkg_tool_skill --> pkg_llm pkg_tool_skill --> pkg_skill @@ -1329,7 +1324,6 @@ flowchart TD | [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | -| [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) | | [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) | | [`tool-web`](../packages/web/tool-web) | `web` | [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) | | [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`tools`](../packages/core/tools) | diff --git a/docs/subsystems/filesystem.i18n.yaml b/docs/subsystems/filesystem.i18n.yaml index 7b592e60fa..1b4c450319 100644 --- a/docs/subsystems/filesystem.i18n.yaml +++ b/docs/subsystems/filesystem.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/filesystem.md -filesystem.md: 06fb92f453f8a09f87aee817ab56ef147be9a91f -filesystem.zh.md: ea5b6f7a0d6f45b0d7960298ea10417c0b6c2dd3 +filesystem.md: 2d64c0abacd12133be0cca275c301f361661d031 +filesystem.zh.md: e563fafc6f4b3bd5b30786c88501f9c813218097 diff --git a/docs/subsystems/filesystem.md b/docs/subsystems/filesystem.md index 06fb92f453..2d64c0abac 100644 --- a/docs/subsystems/filesystem.md +++ b/docs/subsystems/filesystem.md @@ -239,7 +239,7 @@ interface FileReadOutcome { ## Observed-file state (policy plugin) -Observed state is a `WeakMap>` held inside the `dsh-fs-observation-policy` plugin. Missing map entry means unseen; `{ kind: 'absent' }` means a `read` or `str_replace_editor` `view`, `str_replace`, or `insert` metadata miss confirmed absence; `{ kind: 'present', version }` means a read, write, or edit observed that version. The write decision maps unseen and absent to `createIfAbsent`, while present maps to `replaceIfVersion`; the edit decision maps unseen to `FS_NOT_OBSERVED`, absent to `FS_NOT_FOUND`, and present to its version guard. The owner is derived from the event actor (normally `exec.agent.session`), treated as opaque and never read. Disposal drops everything (HMR safety), and the policy performs no filesystem I/O. +Observed state is a `WeakMap>` held inside the `dsh-fs-observation-policy` plugin. Missing map entry means unseen; `{ kind: 'absent' }` means a `read` metadata miss confirmed absence; `{ kind: 'present', version }` means a read, write, or edit observed that version. The write decision maps unseen and absent to `createIfAbsent`, while present maps to `replaceIfVersion`; the edit decision maps unseen to `FS_NOT_OBSERVED`, absent to `FS_NOT_FOUND`, and present to its version guard. The owner is derived from the event actor (normally `exec.agent.session`), treated as opaque and never read. Disposal drops everything (HMR safety), and the policy performs no filesystem I/O. ## Error taxonomy (provider contract) diff --git a/docs/subsystems/filesystem.zh.md b/docs/subsystems/filesystem.zh.md index ea5b6f7a0d..e563fafc6f 100644 --- a/docs/subsystems/filesystem.zh.md +++ b/docs/subsystems/filesystem.zh.md @@ -239,7 +239,7 @@ interface FileReadOutcome { ## 已观测文件状态(策略插件) -已观测状态是 `dsh-fs-observation-policy` 插件内部持有的 `WeakMap>`。映射中没有条目表示未见;`{ kind: 'absent' }` 表示 `read` 的元数据未命中,或 `str_replace_editor` 的 `view`、`str_replace`、`insert` 命令发生元数据未命中,从而确认缺失;`{ kind: 'present', version }` 表示 read、write 或 edit 观测到该版本。写入决策把未见和缺失映射到 `createIfAbsent`,把存在映射到 `replaceIfVersion`;编辑决策把未见映射到 `FS_NOT_OBSERVED`,把缺失映射到 `FS_NOT_FOUND`,把存在映射到其版本守卫。所有者从事件 actor 推导(通常是 `exec.agent.session`),被视为不透明且从不读取。dispose(资源释放)时丢弃全部数据(HMR(热模块替换)安全),策略不执行任何文件系统 I/O。 +已观测状态是 `dsh-fs-observation-policy` 插件内部持有的 `WeakMap>`。映射中没有条目表示未见;`{ kind: 'absent' }` 表示 `read` 的元数据未命中确认了缺失;`{ kind: 'present', version }` 表示 read、write 或 edit 观测到该版本。写入决策把未见和缺失映射到 `createIfAbsent`,把存在映射到 `replaceIfVersion`;编辑决策把未见映射到 `FS_NOT_OBSERVED`,把缺失映射到 `FS_NOT_FOUND`,把存在映射到其版本守卫。所有者从事件 actor 推导(通常是 `exec.agent.session`),被视为不透明且从不读取。dispose(资源释放)时丢弃全部数据(HMR(热模块替换)安全),策略不执行任何文件系统 I/O。 ## 错误分类体系(提供方约定) diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml index a70f3d6e35..9b4b0aa4e4 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: 416585106de28296a3105fafbbe6e4edc0d0f16b -tool-catalog.zh.md: 89d2e467863c09b78805fd9db8ec785ad7936a9d +tool-catalog.md: 5242b761b860daf5259c49281170cbaf24d3f86c +tool-catalog.zh.md: d1b4d67ce7bcb2290de763cd9957a8ba7d122e20 diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index 416585106d..5242b761b8 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -23,7 +23,6 @@ This table connects model-visible tool names to the plugin package and service s | `@deepseek-ai/dsh-tool-cordis` | `cordis_define`, `cordis_inspect_list`, `cordis_inspect_query`, `cordis_inspect_self`, `cordis_run`, `cordis_stop`, `cordis_undefine` | `ctx.tools`, `ctx.dynamicCordisRunner` | `tool/call`, `tool/result`, `process-local dynamic package lifecycle` | - | Not in any shipped tree (a deliberate opt-in — dynamic package code reaches the real runtime, see .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). The toolset injects `ctx.dynamicCordisRunner` from `@deepseek-ai/dsh-cordis-host-runner`, which owns the definition registry and the vm sandbox; a composition missing it never activates the tools. A running package may register ADDITIONAL model-visible tools until it is stopped, undefined, or DSH restarts; a full changed request header logs those tool-set changes. | | `@deepseek-ai/dsh-tool-bash-persistent` | `bash` | `ctx.tools`, `ctx.terminals`, `an owning Agent at execution time` | `tool/call`, `PTY shell state`, `tool/result` | - | One owner-isolated persistent bash tool; deployment composition supplies the PTY backend and may override the model-facing environment description. | | `@deepseek-ai/dsh-tool-pwsh-persistent` | `pwsh` | `ctx.tools`, `ctx.terminals`, `an owning Agent at execution time` | `tool/call`, `PTY shell state`, `tool/result` | - | One owner-isolated persistent pwsh tool, the Windows counterpart of the persistent bash tool; deployment composition supplies a pwsh-dialect PTY backend and may override the model-facing environment description. | -| `@deepseek-ai/dsh-tool-str-replace-editor` | `str_replace_editor` | `ctx.tools`, `ctx.fs` | `tool/call`, `fs/observed after view presence/absence, edit absence, or successful mutation`, `tool/result` | - | Standalone view/create/unique literal replace/line insert tool over the filesystem seam; it composes with any shell or terminal API. | | `@deepseek-ai/dsh-tool-fs` | `edit`, `read`, `read_image`, `write` | `ctx.tools`, `ctx.fs`, `ctx.systemPrompt`, `ctx.attachments (image-tool registration)`, `ctx.llm + an image-capable route (image-tool execution)` | `tool/call`, `fs/write-intent or fs/edit-intent for mutations`, `fs/observed after read presence/absence or successful file operation`, `durable attachment (read_image)`, `tool/result` | - | The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-observation-policy` (an `fs/*` event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. The image tool is not registered without `ctx.attachments`; its schema is route-independent, and execution refuses unless the exact routed model declares image input. | | `@deepseek-ai/dsh-tool-fs-search` | `glob`, `grep` | `ctx.tools`, `ctx.subprocess`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | glob and grep are unconditional discovery tools that spawn the packaged ripgrep binary (`@vscode/ripgrep`) through ctx.subprocess as ordinary foreground calls (never background jobs) — no host `rg` install and no shell layer. The catalog uses `sampleOverCapGlobResults: true`; deployments must choose that behavior explicitly. Capped results save the complete formatted list through the optional ctx.spillStore backend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments. | | `@deepseek-ai/dsh-tool-terminal` | `terminal_close`, `terminal_list`, `terminal_open`, `terminal_read`, `terminal_send`, `terminal_signal` | `ctx.tools`, `ctx.terminals`, `ctx.systemPrompt`, `ctx.jobs at call time for run_in_background` | `tool/call`, `tool/result` | - | The six terminal tools are opt-in and complement one-shot shell/filesystem tools. `terminal_send(run_in_background: true)` registers with `ctx.jobs`; TUI, named key sequences, BEL, resize, auto-start, and cross-agent sharing are absent from the schema. | @@ -554,112 +553,6 @@ Source: [`packages/shell/tool-pwsh-persistent/src/index.ts`](../packages/shell/t One owner-isolated persistent pwsh tool, the Windows counterpart of the persistent bash tool; deployment composition supplies a pwsh-dialect PTY backend and may override the model-facing environment description. - - -## `@deepseek-ai/dsh-tool-str-replace-editor` - -### `str_replace_editor` - -Custom editing tool for viewing, creating and editing files -* State is persistent across command calls and discussions with the user -* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep -* The `create` command cannot be used if the specified `path` already exists as a file -* If a `command` generates a long output, it will be truncated and marked with `` -* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match - -Notes for using the `str_replace` command: -* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces! -* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique -* The `new_str` parameter should contain the edited lines that should replace the `old_str` - -```json -{ - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] -} -``` - -Source: [`packages/fs/tool-str-replace-editor/src/index.ts`](../packages/fs/tool-str-replace-editor/src/index.ts) - -Standalone view/create/unique literal replace/line insert tool over the filesystem seam; it composes with any shell or terminal API. - ## `@deepseek-ai/dsh-tool-fs` diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md index 89d2e46786..d1b4d67ce7 100644 --- a/docs/tool-catalog.zh.md +++ b/docs/tool-catalog.zh.md @@ -27,7 +27,6 @@ | `@deepseek-ai/dsh-tool-cordis` | `cordis_define`、`cordis_inspect_list`、`cordis_inspect_query`、`cordis_inspect_self`、`cordis_run`、`cordis_stop`、`cordis_undefine` | `ctx.tools`、`ctx.dynamicCordisRunner` | `tool/call`、`tool/result`、`process-local dynamic package lifecycle` | - | 不在任何随产品发布的树中,需要显式选择启用;动态 Package 代码可以访问真实运行时,见 .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md。该工具集注入 `@deepseek-ai/dsh-cordis-host-runner` 提供的 `ctx.dynamicCordisRunner`,后者拥有定义注册表和 vm 沙箱;组合缺少它时这些工具不会激活。运行中的 Package 在停止、undefine 或 DSH 重启前可以注册**额外的**模型可见工具;发生这类工具集变化时,系统会记录完整且有变动的请求头。 | | `@deepseek-ai/dsh-tool-bash-persistent` | `bash` | `ctx.tools`、`ctx.terminals`、`an owning Agent at execution time` | `tool/call`、`PTY shell state`、`tool/result` | - | 一个按所有者隔离的持久 bash 工具;部署组合提供 PTY 后端,并可覆盖面向模型的环境描述。 | | `@deepseek-ai/dsh-tool-pwsh-persistent` | `pwsh` | `ctx.tools`、`ctx.terminals`、`an owning Agent at execution time` | `tool/call`、`PTY shell state`、`tool/result` | - | 一个按所有者隔离的持久 pwsh 工具,持久 bash 工具的 Windows 对应物;部署组合提供 pwsh 方言的 PTY 后端,并可覆盖面向模型的环境描述。 | -| `@deepseek-ai/dsh-tool-str-replace-editor` | `str_replace_editor` | `ctx.tools`、`ctx.fs` | `tool/call`、`fs/observed after view presence/absence, edit absence, or successful mutation`、`tool/result` | - | 基于文件系统 seam 的独立查看/创建/唯一字面量替换/按行插入工具;可与任何 shell 或终端接口组合。 | | `@deepseek-ai/dsh-tool-fs` | `edit`、`read`、`read_image`、`write` | `ctx.tools`、`ctx.fs`、`ctx.systemPrompt`、`ctx.attachments (image-tool registration)`、`ctx.llm + an image-capable route (image-tool execution)` | `tool/call`、`fs/write-intent or fs/edit-intent for mutations`、`fs/observed after read presence/absence or successful file operation`、`durable attachment (read_image)`、`tool/result` | - | 先读后写/编辑策略由 `@deepseek-ai/dsh-fs-observation-policy` 添加;它是一个 `fs/*` 事件门禁插件,不会改变 schema。加载这些工具的部署按预期也应加载该插件。没有 `ctx.attachments` 时图片工具不会注册;其 schema 与路由无关,执行时除非确切路由的模型声明图片输入,否则拒绝。 | | `@deepseek-ai/dsh-tool-fs-search` | `glob`、`grep` | `ctx.tools`、`ctx.subprocess`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | glob 和 grep 是无条件可用的发现工具,通过 ctx.subprocess spawn 随包提供的 ripgrep 二进制文件(`@vscode/ripgrep`),并作为普通前台调用运行,绝不作为后台任务;无需在宿主机安装 `rg`,也不经过 shell 层。本目录使用 `sampleOverCapGlobResults: true`;部署必须显式选择该行为。结果超过上限时,会通过可选的 ctx.spillStore 后端保存完整的格式化列表;在共置部署中,如果后端公开本地路径,返回的定位信息可供后续读取/搜索。 | | `@deepseek-ai/dsh-tool-terminal` | `terminal_close`、`terminal_list`、`terminal_open`、`terminal_read`、`terminal_send`、`terminal_signal` | `ctx.tools`、`ctx.terminals`、`ctx.systemPrompt`、`ctx.jobs at call time for run_in_background` | `tool/call`、`tool/result` | - | 这 6 个终端工具需要选择启用,用于补充一次性 bash/文件系统工具。`terminal_send(run_in_background: true)` 会注册到 `ctx.jobs`;schema 不包含 TUI、具名按键序列、BEL、调整尺寸、自动启动和跨 agent 共享。 | @@ -558,114 +557,6 @@ pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费 一个按所有者隔离的持久 pwsh 工具,持久 bash 工具的 Windows 对应物;部署组合提供 pwsh 方言的 PTY 后端,并可覆盖面向模型的环境描述。 - - -## `@deepseek-ai/dsh-tool-str-replace-editor` - -### `str_replace_editor` - -用于查看、创建和编辑文件的自定义编辑工具: - -* 状态会在命令调用以及与用户的讨论之间持久保留 -* 如果 `path` 是文件,`view` 会显示应用 `cat -n` 后的结果。如果 `path` 是目录,`view` 会列出最多向下 2 层的非隐藏文件和目录 -* 如果指定的 `create` 命令目标 `path` 已作为文件存在,则不能使用该命令 -* 如果 `command` 产生较长输出,输出会被截断并标记为 `` -* 当前命令不使用某个参数时,值为 `null` 的占位参数视为未提供。必填参数仍须提供值;删除匹配内容时应省略 `str_replace.new_str`,而不是将其设为 `null` - -使用 `str_replace` 命令时请注意: - -* `old_str` 参数应与原文件中一行或多行连续内容**完全**匹配。请留意空白字符! -* 如果 `old_str` 参数在文件中不唯一,则不会执行替换。请确保在 `old_str` 中包含足够的上下文,使其唯一 -* `new_str` 参数应包含用于替换 `old_str` 的已编辑行 - -```json -{ - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] -} -``` - -来源:[`packages/fs/tool-str-replace-editor/src/index.ts`](../packages/fs/tool-str-replace-editor/src/index.ts) - -基于文件系统 seam 的独立查看/创建/唯一字面量替换/按行插入工具;可与任何 shell 或终端接口组合。 - ## `@deepseek-ai/dsh-tool-fs` diff --git a/packages/bundle/base/cordis.patch.yml b/packages/bundle/base/cordis.patch.yml index 94b5d23204..826e0d3639 100644 --- a/packages/bundle/base/cordis.patch.yml +++ b/packages/bundle/base/cordis.patch.yml @@ -421,11 +421,6 @@ subagentProvider: spawn maxRounds: 64 - - id: tool-str-replace-editor - name: '@deepseek-ai/dsh-tool-str-replace-editor' - config: - maxOutputChars: 16000 - # Consecutive-repeat reminders on the tool chain. - id: repeat-tool-reminder name: '@deepseek-ai/dsh-repeat-tool-reminder' diff --git a/packages/bundle/base/package.json b/packages/bundle/base/package.json index 24ff47b915..bc400c3250 100644 --- a/packages/bundle/base/package.json +++ b/packages/bundle/base/package.json @@ -101,7 +101,6 @@ "@deepseek-ai/dsh-tool-pwsh": "workspace:^", "@deepseek-ai/dsh-tool-ralph": "workspace:^", "@deepseek-ai/dsh-tool-skill": "workspace:^", - "@deepseek-ai/dsh-tool-str-replace-editor": "workspace:^", "@deepseek-ai/dsh-tool-subagent": "workspace:^", "@deepseek-ai/dsh-tool-subagent-control": "workspace:^", "@deepseek-ai/dsh-tool-jobs": "workspace:^", diff --git a/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts b/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts index c98cf87881..bdcdd4bc0a 100644 --- a/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts +++ b/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts @@ -82,7 +82,6 @@ describe('dsh-sdk-minimal bundle', () => { config: { shellDialect: 'pwsh', timeoutMs: 300000 }, }) expect(manifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-fs-local') - expect(manifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-tool-str-replace-editor') expect(Object.keys(manifest.dependencies ?? {}).sort()).toEqual( [...new Set(rows.map(row => row.name).filter((name): name is string => name !== undefined).map(packageName))].sort(), ) diff --git a/packages/bundle/web-app/cordis.patch.yml b/packages/bundle/web-app/cordis.patch.yml index 2c9bd25990..74a6de9d21 100644 --- a/packages/bundle/web-app/cordis.patch.yml +++ b/packages/bundle/web-app/cordis.patch.yml @@ -347,9 +347,6 @@ - id: tool-fs-search disabled: true -- id: tool-str-replace-editor - disabled: true - # The `skill` REGISTRY stays in the host plane. It is host+per-scope layered # (the tools-registry shape): deployment-level providers — repository plugins, # a host skill-filesystem row — register into its global layer, while a preset's diff --git a/packages/client/ui-deliverables/README.i18n.yaml b/packages/client/ui-deliverables/README.i18n.yaml index 3ccef63ed5..016b0db83a 100644 --- a/packages/client/ui-deliverables/README.i18n.yaml +++ b/packages/client/ui-deliverables/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-deliverables/README.md -README.md: b228106848549dcddcde84be07c47392dc92a9db -README.zh.md: 0a9d5207fe3c09b89ac34592f04c7eeaeed9b59a +README.md: dc8f0fb1f923b5df981700ed7b33b8d13432dd2d +README.zh.md: bb207b7e51b0769404711e4c45776b74bd0befd4 diff --git a/packages/client/ui-deliverables/README.md b/packages/client/ui-deliverables/README.md index b228106848..dc8f0fb1f9 100644 --- a/packages/client/ui-deliverables/README.md +++ b/packages/client/ui-deliverables/README.md @@ -43,7 +43,7 @@ The closing prose carries the same vocabulary: an inline-code token resolves by
Implementation internals — click to expand -The Node half registers the static `ui:deliverable-file-references` system-prompt section asking the model to mention primary files from successful creation or modification calls and to write those and any other changed-file references as Markdown inline code. The browser half registers `ProducedFiles` into the chat view's `conversation.chat.turnTail` hole. `deliverablesDefinition` folds each Turn's successful first-party mutation calls into `DeliverablesTurnData` from the validated raw arguments of `write`, `edit`, and mutating `str_replace_editor` commands. Reads, deletes, unsupported tools, malformed calls, and failed results contribute nothing. A new mutation tool needs an explicit Client contribution before it joins the list. The package also provides the `chatFileMentions` service the chat view consults per closing message; composing the plugin out removes both surfaces and leaves the view's empty chain at zero cost. +The Node half registers the static `ui:deliverable-file-references` system-prompt section asking the model to mention primary files from successful creation or modification calls and to write those and any other changed-file references as Markdown inline code. The browser half registers `ProducedFiles` into the chat view's `conversation.chat.turnTail` hole. `deliverablesDefinition` folds each Turn's successful first-party mutation calls into `DeliverablesTurnData` from the validated raw arguments of `write` and `edit`. Reads, unsupported tools, malformed calls, and failed results contribute nothing. A new mutation tool needs an explicit Client contribution before it joins the list. The package also provides the `chatFileMentions` service the chat view consults per closing message; composing the plugin out removes both surfaces and leaves the view's empty chain at zero cost.
diff --git a/packages/client/ui-deliverables/README.zh.md b/packages/client/ui-deliverables/README.zh.md index 0a9d5207fe..bb207b7e51 100644 --- a/packages/client/ui-deliverables/README.zh.md +++ b/packages/client/ui-deliverables/README.zh.md @@ -43,7 +43,7 @@ kind: "package-reference"
实现细节——点击展开 -Node 半部注册静态 `ui:deliverable-file-references` 系统提示词段,要求模型点名成功创建或修改的主要文件,并把这些文件以及正文中提到的其他本轮变更文件写成 Markdown 行内代码。浏览器半部把 `ProducedFiles` 注册进 chat 视图的 `conversation.chat.turnTail` 洞。`deliverablesDefinition` 根据 `write`、`edit` 和有修改作用的 `str_replace_editor` 命令中经过校验的原始参数,把每个轮次成功的第一方修改调用折叠进 `DeliverablesTurnData`。读取、删除、不受支持的工具、格式错误的调用和失败结果不贡献任何条目。新的修改工具必须增加显式 Client contribution 才能加入列表。本包还提供 chat 视图按收尾消息查询的 `chatFileMentions` 服务;把插件组合出去会同时移除两个表面,视图的空链以零成本留下。 +Node 半部注册静态 `ui:deliverable-file-references` 系统提示词段,要求模型点名成功创建或修改的主要文件,并把这些文件以及正文中提到的其他本轮变更文件写成 Markdown 行内代码。浏览器半部把 `ProducedFiles` 注册进 chat 视图的 `conversation.chat.turnTail` 洞。`deliverablesDefinition` 根据 `write` 与 `edit` 中经过校验的原始参数,把每个轮次成功的第一方修改调用折叠进 `DeliverablesTurnData`。读取、不受支持的工具、格式错误的调用和失败结果不贡献任何条目。新的修改工具必须增加显式 Client contribution 才能加入列表。本包还提供 chat 视图按收尾消息查询的 `chatFileMentions` 服务;把插件组合出去会同时移除两个表面,视图的空链以零成本留下。
diff --git a/packages/client/ui-deliverables/src/client/turn-deliverables.ts b/packages/client/ui-deliverables/src/client/turn-deliverables.ts index 90e3248516..ef6e3468aa 100644 --- a/packages/client/ui-deliverables/src/client/turn-deliverables.ts +++ b/packages/client/ui-deliverables/src/client/turn-deliverables.ts @@ -51,8 +51,6 @@ function mutationPath(name: string, argsRaw: string): string | null { return typeof args.content === 'string' ? pathValue(args.file_path) : null case 'edit': return validEditArgs(args) ? pathValue(args.file_path) : null - case 'str_replace_editor': - return editorMutationPath(args) default: return null } @@ -67,31 +65,6 @@ function validEditArgs(args: Readonly>): boolean { && (args.replace_all === undefined || typeof args.replace_all === 'boolean') } -/** Extract a path only from a complete mutating editor command. */ -function editorMutationPath(args: Readonly>): string | null { - const path = pathValue(args.path) - if (path === null) return null - switch (args.command) { - case 'create': - return typeof args.file_text === 'string' ? path : null - case 'str_replace': - return typeof args.old_str === 'string' - && args.old_str.length > 0 - && (args.new_str === undefined || typeof args.new_str === 'string') - ? path - : null - case 'insert': - return typeof args.insert_line === 'number' - && Number.isInteger(args.insert_line) - && args.insert_line >= 0 - && typeof args.new_str === 'string' - ? path - : null - default: - return null - } -} - /** A non-blank path preserves the exact spelling supplied to the tool. */ function pathValue(value: unknown): string | null { return typeof value === 'string' && value.trim().length > 0 ? value : null @@ -105,10 +78,10 @@ function isRecord(value: unknown): value is Record { /** * Files produced by one Turn data value. * - * The source is the arguments of successful `write`, `edit`, and mutating - * `str_replace_editor` calls, not the closing prose: a produced file must be - * listed whether or not the model remembered to name it. Reads, unsupported - * tools, malformed calls, and failed results contribute nothing. Paths keep + * The source is the arguments of successful `write` and `edit` calls, not the + * closing prose: a produced file must be listed whether or not the model + * remembered to name it. Reads, unsupported tools, malformed calls, and failed + * results contribute nothing. Paths keep * first-seen order and appear once, so a file written and then edited in the * same turn is one entry. * diff --git a/packages/client/ui-deliverables/tests/produced-files.client.spec.tsx b/packages/client/ui-deliverables/tests/produced-files.client.spec.tsx index 49a93b4ade..061f76194b 100644 --- a/packages/client/ui-deliverables/tests/produced-files.client.spec.tsx +++ b/packages/client/ui-deliverables/tests/produced-files.client.spec.tsx @@ -202,31 +202,11 @@ describe('produced-file Turn data', () => { replace_all: false, }), result(5, 'edit'), - call(6, 'create', 'str_replace_editor', { - command: 'create', path: 'notes/new.md', file_path: 'wrong-create.txt', file_text: 'new', - }), - result(7, 'create'), - call(8, 'replace', 'str_replace_editor', { - command: 'str_replace', path: 'notes/existing.md', old_str: 'old', new_str: 'new', - }), - result(9, 'replace'), - call(10, 'delete-text', 'str_replace_editor', { - command: 'str_replace', path: 'notes/deleted-text.md', old_str: 'remove me', - }), - result(11, 'delete-text'), - call(12, 'insert', 'str_replace_editor', { - command: 'insert', path: 'notes/inserted.md', insert_line: 1, new_str: 'line', - }), - result(13, 'insert'), ]) expect(producedForClosing(deliverablesOf(value))).toEqual([ 'out/index.html', 'out/app.css', - 'notes/new.md', - 'notes/existing.md', - 'notes/deleted-text.md', - 'notes/inserted.md', ]) }) @@ -253,42 +233,6 @@ describe('produced-file Turn data', () => { caseName: 'edit has a non-boolean replace_all', name: 'edit', args: { file_path: 'edit.txt', old_string: 'old', new_string: 'new', replace_all: 'yes' }, }, - { - caseName: 'editor create omits file_text', name: 'str_replace_editor', - args: { command: 'create', path: 'create.txt' }, - }, - { - caseName: 'editor create has non-string file_text', name: 'str_replace_editor', - args: { command: 'create', path: 'create.txt', file_text: 1 }, - }, - { - caseName: 'editor replace omits old_str', name: 'str_replace_editor', - args: { command: 'str_replace', path: 'replace.txt', new_str: 'new' }, - }, - { - caseName: 'editor replace has an empty old_str', name: 'str_replace_editor', - args: { command: 'str_replace', path: 'replace.txt', old_str: '' }, - }, - { - caseName: 'editor replace has non-string new_str', name: 'str_replace_editor', - args: { command: 'str_replace', path: 'replace.txt', old_str: 'old', new_str: 1 }, - }, - { - caseName: 'editor insert omits insert_line', name: 'str_replace_editor', - args: { command: 'insert', path: 'insert.txt', new_str: 'new' }, - }, - { - caseName: 'editor insert has a fractional insert_line', name: 'str_replace_editor', - args: { command: 'insert', path: 'insert.txt', insert_line: 1.5, new_str: 'new' }, - }, - { - caseName: 'editor insert has a negative insert_line', name: 'str_replace_editor', - args: { command: 'insert', path: 'insert.txt', insert_line: -1, new_str: 'new' }, - }, - { - caseName: 'editor insert omits new_str', name: 'str_replace_editor', - args: { command: 'insert', path: 'insert.txt', insert_line: 1 }, - }, ])('ignores a successful result when $caseName', ({ name, args }) => { const value = assembler([ at(1, 'turn/start', { turn: 1 }), @@ -299,12 +243,9 @@ describe('produced-file Turn data', () => { expect(producedForClosing(deliverablesOf(value))).toEqual([]) }) - it('ignores editor views, unsupported tools, failures, interruptions, malformed calls, and orphan results', () => { - const replacement = result(25, 'replacement') + it('ignores unsupported tools, failures, interruptions, malformed calls, and orphan results', () => { const value = assembler([ at(1, 'turn/start', { turn: 1 }), - call(2, 'view', 'str_replace_editor', { command: 'view', path: 'viewed.txt' }), - result(3, 'view'), call(4, 'read', 'read', { file_path: 'input.txt' }), result(5, 'read'), call(6, 'unknown', 'custom_edit', { file_path: 'custom.txt', path: 'custom.txt' }), @@ -326,19 +267,7 @@ describe('produced-file Turn data', () => { file_path: ' ', old_string: 'old', new_string: 'new', }), result(20, 'blank-path'), - call(21, 'missing-editor-path', 'str_replace_editor', { command: 'create', file_text: 'x' }), - result(22, 'missing-editor-path'), result(23, 'orphan'), - call(24, 'replacement', 'str_replace_editor', { - command: 'insert', path: 'replaced.txt', insert_line: 0, new_str: 'new', - }), - { - ...replacement, - event: { - ...replacement.event, - surfaceOp: { op: 'replace', start: 1, end: 1 }, - } as SessionEvent, - }, at(26, 'turn/end', { turn: 1, reason: { kind: 'interrupted' } }), ]) diff --git a/packages/client/ui-tool/README.i18n.yaml b/packages/client/ui-tool/README.i18n.yaml index 4b0b8c7bbc..dff7d298b2 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: 2fe410425935aed46ab95844c81568037ac929e9 -README.zh.md: 83440881fe16582a75fd8dd762577dc7b4d5335b +README.md: e5a6c85e21b9383b228ae439912cd991b043febb +README.zh.md: 1bbc43e3c4a51dab111328791300f2dc5fb8fae4 diff --git a/packages/client/ui-tool/README.md b/packages/client/ui-tool/README.md index 2fe4104259..e5a6c85e21 100644 --- a/packages/client/ui-tool/README.md +++ b/packages/client/ui-tool/README.md @@ -43,7 +43,7 @@ The owner payload is `ToolCallOwnerProps`: `callId`, `toolName`, the frozen `blo ### Built-in views -This package owns the generic fallback and the built-in shell/pwsh, read, read_image, write/edit, running `str_replace_editor` `create`/`str_replace`, grep/glob, web, todo, question, and Code Dispatch presentations. Structured cards derive directly from first-party raw event fields; Host `presentCall` and `presentResult` values never enter the Client. Foreground one-shot shell results use terminal cards. Settled persistent-shell results use the expandable generic input/output card because reset and partial-output diagnostics do not always describe one process exit status; background acknowledgements remain collapsed. A successful question row pairs call questions with result answers by their stable ids and shows readable question/answer lines when expanded. A cancelled or interrupted row shows its verdict and original questions without inventing answers. Unsupported, malformed, or ambiguous inputs fall back to flattened Tool input/result text. `ui-skill` demonstrates a business-owned registration for `skill`. +This package owns the generic fallback and the built-in shell/pwsh, read, read_image, write/edit, grep/glob, web, todo, question, and Code Dispatch presentations. Structured cards derive directly from first-party raw event fields; Host `presentCall` and `presentResult` values never enter the Client. Foreground one-shot shell results use terminal cards. Settled persistent-shell results use the expandable generic input/output card because reset and partial-output diagnostics do not always describe one process exit status; background acknowledgements remain collapsed. A successful question row pairs call questions with result answers by their stable ids and shows readable question/answer lines when expanded. A cancelled or interrupted row shows its verdict and original questions without inventing answers. Unsupported, malformed, or ambiguous inputs fall back to flattened Tool input/result text. `ui-skill` demonstrates a business-owned registration for `skill`. ----- diff --git a/packages/client/ui-tool/README.zh.md b/packages/client/ui-tool/README.zh.md index 83440881fe..1bbc43e3c4 100644 --- a/packages/client/ui-tool/README.zh.md +++ b/packages/client/ui-tool/README.zh.md @@ -43,7 +43,7 @@ owner 载荷为 `ToolCallOwnerProps`:`callId`、`toolName`、冻结的 `block` ### 内置视图 -本包拥有 generic fallback,以及 shell/pwsh、read、read_image、write/edit、running `str_replace_editor` `create`/`str_replace`、grep/glob、web、todo、question 与 Code Dispatch 的内置展示。结构化卡片直接从第一方原始 event 字段派生;Host `presentCall` 与 `presentResult` 值不会进入 Client。前台一次性 shell 结果使用 terminal 卡片。已完成的持久 shell 结果使用可展开的 generic 输入/输出卡片,因为 reset 与部分输出诊断不一定描述单个进程的退出状态;后台启动回执保持折叠。成功的问题行按稳定 id 配对调用中的问题与结果中的回答,展开后显示可读的问答行。已取消或已中断的问题行显示其裁决与原始问题,不虚构回答。不受支持、格式错误或含糊的输入回退为压平的工具输入/结果文本。`ui-skill` 展示了业务包自行拥有的 `skill` 注册项。 +本包拥有 generic fallback,以及 shell/pwsh、read、read_image、write/edit、grep/glob、web、todo、question 与 Code Dispatch 的内置展示。结构化卡片直接从第一方原始 event 字段派生;Host `presentCall` 与 `presentResult` 值不会进入 Client。前台一次性 shell 结果使用 terminal 卡片。已完成的持久 shell 结果使用可展开的 generic 输入/输出卡片,因为 reset 与部分输出诊断不一定描述单个进程的退出状态;后台启动回执保持折叠。成功的问题行按稳定 id 配对调用中的问题与结果中的回答,展开后显示可读的问答行。已取消或已中断的问题行显示其裁决与原始问题,不虚构回答。不受支持、格式错误或含糊的输入回退为压平的工具输入/结果文本。`ui-skill` 展示了业务包自行拥有的 `skill` 注册项。 ----- diff --git a/packages/client/ui-tool/src/client/tool/models/diff-card-model.ts b/packages/client/ui-tool/src/client/tool/models/diff-card-model.ts index 03eaf04a3a..439cdbbc1c 100644 --- a/packages/client/ui-tool/src/client/tool/models/diff-card-model.ts +++ b/packages/client/ui-tool/src/client/tool/models/diff-card-model.ts @@ -47,31 +47,11 @@ function narrowDiffs(diffs: unknown): DiffHunk[] | null { return out } -type IntendedDiff = { tool: 'write' | 'edit' | 'str_replace_editor'; diff: DiffHunk } +type IntendedDiff = { tool: 'write' | 'edit'; diff: DiffHunk } function intendedDiff(block: ToolCallBlock): IntendedDiff | null { const parsed = parsedToolCall(block) if (parsed === null) return null - if (parsed.name === 'str_replace_editor') { - const { command, path, file_text: fileText, old_str: oldText, new_str: newText } = parsed.args - if (typeof path !== 'string' || path.trim() === '') return null - if (command === 'create') { - if (fileText !== undefined && typeof fileText !== 'string') return null - return { - tool: 'str_replace_editor', - diff: { path, oldText: null, newText: fileText ?? '' }, - } - } - if (command === 'str_replace') { - if (oldText !== undefined && typeof oldText !== 'string') return null - if (newText !== undefined && typeof newText !== 'string') return null - return { - tool: 'str_replace_editor', - diff: { path, oldText: oldText ?? null, newText: newText ?? '' }, - } - } - return null - } const { file_path: path } = parsed.args if (typeof path !== 'string' || path.trim() === '') return null if (!validEscalationFields(parsed.args)) return null @@ -97,11 +77,9 @@ function appliedDiffs(meta: unknown): DiffHunk[] | 'empty' | null { } /** - * Derive running diffs for root write/edit and `str_replace_editor` - * create/replace calls, plus applied settled diffs for root write/edit calls. + * Derive running diffs and applied settled diffs for root write/edit calls. * A successful write with valid empty metadata uses its argument-derived - * whole-file diff, matching create and identical-overwrite presentation; - * `str_replace_editor` settles through Generic because it has no result view. + * whole-file diff, matching create and identical-overwrite presentation. * @param block - running or settled Tool block. * @returns the diff-card props, or null for the generic path. */ @@ -110,7 +88,6 @@ export function diffCardModel(block: ToolCallBlock): DiffCardModel | null { const intended = intendedDiff(block) if (intended === null) return null if (!('kind' in block)) return { card: { diffs: [intended.diff] } } - if (intended.tool === 'str_replace_editor') return null if (block.isError) return null const applied = appliedDiffs(block.meta) if (applied === null || applied === 'empty') { diff --git a/packages/client/ui-tool/tests/diff-card.client.spec.tsx b/packages/client/ui-tool/tests/diff-card.client.spec.tsx index ed386b8321..8388e5dcf4 100644 --- a/packages/client/ui-tool/tests/diff-card.client.spec.tsx +++ b/packages/client/ui-tool/tests/diff-card.client.spec.tsx @@ -63,47 +63,6 @@ describe('diffCardModel', () => { }) }) - it.each([ - { - command: 'create', - args: { command: 'create', path: 'notes/new.txt', file_text: 'new file\n' }, - diff: { path: 'notes/new.txt', oldText: null, newText: 'new file\n' }, - }, - { - command: 'str_replace', - args: { command: 'str_replace', path: 'notes/demo.txt', old_str: 'old', new_str: 'new' }, - diff: { path: 'notes/demo.txt', oldText: 'old', newText: 'new' }, - }, - ])('preserves the running str_replace_editor $command diff', ({ args, diff }) => { - expect(diffCardModel(running({ - name: 'str_replace_editor', - argsRaw: JSON.stringify(args), - }))).toEqual({ card: { diffs: [diff] } }) - }) - - it('preserves str_replace_editor defaults and its settled Generic result', () => { - const argsRaw = JSON.stringify({ command: 'str_replace', path: 'notes/demo.txt' }) - expect(diffCardModel(running({ name: 'str_replace_editor', argsRaw }))).toEqual({ - card: { diffs: [{ path: 'notes/demo.txt', oldText: null, newText: '' }] }, - }) - expect(diffCardModel(settled({ - call: { name: 'str_replace_editor', argsRaw }, - meta: { diffs: [{ path: 'notes/demo.txt', oldText: 'old', newText: 'new' }] }, - }))).toBeNull() - }) - - it('keeps unsupported or malformed str_replace_editor calls generic', () => { - const editor = (args: Record) => running({ - name: 'str_replace_editor', argsRaw: JSON.stringify(args), - }) - expect(diffCardModel(editor({ command: 'view', path: 'notes/demo.txt' }))).toBeNull() - expect(diffCardModel(editor({ command: 'insert', path: 'notes/demo.txt', new_str: 'x' }))).toBeNull() - expect(diffCardModel(editor({ command: 'create', path: '', file_text: 'x' }))).toBeNull() - expect(diffCardModel(editor({ command: 'create', path: 'notes/demo.txt', file_text: 1 }))).toBeNull() - expect(diffCardModel(editor({ command: 'str_replace', path: 'notes/demo.txt', old_str: 1 }))).toBeNull() - expect(diffCardModel(editor({ command: 'str_replace', path: 'notes/demo.txt', new_str: 1 }))).toBeNull() - }) - it('derives a settled card from result metadata, which replaces the intended diff', () => { expect(diffCardModel(settled({ meta: { diffs: [{ path: 'notes/demo.txt', oldText: 'a', newText: 'b' }] }, diff --git a/packages/core/tools/tests/gen-tool-catalog.spec.ts b/packages/core/tools/tests/gen-tool-catalog.spec.ts index 0630cfaa8e..33f970a180 100644 --- a/packages/core/tools/tests/gen-tool-catalog.spec.ts +++ b/packages/core/tools/tests/gen-tool-catalog.spec.ts @@ -34,7 +34,7 @@ describe('gen-tool-catalog collectToolCatalog', () => { 'read', 'read_image', 'run_code', 'schedule_create', 'schedule_delete', 'schedule_list', 'send_message', 'send_message', 'session_event_read', 'session_event_search', 'session_event_trace', 'session_search', 'session_trace', 'skill', 'spawn_teammate', - 'str_replace_editor', 'subagent', 'team_task_create', + 'subagent', 'team_task_create', 'team_task_get', 'team_task_list', 'team_task_update', 'terminal_close', 'terminal_list', 'terminal_open', 'terminal_read', 'terminal_send', 'terminal_signal', 'todo_write', 'update_goal', 'wait_agent', 'web_fetch', 'web_search', 'workflow', 'write', diff --git a/packages/fs/README.i18n.yaml b/packages/fs/README.i18n.yaml index 5311da4c5b..7675ae6897 100644 --- a/packages/fs/README.i18n.yaml +++ b/packages/fs/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/fs/README.md -README.md: c8cfc5f8d5dd5d225149abbf192d23df1b51cf71 -README.zh.md: 9596f72e76241de43072ee590c141b043af6a861 +README.md: 797cdf1697a088958f56c9e4ec2e7de0bd854835 +README.zh.md: 900619ca1e53f2561e23dc3e7f1efe559739a4be diff --git a/packages/fs/README.md b/packages/fs/README.md index c8cfc5f8d5..797cdf1697 100644 --- a/packages/fs/README.md +++ b/packages/fs/README.md @@ -33,7 +33,6 @@ Seven packages plus the remote sibling `fs-e2b` play the filesystem roles; the s | [`fs-observation-policy/`](fs-observation-policy/README.md) | Read-before-edit policy: records observed presence or absence and guards write/edit through the `fs/*` events | `fs/*` listeners | | [`tool-fs/`](tool-fs/README.md) | Model-facing `read`, `read_image`, `write`, and `edit` tools plus their executor | registers on `ctx.tools` | | [`tool-fs-search/`](tool-fs-search/README.md) | Model-facing `glob` and `grep` discovery tools backed by the packaged ripgrep binary | registers on `ctx.tools` | -| [`tool-str-replace-editor/`](tool-str-replace-editor/README.md) | Standalone `str_replace_editor` tool: `view`, `create`, `str_replace`, and `insert` over `ctx.fs` | registers on `ctx.tools` | The policy is a plugin, not a service the tools inject: removing it leaves the bare provider's unconditional mutation behavior instead of breaking the tools. The mode fence in `fs-sandbox` and the read-before-edit gate compose. `tool-fs-search` deliberately does not extend the provider contract — search is a process-backed ripgrep workflow, so filesystem backends stay free of a universal search API. diff --git a/packages/fs/README.zh.md b/packages/fs/README.zh.md index 9596f72e76..900619ca1e 100644 --- a/packages/fs/README.zh.md +++ b/packages/fs/README.zh.md @@ -33,7 +33,6 @@ kind: "package-group" | [`fs-observation-policy/`](fs-observation-policy/README.zh.md) | 编辑前读取策略:记录观测到的存在或缺失,并通过 `fs/*` 事件防护写入/编辑 | `fs/*` 监听器 | | [`tool-fs/`](tool-fs/README.zh.md) | 面向模型的 `read`、`read_image`、`write` 与 `edit` 工具及其执行器 | 注册到 `ctx.tools` | | [`tool-fs-search/`](tool-fs-search/README.zh.md) | 由打包 ripgrep 二进制支持的面向模型 `glob` 与 `grep` 发现工具 | 注册到 `ctx.tools` | -| [`tool-str-replace-editor/`](tool-str-replace-editor/README.zh.md) | 独立的 `str_replace_editor` 工具:基于 `ctx.fs` 的 `view`、`create`、`str_replace` 与 `insert` | 注册到 `ctx.tools` | 策略是插件,不是工具注入的服务:移除它只会让工具回到裸提供方的无条件变更行为,而不会破坏工具。`fs-sandbox` 的模式围栏与编辑前读取门禁可以组合。`tool-fs-search` 有意不扩展提供方约定——搜索是由进程支持的 ripgrep 工作流,因此文件系统后端无需承担通用搜索 API。 diff --git a/packages/fs/tool-str-replace-editor/README.i18n.yaml b/packages/fs/tool-str-replace-editor/README.i18n.yaml deleted file mode 100644 index d753b957de..0000000000 --- a/packages/fs/tool-str-replace-editor/README.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# side as of the last confirmed-consistent state. Both languages carry equal authority; -# after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write packages/fs/tool-str-replace-editor/README.md -README.md: 66663b2ab19585c30f00e6e6d91ce759b4fb8d81 -README.zh.md: 60c63b25cb5c5960aaafcf4f91b3ce23e8911cd7 diff --git a/packages/fs/tool-str-replace-editor/README.md b/packages/fs/tool-str-replace-editor/README.md deleted file mode 100644 index 66663b2ab1..0000000000 --- a/packages/fs/tool-str-replace-editor/README.md +++ /dev/null @@ -1,149 +0,0 @@ ---- -description: "The standalone str_replace_editor tool over ctx.fs for users and maintainers composing Claude-Code-style file editing for agents." -kind: "package-reference" ---- - -# @deepseek-ai/dsh-tool-str-replace-editor - -English | [中文](README.zh.md) - -## Summary - -`dsh-tool-str-replace-editor` provides a standalone model-facing `str_replace_editor` tool over `ctx.fs`: `view` shows numbered file content or a shallow directory listing, `create` makes a new file, `str_replace` applies a unique literal replacement, and `insert` adds lines at a chosen boundary. It is composable with persistent Bash, one-shot Bash, sandboxed Bash, or another terminal surface. Mutations obey the same read-before-edit policy and sandbox fence as the rest of the fs family, enforced by whichever backend and policy plugins are mounted. Choose it when a deployment wants the Claude-Code-style single editor tool with absolute paths; the `dsh-tool-fs` package provides the alternative `read`/`write`/`edit` suite. - -## Table of Contents - -- [Use this package](#use-this-package) -- [Understand the implementation](#understand-the-implementation) -- [Further Exploration](#further-exploration) -- [Model Experience](#model-experience) -- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work) -- [Dev Note](#dev-note) - ------ - - -## Use this package - -Mount the tool alongside a `ctx.fs` backend (and, for guarded mutations, the policy plugin) when the model should edit files through the familiar `view`/`create`/`str_replace`/`insert` command vocabulary on absolute paths. - -### Minimal composition - -A backend, optionally the policy plugin, then the tool; the editor composes with any terminal surface. - -```yaml -- name: '@deepseek-ai/dsh-fs-local' -- name: '@deepseek-ai/dsh-fs-observation-policy' -- name: '@deepseek-ai/dsh-tool-str-replace-editor' -``` - -### Configuration - -| Key | Default | Meaning | -|---|---|---| -| `maxOutputChars` | `16000` | Prefix characters retained for file and directory views | -| `description` | `Custom editing tool for viewing, creating and editing files` (multi-line) | Model-facing tool description | - -### The commands - -`view` returns one-based numbered file content (tabs preserved, so displayed text stays valid literal replacement input) or a two-level directory listing that omits hidden, dependency, and Python-cache entries. `create` makes a new file and refuses to overwrite an existing one. Command-specific fields may contain `null` placeholders when the selected command does not use them; required fields stay required, `view_range: null` selects the full view, and `str_replace.new_str: null` is rejected so deletion requires omission. `str_replace` requires one unique literal match, with errors reported in the public `old_str` vocabulary; `insert` follows the selected zero-based insertion boundary without adding an implicit trailing newline. Mutations preserve tabs outside the requested edit. - -### Failures and recovery - -A metadata miss from `view`, `str_replace`, or `insert` records confirmed absence before returning `FS_NOT_FOUND`, so a later `create` can recover an externally deleted path through the mounted policy's guarded-create flow; absence never authorizes `str_replace` or `insert`. Guarded mutations inherit the policy plugin's codes and remedies — `FS_NOT_OBSERVED` (read the file, then retry), `FS_STALE_VERSION` (re-read, then retry) — and sandbox denials surface as the `[sandbox: file access denied under mode]` marker. Paths must be absolute; a relative path is refused with a hint. - ------ - - -## Understand the implementation - -
-Implementation internals — click to expand - -This section explains the design decisions behind the editor tool and points at the code that realizes them; the observable behavior is fully covered in [Use this package](#use-this-package). - -### Design concept - -The tool is one schema with four commands over `ctx.fs`. Mutations never touch the provider directly with their own assumptions: each one runs the `fs/write-intent` or `fs/edit-intent` waterfall to obtain the policy plugin's guard, resolves the per-call sandbox policy when the mounted `ctx.fs` confines, and delegates enforcement to the provider. `str_replace` and `insert` additionally re-read the file and use the observed version as the compare-and-swap basis when no policy plugin supplies a guard. - -### Source map - -| File | Role | -|---|---| -| [`src/index.ts`](src/index.ts) | The whole tool: schema, command dispatch, view rendering, mutation policy | - -### How each command runs - -Every command resolves the absolute path first; mutations then follow one shared flow — policy guard, provider enforcement, then an `fs/observed` record on success — while `view` only stats and renders. The entire tool — schema, command dispatch, and view rendering — lives in `src/index.ts`. - -
- ------ - - -## Further Exploration - -Read these pages when the package-level contract is not enough. They move from the tool to the contract, policy, and backends it composes with. - -- [Filesystem subsystem](../../../docs/subsystems/filesystem.md) — exhaustive provider contract, policy events, and error taxonomy. -- [dsh-fs](../fs/README.md) — the `ctx.fs` contract this tool consumes. -- [tool-fs](../tool-fs/README.md) — the alternative `read`/`write`/`edit` tool suite. -- [fs-observation-policy](../fs-observation-policy/README.md) — the policy plugin that guards mutations through the `fs/*` events. -- [fs-sandbox](../fs-sandbox/README.md) — the sandbox-enforcing backend that fences mutations. -- [Generated tool catalog](../../../docs/tool-catalog.md#deepseek-aidsh-tool-str-replace-editor) — the exhaustive schema this package registers. - ------ - - -## Model Experience - -### Tool schema - -#### What the model sees - -The generated [`str_replace_editor` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-str-replace-editor), including the configured `description`. The plugin contributes no standalone system-prompt section. - -#### Token effect - -Fixed schema cost while `str_replace_editor` is visible. - -#### KV Cache effect - -Prefix-stable while the configured description and schema remain unchanged. - -### Tool results - -#### What the model sees - -Views return numbered text or a shallow directory listing. Calls expose file locations, and create/replace calls expose diff cards to presentation surfaces. Mutations return concise confirmations. Long views keep their prefix and append a clipping notice. - -#### Token effect - -Data-dependent and bounded by `maxOutputChars` plus the fixed clipping notice. - -#### KV Cache effect - -Append-only tool results follow the reusable request prefix. - -## Known Limitations and Deferred Work - - - - -These limits define when the editor tool is a poor fit or needs special operational care. They are current package constraints, not a general editor comparison or a task backlog. - -- **Operations target UTF-8 text** — binary files are unsupported. -- **`str_replace` intentionally rejects zero or multiple matches** — it has no `replace_all` argument. -- **Every mutation goes through the mounted policy and sandbox** — `fs/write-intent` or `fs/edit-intent` resolves the current session sandbox policy and delegates enforcement to the mounted filesystem and policy plugins, so a deployment without them gets unconditional mutations. - - -### Dev Note - -
-Working context for maintainers — click to expand - -None. - -
- -**Runtime invariant:** No companion is published. The tool adapter owns no independent durable state; filesystem mutation relations stay with the provider and policy plugins. diff --git a/packages/fs/tool-str-replace-editor/README.zh.md b/packages/fs/tool-str-replace-editor/README.zh.md deleted file mode 100644 index 60c63b25cb..0000000000 --- a/packages/fs/tool-str-replace-editor/README.zh.md +++ /dev/null @@ -1,149 +0,0 @@ ---- -description: "基于 `ctx.fs` 的独立 str_replace_editor 工具:供组合 Claude Code 风格文件编辑能力的用户与维护者使用。" -kind: "package-reference" ---- - -# @deepseek-ai/dsh-tool-str-replace-editor - -[English](README.md) | 中文 - -## 概述 - -`dsh-tool-str-replace-editor` 提供基于 `ctx.fs` 的独立面向模型 `str_replace_editor` 工具:`view` 显示带行号的文件内容或浅层目录列表,`create` 创建新文件,`str_replace` 应用唯一的字面量替换,`insert` 在选定的边界处插入行。它可以与持久 Bash、一次性 Bash、沙箱 Bash 或其他终端接口组合。修改操作遵守与 fs 家族其余部分相同的编辑前读取策略与沙箱围栏,具体由所挂载的后端与策略插件强制执行。当部署需要 Claude Code 风格、使用绝对路径的单一编辑器工具时选择它;`dsh-tool-fs` 包提供替代的 `read`/`write`/`edit` 套件。 - -## 目录 - -- [使用本包](#use-this-package) -- [理解实现](#understand-the-implementation) -- [进一步探索](#further-exploration) -- [模型体验](#model-experience) -- [已知限制与延期工作](#known-limitations-and-deferred-work) -- [开发备注](#dev-note) - ------ - - -## 使用本包 - -当模型应通过熟悉的 `view`/`create`/`str_replace`/`insert` 命令词汇在绝对路径上编辑文件时,把工具与 `ctx.fs` 后端(以及需要防护变更时的策略插件)一起挂载。 - -### 最小组合 - -一个后端、可选地加策略插件,然后是工具;编辑器可与任何终端接口组合。 - -```yaml -- name: '@deepseek-ai/dsh-fs-local' -- name: '@deepseek-ai/dsh-fs-observation-policy' -- name: '@deepseek-ai/dsh-tool-str-replace-editor' -``` - -### 配置 - -| 键 | 默认值 | 含义 | -|---|---|---| -| `maxOutputChars` | `16000` | 文件和目录查看结果保留的前缀字符数 | -| `description` | `Custom editing tool for viewing, creating and editing files`(多行) | 面向模型的工具描述 | - -### 命令 - -`view` 返回从 1 开始编号的文件内容(保留制表符,因此显示的文本仍可作为有效的字面量替换输入)或忽略隐藏、依赖与 Python 缓存条目的两层目录列表。`create` 创建新文件,并拒绝覆盖现有文件。选定命令不使用的专用字段可以包含 `null` 占位符;必填字段仍保持必填,`view_range: null` 选择完整视图,`str_replace.new_str: null` 会被拒绝,因此删除内容必须省略该字段。`str_replace` 要求字面量唯一匹配,错误只使用公开的 `old_str` 词汇;`insert` 遵循所选的零基插入边界,不会隐式补尾换行。修改操作会保留请求编辑范围之外的制表符。 - -### 失败与恢复 - -`view`、`str_replace` 或 `insert` 发生元数据未命中时,工具会在返回 `FS_NOT_FOUND` 前记录确认缺失,因此后续 `create` 可以通过已挂载策略的防护创建流程恢复外部删除的路径;缺失状态绝不会授权 `str_replace` 或 `insert`。防护变更继承策略插件的错误码与恢复指令——`FS_NOT_OBSERVED`(先读取文件再重试)、`FS_STALE_VERSION`(先重新读取再重试)——沙箱拒绝则表现为 `[sandbox: file access denied under mode]` 标记。路径必须是绝对路径;相对路径会被拒绝并给出提示。 - ------ - - -## 理解实现 - -
-实现细节——点击展开 - -本节解释编辑器工具背后的设计决策,并指出实现它们的代码位置;可观察行为已在[使用本包](#use-this-package)中完整说明。 - -### 设计理念 - -该工具是基于 `ctx.fs` 的单一 schema、四个命令。修改操作绝不带着自己的假设直接触碰提供方:每个操作都运行 `fs/write-intent` 或 `fs/edit-intent` waterfall 以取得策略插件的防护,在已挂载的 `ctx.fs` 实施沙箱限制时解析按调用沙箱策略,并把强制执行委托给提供方。`str_replace` 与 `insert` 还会重新读取文件,并在没有策略插件提供防护时把观察到的版本作为比较并交换的基础。 - -### 源码地图 - -| 文件 | 职责 | -|---|---| -| [`src/index.ts`](src/index.ts) | 整个工具:schema、命令分派、查看渲染、修改策略 | - -### 各命令如何运行 - -每个命令都先解析绝对路径;修改操作随后遵循同一条共享流程——策略防护、提供方强制执行、成功后记录 `fs/observed`——而 `view` 只执行 stat 并渲染。整个工具——schema、命令分派与查看渲染——都位于 `src/index.ts`。 - -
- ------ - - -## 进一步探索 - -当包级约定不够用时阅读以下页面。它们从工具逐步进入它所组合的约定、策略与后端。 - -- [文件系统子系统](../../../docs/subsystems/filesystem.zh.md)——穷尽式提供方约定、策略事件与错误分类体系。 -- [dsh-fs](../fs/README.zh.md)——本工具消费的 `ctx.fs` 约定。 -- [tool-fs](../tool-fs/README.zh.md)——替代的 `read`/`write`/`edit` 工具套件。 -- [fs-observation-policy](../fs-observation-policy/README.zh.md)——通过 `fs/*` 事件防护变更的策略插件。 -- [fs-sandbox](../fs-sandbox/README.zh.md)——围栏变更的沙箱强制后端。 -- [生成工具目录](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-str-replace-editor)——本包注册的穷尽式 schema。 - ------ - - -## 模型体验 - -### 工具 schema - -#### 模型看到的内容 - -生成的 [`str_replace_editor` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-str-replace-editor),包含配置的 `description`。本插件不贡献独立系统提示词段。 - -#### Token 影响 - -`str_replace_editor` 可见时产生固定的 schema 成本。 - -#### KV Cache 影响 - -配置的描述与 schema 不变时前缀稳定。 - -### 工具结果 - -#### 模型看到的内容 - -查看操作返回带行号文本或浅层目录列表。调用会提供文件位置,创建/替换调用还会向展示层提供 diff 卡片。修改操作返回简洁确认。长查看结果保留前缀并追加截断提示。 - -#### Token 影响 - -随数据变化,并受 `maxOutputChars` 与固定截断提示约束。 - -#### KV Cache 影响 - -工具结果以追加方式位于可复用请求前缀之后。 - -## 已知限制与延期工作 - - - - -这些限制说明编辑器工具何时不合适,或何时需要特别的运维注意。它们是当前包约束,不是通用编辑器对比或任务积压。 - -- **操作面向 UTF-8 文本**——不支持二进制文件。 -- **`str_replace` 刻意拒绝零匹配或多匹配**——它没有 `replace_all` 参数。 -- **每个修改操作都会经过已挂载的策略与沙箱**——`fs/write-intent` 或 `fs/edit-intent` 解析当前会话的沙箱策略,并把强制执行委托给已挂载的文件系统与策略插件,因此未挂载它们的部署会得到无条件变更。 - - -### 开发备注 - -
-维护者的工作上下文——点击展开 - -无。 - -
- -**运行时不变式:** 不发布伴生入口。tool adapter 不持有独立持久状态;filesystem mutation 关系属于 provider 与 policy 插件。 diff --git a/packages/fs/tool-str-replace-editor/package.json b/packages/fs/tool-str-replace-editor/package.json deleted file mode 100644 index d989328728..0000000000 --- a/packages/fs/tool-str-replace-editor/package.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "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.2-rc.1", - "publishConfig": { - "access": "public" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", - "directory": "packages/fs/tool-str-replace-editor" - }, - "type": "module", - "main": "lib/index.js", - "types": "lib/types/index.d.ts", - "exports": { - ".": { - "types": "./lib/types/index.d.ts", - "default": "./lib/index.js" - }, - "./package.json": "./package.json" - }, - "files": [ - "lib/index.js", - "lib/types/**/*.d.ts" - ], - "license": "MIT", - "peerDependencies": { - "@deepseek-ai/dsh-fs": "workspace:^", - "@deepseek-ai/dsh-sandbox": "workspace:^", - "@deepseek-ai/dsh-sandbox-policy": "workspace:^", - "@deepseek-ai/dsh-tools": "workspace:^", - "@deepseek-ai/cordis": "workspace:^" - }, - "dependencies": { - "@deepseek-ai/schemastery": "workspace:^" - }, - "devDependencies": { - "@deepseek-ai/dsh-agent": "workspace:^", - "@deepseek-ai/dsh-fs": "workspace:^", - "@deepseek-ai/dsh-fs-local": "workspace:^", - "@deepseek-ai/dsh-fs-observation-policy": "workspace:^", - "@deepseek-ai/dsh-fs-sandbox": "workspace:^", - "@deepseek-ai/dsh-llm": "workspace:^", - "@deepseek-ai/dsh-sandbox": "workspace:^", - "@deepseek-ai/dsh-sandbox-policy": "workspace:^", - "@deepseek-ai/dsh-session": "workspace:^", - "@deepseek-ai/dsh-system-prompt": "workspace:^", - "@deepseek-ai/dsh-tools": "workspace:^", - "@deepseek-ai/cordis": "workspace:^", - "@deepseek-ai/dsh-session-projection": "workspace:^" - } -} diff --git a/packages/fs/tool-str-replace-editor/src/index.ts b/packages/fs/tool-str-replace-editor/src/index.ts deleted file mode 100644 index f77736685e..0000000000 --- a/packages/fs/tool-str-replace-editor/src/index.ts +++ /dev/null @@ -1,531 +0,0 @@ -/** - * Model-facing `str_replace_editor` over the Harness filesystem seam. - * @module @deepseek-ai/dsh-tool-str-replace-editor - */ - -import { isAbsolute } from 'node:path' -import type { Context } from '@deepseek-ai/cordis' -import z from '@deepseek-ai/schemastery' -import { FsError } from '@deepseek-ai/dsh-fs' -import type { FsInfo, FsTarget, FsWriteIntent } from '@deepseek-ai/dsh-fs' -import { sandboxDenialMarker } from '@deepseek-ai/dsh-sandbox' -import type { SandboxExecutionPolicy } from '@deepseek-ai/dsh-sandbox' -import type { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy' -import { defineTool } from '@deepseek-ai/dsh-tools' -import type { ToolCallView, ToolRunContext } from '@deepseek-ai/dsh-tools' - -const TRUNCATED_MESSAGE = 'To save on context only part of this file has been shown to you. You should retry this tool after you have searched inside the file with `grep -n` in order to find the line numbers of what you are looking for.' - -const DEFAULT_DESCRIPTION = ` -Custom editing tool for viewing, creating and editing files -* State is persistent across command calls and discussions with the user -* If \`path\` is a file, \`view\` displays the result of applying \`cat -n\`. If \`path\` is a directory, \`view\` lists non-hidden files and directories up to 2 levels deep -* The \`create\` command cannot be used if the specified \`path\` already exists as a file -* If a \`command\` generates a long output, it will be truncated and marked with \`\` -* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit \`str_replace.new_str\` rather than setting it to null when deleting a match - -Notes for using the \`str_replace\` command: -* The \`old_str\` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces! -* If the \`old_str\` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in \`old_str\` to make it unique -* The \`new_str\` parameter should contain the edited lines that should replace the \`old_str\` -`.trim() - -function maybeTruncate(content: string, maxOutputChars: number): string { - return content.length <= maxOutputChars - ? content - : content.slice(0, maxOutputChars) + TRUNCATED_MESSAGE -} - -function codepointCompare(left: string, right: string): number { - return left < right ? -1 : left > right ? 1 : 0 -} - -function matchOffsets(content: string, search: string): number[] { - const offsets: number[] = [] - let offset = 0 - while (true) { - const match = content.indexOf(search, offset) - if (match < 0) return offsets - offsets.push(match) - offset = match + search.length - } -} - -function lineNumbersAt(content: string, offsets: readonly number[]): number[] { - let line = 1 - let cursor = 0 - return offsets.map((offset) => { - while (cursor < offset) { - if (content[cursor] === '\n') line += 1 - cursor += 1 - } - return line - }) -} - -class MutationPolicy { - private readonly policy: SandboxPolicyService | undefined - - constructor(ctx: Context) { - this.policy = ctx.fs.sandboxMode === undefined ? undefined : ctx.get('sandboxPolicy') - if (ctx.fs.sandboxMode !== undefined && this.policy === undefined) { - throw new Error('tool-str-replace-editor: the mounted filesystem confines but ctx.sandboxPolicy is missing') - } - } - - resolve(exec: ToolRunContext): SandboxExecutionPolicy | undefined { - return this.policy?.resolve({ - ...exec.agent === undefined ? {} : { session: exec.agent.session }, - }) - } - - mapError(error: unknown, policy: SandboxExecutionPolicy | undefined): unknown { - if (!(error instanceof FsError) || error.code !== 'FS_SANDBOX_DENIED') return error - const mode = (policy as SandboxExecutionPolicy).mode - return new FsError(sandboxDenialMarker(mode), 'FS_SANDBOX_DENIED', { cause: error }) - } -} - -async function resolveTarget( - ctx: Context, - path: string, - signal: AbortSignal, -): Promise { - if (path.trim().length === 0) throw new Error('path must be a non-empty string') - if (!isAbsolute(path)) { - throw new Error(`The path ${path} is not an absolute path, it should start with \`/\`. Maybe you meant /${path}?`) - } - return ctx.fs.resolve(path, { signal }) -} - -async function statExisting( - ctx: Context, - target: FsTarget, - command: 'view' | 'str_replace' | 'insert', - exec: ToolRunContext, -): Promise { - const info = await ctx.fs.stat(target, exec.signal) - if (info === undefined) { - ctx.emit('fs/observed', target, { kind: 'absent' }, exec) - throw new FsError( - `The path ${target.displayPath} does not exist. Please provide a valid path.`, - 'FS_NOT_FOUND', - ) - } - if (info.type === 'directory' && command !== 'view') { - throw new FsError( - `The path ${target.displayPath} is a directory and only the \`view\` command can be used on directories`, - 'FS_NOT_REGULAR_FILE', - ) - } - return info -} - -function requiredForCommand( - value: string | undefined, - parameter: string, - command: string, - allowEmpty = true, -): string { - if (value === undefined) throw new Error(`Parameter \`${parameter}\` is required for command: ${command}`) - if (!allowEmpty && value.length === 0) { - throw new Error(`Parameter \`${parameter}\` is empty for command: ${command}`) - } - return value -} - -function formatFileView( - path: string, - content: string, - maxOutputChars: number, - viewRange?: number[], -): string { - const allLines = content.split('\n') - let lines = allLines - let initialLine = 1 - let finalLine: number | undefined - let prompt = `Here's the content of ${path} with line numbers (which has a total of ${allLines.length} lines)` - if (viewRange !== undefined) { - const [requestedInitialLine, requestedFinalLine] = viewRange - if ( - viewRange.length !== 2 - || requestedInitialLine === undefined - || requestedFinalLine === undefined - || !viewRange.every(Number.isInteger) - ) { - throw new Error('Invalid `view_range`. It should be a list of two integers.') - } - initialLine = requestedInitialLine - finalLine = requestedFinalLine - if (initialLine < 1 || initialLine > allLines.length) { - throw new Error( - `Invalid \`view_range\`: [${viewRange.join(', ')}]. Its first element \`${initialLine}\` should be within the range of lines of the file: [1, ${allLines.length}]`, - ) - } - if (finalLine > allLines.length) { - throw new Error( - `Invalid \`view_range\`: [${viewRange.join(', ')}]. Its second element \`${finalLine}\` should be smaller than the number of lines in the file: \`${allLines.length}\``, - ) - } - if (finalLine !== -1 && finalLine < initialLine) { - throw new Error( - `Invalid \`view_range\`: [${viewRange.join(', ')}]. Its second element \`${finalLine}\` should be larger or equal than its first \`${initialLine}\``, - ) - } - lines = finalLine === -1 - ? allLines.slice(initialLine - 1) - : allLines.slice(initialLine - 1, finalLine) - prompt += ` with view_range=[${initialLine}, ${finalLine}]` - } - const numbered = lines - .map((line, index) => `${String(initialLine + index).padStart(6, ' ')} ${line}`) - .join('\n') - return maybeTruncate(`${prompt}:\n${numbered}\n`, maxOutputChars) -} - -async function listDirectory( - ctx: Context, - target: FsTarget, - maxOutputChars: number, - exec: ToolRunContext, -): Promise { - async function visit(dir: FsTarget, depth: number): Promise { - const entries = await ctx.fs.listDir(dir, exec.signal) - const rows: string[] = [] - for (const entry of entries.filter(candidate => - !candidate.name.startsWith('.') - && candidate.name !== 'node_modules' - && candidate.name !== '__pycache__')) { - const type = entry.type === 'directory' ? 'd' : entry.type === 'file' ? 'f' : '?' - rows.push(`${type}\t${entry.target.displayPath}`) - if (entry.type === 'directory' && depth < 2) { - rows.push(...await visit(entry.target, depth + 1)) - } - } - return rows - } - const rows = [`d\t${target.displayPath}`, ...await visit(target, 1)] - rows.sort((left, right) => { - const leftPath = left.slice(left.indexOf('\t') + 1) - const rightPath = right.slice(right.indexOf('\t') + 1) - return codepointCompare(leftPath, rightPath) - }) - const listing = maybeTruncate(rows.join('\n') + '\n', maxOutputChars) - return `Here're the files and directories up to 2 levels deep in ${target.displayPath}, excluding hidden items, node_modules, and Python cache directories:\n${listing}\n` -} - -async function viewPath( - ctx: Context, - path: string, - viewRange: number[] | undefined, - maxOutputChars: number, - exec: ToolRunContext, -): Promise { - const target = await resolveTarget(ctx, path, exec.signal) - const info = await statExisting(ctx, target, 'view', exec) - if (info.type === 'directory') { - if (viewRange !== undefined) { - throw new Error('The `view_range` parameter is not allowed when `path` points to a directory.') - } - return listDirectory(ctx, target, maxOutputChars, exec) - } - if (info.type !== 'file') { - throw new FsError(`cannot view "${target.displayPath}": not a regular file or directory`, 'FS_NOT_REGULAR_FILE') - } - const content = await ctx.fs.readText(target, exec.signal) - ctx.emit('fs/observed', target, { kind: 'present', version: info.version }, exec) - return formatFileView(target.displayPath, content, maxOutputChars, viewRange) -} - -async function createFile( - ctx: Context, - policy: MutationPolicy, - path: string, - fileText: string | undefined, - exec: ToolRunContext, -): Promise { - const content = requiredForCommand(fileText, 'file_text', 'create') - const sandboxPolicy = policy.resolve(exec) - const target = await resolveTarget(ctx, path, exec.signal) - if (await ctx.fs.stat(target, exec.signal) !== undefined) { - throw new Error(`File already exists at: ${target.displayPath}. Cannot overwrite files using command \`create\`.`) - } - const intent = await ctx.waterfall( - 'fs/write-intent', - target, - exec, - () => ({ kind: 'createIfAbsent' } as const), - ) - let outcome - try { - outcome = await ctx.fs.writeText( - target, - content, - intent, - exec.signal, - sandboxPolicy, - ) - } catch (error: unknown) { - throw policy.mapError(error, sandboxPolicy) - } - ctx.emit('fs/observed', target, { kind: 'present', version: outcome.version }, exec) - return `New file created successfully at: ${target.displayPath}` -} - -async function replaceInFile( - ctx: Context, - policy: MutationPolicy, - path: string, - oldStr: string | undefined, - newStr: string | null | undefined, - exec: ToolRunContext, -): Promise { - if (newStr === null) { - throw new Error('Parameter `new_str` must be omitted or contain a string for command: str_replace') - } - const sandboxPolicy = policy.resolve(exec) - const target = await resolveTarget(ctx, path, exec.signal) - const intent = await ctx.waterfall('fs/edit-intent', target, exec, () => undefined) - const oldValue = requiredForCommand(oldStr, 'old_str', 'str_replace', false) - const newValue = newStr ?? '' - const info = await statExisting(ctx, target, 'str_replace', exec) - if (info.type !== 'file') { - throw new FsError(`cannot edit "${target.displayPath}": not a regular file`, 'FS_NOT_REGULAR_FILE') - } - const before = await ctx.fs.readText(target, exec.signal) - const offsets = matchOffsets(before, oldValue) - const offset = offsets[0] - if (offset === undefined) { - throw new FsError( - `No replacement was performed, old_str \`${oldValue}\` did not appear verbatim in ${target.displayPath}.`, - 'FS_EDIT_NOT_FOUND', - ) - } - if (offsets.length > 1) { - const lines = lineNumbersAt(before, offsets) - throw new FsError( - `No replacement was performed. Multiple occurrences of old_str \`${oldValue}\` in lines [${lines.join(', ')}]. Please ensure it is unique`, - 'FS_AMBIGUOUS_EDIT', - ) - } - let outcome - try { - outcome = await ctx.fs.writeText( - target, - before.slice(0, offset) + newValue + before.slice(offset + oldValue.length), - intent === undefined - ? { kind: 'replaceIfVersion', version: info.version } - : { kind: 'replaceIfVersion', version: intent.version }, - exec.signal, - sandboxPolicy, - ) - } catch (error: unknown) { - throw policy.mapError(error, sandboxPolicy) - } - ctx.emit('fs/observed', target, { kind: 'present', version: outcome.version }, exec) - return `The file ${target.displayPath} has been edited successfully.` -} - -async function insertInFile( - ctx: Context, - policy: MutationPolicy, - path: string, - insertLine: number | undefined, - newStr: string | undefined, - exec: ToolRunContext, -): Promise { - if (insertLine === undefined) throw new Error('Parameter `insert_line` is required for command: insert') - const value = requiredForCommand(newStr, 'new_str', 'insert') - const sandboxPolicy = policy.resolve(exec) - const target = await resolveTarget(ctx, path, exec.signal) - const intent = await ctx.waterfall('fs/edit-intent', target, exec, () => undefined) - const info = await statExisting(ctx, target, 'insert', exec) - if (info.type !== 'file') { - throw new FsError(`cannot insert into "${target.displayPath}": not a regular file`, 'FS_NOT_REGULAR_FILE') - } - const before = await ctx.fs.readText(target, exec.signal) - const lines = before.split('\n') - if (!Number.isInteger(insertLine) || insertLine < 0 || insertLine > lines.length) { - throw new Error( - `Invalid \`insert_line\` parameter: ${insertLine}. It should be within the range of lines of the file: [0, ${lines.length}]`, - ) - } - const after = [ - ...lines.slice(0, insertLine), - ...value.split('\n'), - ...lines.slice(insertLine), - ].join('\n') - const expected: FsWriteIntent = intent === undefined - ? { kind: 'replaceIfVersion', version: info.version } - : { kind: 'replaceIfVersion', version: intent.version } - let outcome - try { - outcome = await ctx.fs.writeText(target, after, expected, exec.signal, sandboxPolicy) - } catch (error: unknown) { - throw policy.mapError(error, sandboxPolicy) - } - ctx.emit('fs/observed', target, { kind: 'present', version: outcome.version }, exec) - return `The file ${target.displayPath} has been edited successfully.` -} - -interface ResolvedConfig { - maxOutputChars: number - description: string -} - -function presentEditorCall(args: { - command: 'view' | 'create' | 'str_replace' | 'insert' - path: string - file_text?: string | null - insert_line?: number | null - new_str?: string | null - old_str?: string | null -}): ToolCallView { - switch (args.command) { - case 'view': - return { - card: 'generic', - title: `view ${args.path}`, - kind: 'read', - locations: [{ path: args.path }], - } - case 'create': - return { - card: 'diff', - title: `create ${args.path}`, - diffs: [{ path: args.path, oldText: null, newText: args.file_text ?? '' }], - locations: [{ path: args.path }], - } - case 'str_replace': - return { - card: 'diff', - title: `str_replace ${args.path}`, - diffs: [{ - path: args.path, - oldText: args.old_str ?? null, - newText: args.new_str ?? '', - }], - locations: [{ path: args.path }], - } - case 'insert': - return { - card: 'generic', - title: `insert ${args.path}`, - kind: 'edit', - locations: [{ - path: args.path, - ...args.insert_line === undefined || args.insert_line === null - ? {} - : { line: Math.max(1, args.insert_line + 1) }, - }], - } - } -} - -/** Register the model-facing `str_replace_editor` tool. */ -function registerStrReplaceEditor(ctx: Context, config: ResolvedConfig): void { - const policy = new MutationPolicy(ctx) - ctx.tools.register(defineTool({ - name: 'str_replace_editor', - description: config.description, - parameters: { - command: { - type: 'string', - required: true, - enum: ['view', 'create', 'str_replace', 'insert'], - description: 'The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.', - }, - path: { - type: 'string', - required: true, - description: 'Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`.', - }, - file_text: { - oneOf: [{ type: 'string' }, { type: 'null' }], - description: 'Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter.', - }, - insert_line: { - oneOf: [{ type: 'integer' }, { type: 'null' }], - description: 'Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter.', - }, - new_str: { - oneOf: [{ type: 'string' }, { type: 'null' }], - description: 'Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter.', - }, - old_str: { - oneOf: [{ type: 'string' }, { type: 'null' }], - description: 'Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter.', - }, - view_range: { - oneOf: [ - { type: 'array', items: { type: 'integer' } }, - { type: 'null' }, - ], - description: 'Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file.', - }, - }, - output: { - schema: { type: 'string' }, - render: (_args, value) => [{ type: 'text', text: value }], - }, - async execute(args, exec) { - switch (args.command) { - case 'view': - return viewPath(ctx, args.path, args.view_range ?? undefined, config.maxOutputChars, exec) - case 'create': - return createFile(ctx, policy, args.path, args.file_text ?? undefined, exec) - case 'str_replace': - return replaceInFile( - ctx, - policy, - args.path, - args.old_str ?? undefined, - args.new_str, - exec, - ) - case 'insert': - return insertInFile( - ctx, - policy, - args.path, - args.insert_line ?? undefined, - args.new_str ?? undefined, - exec, - ) - } - }, - presentCall: presentEditorCall, - })) -} - -export const name = 'tool-str-replace-editor' -export const inject = ['tools', 'fs'] - -/** Configuration for the string-replacement editor tool. */ -export interface Config { - /** Maximum returned view characters before clipping (default 16000). */ - maxOutputChars?: number - /** Model-facing tool description. */ - description?: string -} - -/** Runtime configuration schema for the string-replacement editor tool. */ -export const Config: z = z.object({ - maxOutputChars: z.number().default(16_000), - description: z.string().default(DEFAULT_DESCRIPTION), -}) - -/** Register one `str_replace_editor` tool over `ctx.fs`. */ -export function apply(ctx: Context, config: Config): void { - const resolved: ResolvedConfig = { - maxOutputChars: config.maxOutputChars ?? 16_000, - description: config.description ?? DEFAULT_DESCRIPTION, - } - if (!Number.isSafeInteger(resolved.maxOutputChars) || resolved.maxOutputChars <= 0) { - throw new Error('tool-str-replace-editor: maxOutputChars must be a positive safe integer') - } - if (resolved.description.trim().length === 0) { - throw new Error('tool-str-replace-editor: description must be non-empty') - } - registerStrReplaceEditor(ctx, resolved) -} diff --git a/packages/fs/tool-str-replace-editor/tests/tools.spec.ts b/packages/fs/tool-str-replace-editor/tests/tools.spec.ts deleted file mode 100644 index 0965db21d2..0000000000 --- a/packages/fs/tool-str-replace-editor/tests/tools.spec.ts +++ /dev/null @@ -1,643 +0,0 @@ -import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { afterEach, describe, expect, it } from 'vitest' -import { Context } from '@deepseek-ai/cordis' -import { FsVersion } from '@deepseek-ai/dsh-fs' -import { ToolCallId } from '@deepseek-ai/dsh-llm' -import { Session, SessionId } from '@deepseek-ai/dsh-session' -import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent' -import type { Agent } from '@deepseek-ai/dsh-agent' -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' - -const contexts: Context[] = [] -const roots: string[] = [] -let callNumber = 0 - -afterEach(async () => { - for (const ctx of contexts.splice(0)) await ctx.fiber.dispose() - for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true }) -}) - -function agent(ctx: Context, cwd: string): Agent { - const id = SessionId(`str-replace-editor-owner-${callNumber}`) - const scope = ctx.plugin(() => {}) - const session = Session.create(id, [], { version: 0, id, createdAt: 0, cwd, isSeeded: false }) - const value: Agent = { - id, - options: {}, - session, - inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }), - status: 'idle', - ctx: scope.ctx, - send: () => {}, - followup: () => {}, - steer: () => ({ outcome: Promise.resolve({ status: 'rejected' as const }) }), - inject: () => {}, - cancel() {}, - runMaintenance: task => task(new AbortController().signal), - whenIdle: () => Promise.resolve(), - } - ctx.agents.register(value) - return value -} - -function text(result: { content: { type: string; text?: string }[] }): string { - return result.content.filter(block => block.type === 'text').map(block => block.text).join('') -} - -function call(ctx: Context, owner: Agent | undefined, args: unknown) { - return ctx.tools.execute({ - signal: new AbortController().signal, - callId: ToolCallId(`str-replace-editor-${++callNumber}`), - name: 'str_replace_editor', - arguments: args, - ...owner === undefined ? {} : { agent: owner }, - }) -} - -async function setup( - config: ToolStrReplaceEditor.Config = {}, - options: { fsPolicy?: boolean; sandboxMode?: 'read-only' | 'workspace-write' | 'danger-full-access' } = {}, -) { - const root = await mkdtemp(join(tmpdir(), 'dsh-tool-str-replace-editor-')) - roots.push(root) - const ctx = new Context() - contexts.push(ctx) - await ctx.plugin(SystemPrompt) - await ctx.plugin(ToolRuntime) - await ctx.plugin(AgentRegistry) - 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 }) - } - if (options.fsPolicy === true) await ctx.plugin(FsPolicy) - const fiber = await ctx.plugin(ToolStrReplaceEditor, config) - return { ctx, root, fiber, owner: agent(ctx, root) } -} - -describe('tool-str-replace-editor', () => { - it('registers the standalone schema and configurable description', async () => { - const { ctx, fiber } = await setup({ description: 'custom editor description' }) - const schema = ctx.tools.schemas()[0] - expect(ctx.tools.schemas().map(item => item.name)).toEqual(['str_replace_editor']) - expect(schema?.description).toBe('custom editor description') - const properties = (schema?.parameters as { - properties: Record - }).properties - expect(properties).not.toHaveProperty('replace_all') - expect(properties.file_text?.oneOf?.map(option => option.type)).toEqual(['string', 'null']) - expect(properties.insert_line?.oneOf?.map(option => option.type)).toEqual(['integer', 'null']) - expect(properties.new_str?.oneOf?.map(option => option.type)).toEqual(['string', 'null']) - expect(properties.old_str?.oneOf?.map(option => option.type)).toEqual(['string', 'null']) - expect(properties.view_range?.oneOf?.map(option => option.type)).toEqual(['array', 'null']) - expect(properties.view_range?.oneOf?.[0]?.items?.type).toBe('integer') - expect(ctx.tools.get('str_replace_editor')?.presentCall?.({ - command: 'view', - path: '/workspace/a.txt', - file_text: null, - insert_line: null, - new_str: null, - old_str: null, - view_range: null, - })).toMatchObject({ - card: 'generic', - kind: 'read', - locations: [{ path: '/workspace/a.txt' }], - }) - expect(ctx.tools.get('str_replace_editor')?.presentCall?.({ - command: 'create', - path: '/workspace/a.txt', - file_text: 'hello', - insert_line: null, - new_str: null, - old_str: null, - view_range: null, - })).toMatchObject({ - card: 'diff', - diffs: [{ path: '/workspace/a.txt', oldText: null, newText: 'hello' }], - }) - expect(ctx.tools.get('str_replace_editor')?.presentCall?.({ - command: 'str_replace', - path: '/workspace/a.txt', - old_str: 'old', - new_str: 'new', - file_text: null, - insert_line: null, - view_range: null, - })).toMatchObject({ - card: 'diff', - diffs: [{ path: '/workspace/a.txt', oldText: 'old', newText: 'new' }], - }) - expect(ctx.tools.get('str_replace_editor')?.presentCall?.({ - command: 'insert', - path: '/workspace/a.txt', - insert_line: null, - new_str: 'x', - })).toMatchObject({ - card: 'generic', - kind: 'edit', - locations: [{ path: '/workspace/a.txt' }], - }) - expect(ctx.tools.get('str_replace_editor')?.presentCall?.({ - command: 'insert', - path: '/workspace/a.txt', - insert_line: 0, - new_str: 'x', - file_text: null, - old_str: null, - view_range: null, - })).toMatchObject({ - card: 'generic', - kind: 'edit', - locations: [{ path: '/workspace/a.txt', line: 1 }], - }) - expect(ctx.tools.get('str_replace_editor')?.presentCall?.({ - command: 'create', - path: '/workspace/empty.txt', - })).toMatchObject({ - diffs: [{ path: '/workspace/empty.txt', oldText: null, newText: '' }], - }) - expect(ctx.tools.get('str_replace_editor')?.presentCall?.({ - command: 'str_replace', - path: '/workspace/a.txt', - })).toMatchObject({ - diffs: [{ path: '/workspace/a.txt', oldText: null, newText: '' }], - }) - expect(ctx.tools.get('str_replace_editor')?.presentCall?.({ - command: 'insert', - path: '/workspace/a.txt', - })).toMatchObject({ - locations: [{ path: '/workspace/a.txt' }], - }) - - await fiber.dispose() - expect(ctx.tools.schemas()).toEqual([]) - expect(ctx.tools.get('str_replace_editor')).toBeUndefined() - }) - - it('creates, views, replaces, and inserts with the canonical model-facing output', async () => { - const { ctx, root, owner } = await setup() - const sample = join(root, 'sample.txt') - expect(text(await call(ctx, owner, { - command: 'create', - path: sample, - file_text: 'one\ntwo\nthree\n', - insert_line: null, - new_str: null, - old_str: null, - view_range: null, - }))).toBe(`New file created successfully at: ${sample}`) - - expect(text(await call(ctx, owner, { - command: 'view', - path: sample, - file_text: null, - insert_line: null, - new_str: null, - old_str: null, - view_range: null, - }))).toContain(' 2 two') - - expect(text(await call(ctx, owner, { - command: 'view', - path: sample, - view_range: [2, -1], - }))).toBe([ - `Here's the content of ${sample} with line numbers (which has a total of 4 lines) with view_range=[2, -1]:`, - ' 2 two', - ' 3 three', - ' 4 ', - '', - ].join('\n')) - - expect(text(await call(ctx, owner, { - command: 'str_replace', - path: sample, - old_str: 'two', - new_str: 'TWO', - file_text: null, - insert_line: null, - view_range: null, - }))).toBe(`The file ${sample} has been edited successfully.`) - expect(text(await call(ctx, owner, { - command: 'str_replace', - path: sample, - old_str: 'TWO', - }))).toBe(`The file ${sample} has been edited successfully.`) - expect(text(await call(ctx, owner, { - command: 'insert', - path: sample, - insert_line: 1, - new_str: 'between', - file_text: null, - old_str: null, - view_range: null, - }))).toBe(`The file ${sample} has been edited successfully.`) - expect(await readFile(sample, 'utf8')).toBe('one\nbetween\n\nthree\n') - }) - - it('a failed view records absence so create can recover after external deletion', async () => { - const { ctx, root, owner } = await setup({}, { fsPolicy: true }) - const sample = join(root, 'deleted.txt') - await writeFile(sample, 'original') - expect((await call(ctx, owner, { command: 'view', path: sample })).isError).toBe(false) - await rm(sample) - - const missing = await call(ctx, owner, { command: 'view', path: sample }) - expect(missing.isError).toBe(true) - expect(missing.error).toMatchObject({ info: { code: 'FS_NOT_FOUND' } }) - - const edit = await call(ctx, owner, { - command: 'str_replace', - path: sample, - old_str: 'original', - new_str: 'edited', - }) - expect(edit.isError).toBe(true) - expect(edit.error).toMatchObject({ info: { code: 'FS_NOT_FOUND' } }) - - const created = await call(ctx, owner, { - command: 'create', - path: sample, - file_text: 'fresh', - }) - expect(created.isError).toBe(false) - expect(await readFile(sample, 'utf8')).toBe('fresh') - }) - - it('writes replacement text literally', async () => { - const { ctx, root, owner } = await setup() - const sample = join(root, 'literal.txt') - const replacement = "$&|$`|$'|$$" - await writeFile(sample, 'before OLD after') - - expect((await call(ctx, owner, { - command: 'str_replace', - path: sample, - old_str: 'OLD', - new_str: replacement, - })).isError).toBe(false) - expect(await readFile(sample, 'utf8')).toBe(`before ${replacement} after`) - }) - - it('lists visible entries to depth two and clips at the configured view limit', async () => { - const { ctx, root, owner } = await setup({ maxOutputChars: 10_000 }) - await mkdir(join(root, 'dir', 'nested', 'third'), { recursive: true }) - await mkdir(join(root, 'dir', 'node_modules', 'pkg'), { recursive: true }) - await mkdir(join(root, 'dir', 'node_modules_old'), { recursive: true }) - await mkdir(join(root, 'dir', '__pycache__'), { recursive: true }) - await mkdir(join(root, 'dir', '__pycache__backup'), { recursive: true }) - await writeFile(join(root, 'dir', 'visible.txt'), 'ok') - await writeFile(join(root, 'dir', '.hidden'), 'hidden') - await writeFile(join(root, 'dir', 'nested', 'child.txt'), 'child') - await writeFile(join(root, 'dir', 'nested', 'third', 'too-deep.txt'), 'deep') - await writeFile(join(root, 'dir', 'node_modules', 'pkg', 'index.js'), 'hidden dependency') - await writeFile(join(root, 'dir', 'node_modules_old', 'kept.js'), 'visible source') - await writeFile(join(root, 'dir', '__pycache__', 'module.pyc'), 'cache') - await writeFile(join(root, 'dir', '__pycache__backup', 'kept.py'), 'visible source') - const listDir = ctx.fs.listDir.bind(ctx.fs) - const otherTarget = await ctx.fs.resolve(join(root, 'dir', 'other')) - ctx.fs.listDir = async (target, signal) => { - const entries = await listDir(target, signal) - return target.displayPath === join(root, 'dir') - ? [ - { name: 'same-target', type: 'other', target: otherTarget }, - { name: 'other', type: 'other', target: otherTarget }, - ...entries.toReversed(), - ] - : entries - } - - const listing = text(await call(ctx, owner, { command: 'view', path: join(root, 'dir') })) - expect(listing).not.toContain('.hidden') - expect(listing).not.toContain('too-deep.txt') - expect(listing).not.toContain('index.js') - expect(listing).not.toContain('module.pyc') - // The listing carries absolute display paths; the POSIX-style substrings - // only match on Linux, so assert with platform separators. - expect(listing).toContain(join('node_modules_old', 'kept.js')) - expect(listing).toContain(join('__pycache__backup', 'kept.py')) - - const clipped = await setup({ maxOutputChars: 10 }) - await writeFile(join(clipped.root, 'large.txt'), 'x'.repeat(100)) - expect(text(await call(clipped.ctx, clipped.owner, { - command: 'view', - path: join(clipped.root, 'large.txt'), - }))) - .toContain('') - }) - - it('matches canonical empty-line, range, and end-insert behavior', async () => { - const { ctx, root, owner } = await setup() - const empty = join(root, 'empty.txt') - const newline = join(root, 'newline.txt') - const plain = join(root, 'plain.txt') - await writeFile(empty, '') - await writeFile(newline, '\n') - await writeFile(plain, 'one\ntwo') - - expect(text(await call(ctx, owner, { command: 'view', path: empty }))) - .toContain('(which has a total of 1 lines):\n 1 \n') - expect(text(await call(ctx, owner, { command: 'view', path: newline }))) - .toContain('(which has a total of 2 lines):\n 1 \n 2 \n') - expect(text(await call(ctx, owner, { - command: 'view', - path: plain, - view_range: [1, 2], - }))).toContain(' 2 two') - expect(text(await call(ctx, undefined, { - command: 'view', - path: plain, - }))).toContain(' 1 one') - expect((await call(ctx, undefined, { - command: 'create', - path: join(root, 'ownerless.txt'), - file_text: 'ownerless', - })).isError).toBe(false) - - await call(ctx, owner, { - command: 'insert', - path: plain, - insert_line: 2, - new_str: 'three', - }) - expect(await readFile(plain, 'utf8')).toBe('one\ntwo\nthree') - - await writeFile(newline, 'one\n') - await call(ctx, owner, { - command: 'insert', - path: newline, - insert_line: 2, - new_str: 'three', - }) - expect(await readFile(newline, 'utf8')).toBe('one\n\nthree') - }) - - it('uses old_str-only replacement failures and rejects relative paths', async () => { - const { ctx, root, owner } = await setup() - const ambiguous = join(root, 'ambiguous.txt') - await writeFile(ambiguous, 'same\nother\nsame') - - const missing = await call(ctx, owner, { - command: 'str_replace', - path: ambiguous, - old_str: 'absent', - new_str: 'x', - }) - expect(missing.isError).toBe(true) - expect(text(missing)).toContain(`old_str \`absent\` did not appear verbatim in ${ambiguous}`) - expect(text(missing)).not.toContain('old_string') - - const repeated = await call(ctx, owner, { - command: 'str_replace', - path: ambiguous, - old_str: 'same', - new_str: 'x', - }) - expect(repeated.isError).toBe(true) - expect(text(repeated)).toContain('Multiple occurrences of old_str `same` in lines [1, 3]') - expect(text(repeated)).not.toContain('replace_all') - - await writeFile(ambiguous, 'alpha\nbeta\nmiddle\nalpha\nbeta') - const repeatedMultiline = await call(ctx, owner, { - command: 'str_replace', - path: ambiguous, - old_str: 'alpha\nbeta', - new_str: 'x', - }) - expect(text(repeatedMultiline)) - .toContain('Multiple occurrences of old_str `alpha\nbeta` in lines [1, 4]') - - const mixedEol = join(root, 'mixed-eol.txt') - await writeFile(mixedEol, 'alpha\r\nbeta\nmiddle\nalpha\nbeta') - expect((await call(ctx, owner, { - command: 'str_replace', - path: mixedEol, - old_str: 'alpha\r\nbeta', - new_str: 'replaced', - })).isError).toBe(false) - expect(await readFile(mixedEol, 'utf8')).toBe('replaced\nmiddle\nalpha\nbeta') - - const relative = await call(ctx, owner, { command: 'view', path: 'ambiguous.txt' }) - expect(relative.isError).toBe(true) - expect(text(relative)).toContain('is not an absolute path') - expect(await readFile(ambiguous, 'utf8')).toBe('alpha\nbeta\nmiddle\nalpha\nbeta') - }) - - it('reports invalid commands or arguments without mutating files', async () => { - const { ctx, root, owner } = await setup() - const ambiguous = join(root, 'ambiguous.txt') - const empty = join(root, 'empty.txt') - const trailingNewline = join(root, 'trailing-newline.txt') - const threeLines = join(root, 'three-lines.txt') - const directory = join(root, 'directory') - await writeFile(ambiguous, 'same same') - await writeFile(empty, '') - await writeFile(trailingNewline, 'one\n') - await writeFile(threeLines, 'one\ntwo\nthree') - await mkdir(directory) - - const cases = [ - { command: null, path: ambiguous }, - { command: 'view', path: null }, - { command: 'view', path: '' }, - { command: 'view', path: join(root, 'missing.txt') }, - { command: 'view', path: ambiguous, view_range: [1] }, - { command: 'view', path: ambiguous, view_range: [0, 1] }, - { command: 'view', path: ambiguous, view_range: [1.5, 2] }, - { command: 'view', path: threeLines, view_range: [1, 99] }, - { command: 'view', path: threeLines, view_range: [2, 1] }, - { command: 'view', path: directory, view_range: [1, 1] }, - { command: 'create', path: join(root, 'new.txt') }, - { command: 'create', path: join(root, 'new.txt'), file_text: null }, - { command: 'create', path: ambiguous, file_text: 'overwrite' }, - { command: 'str_replace', path: ambiguous, new_str: 'x' }, - { command: 'str_replace', path: ambiguous, old_str: null, new_str: 'x' }, - { command: 'str_replace', path: ambiguous, old_str: 'same same', new_str: null }, - { command: 'str_replace', path: ambiguous, old_str: '', new_str: 'x' }, - { command: 'insert', path: ambiguous, new_str: 'x' }, - { command: 'insert', path: ambiguous, insert_line: null, new_str: 'x' }, - { command: 'insert', path: ambiguous, insert_line: 0, new_str: null }, - { command: 'insert', path: ambiguous, insert_line: -1, new_str: 'x' }, - { command: 'insert', path: ambiguous, insert_line: 1.5, new_str: 'x' }, - { command: 'insert', path: ambiguous, insert_line: 99, new_str: 'x' }, - { command: 'insert', path: empty, insert_line: 2, new_str: 'x' }, - { command: 'insert', path: directory, insert_line: 0, new_str: 'x' }, - ] - for (const args of cases) { - expect((await call(ctx, owner, args)).isError).toBe(true) - } - expect(await readFile(ambiguous, 'utf8')).toBe('same same') - - ctx.fs.stat = async () => ({ version: FsVersion('special'), type: 'other' }) - const special = await call(ctx, owner, { command: 'view', path: join(root, 'special') }) - expect(special.isError).toBe(true) - expect(special.error).toMatchObject({ info: { code: 'FS_NOT_REGULAR_FILE' } }) - expect((await call(ctx, owner, { - command: 'str_replace', - path: join(root, 'special'), - old_str: 'x', - new_str: 'y', - })).error).toMatchObject({ info: { code: 'FS_NOT_REGULAR_FILE' } }) - expect((await call(ctx, owner, { - command: 'insert', - path: join(root, 'special'), - insert_line: 0, - new_str: 'x', - })).error).toMatchObject({ info: { code: 'FS_NOT_REGULAR_FILE' } }) - }) - - it('delegates read-before-edit decisions to fs-observation-policy', async () => { - const { ctx, root, owner } = await setup({}, { fsPolicy: true }) - const existing = join(root, 'existing.txt') - const created = join(root, 'created.txt') - await writeFile(existing, 'before') - - const blindEdit = await call(ctx, owner, { - command: 'str_replace', - path: existing, - old_str: 'before', - new_str: 'after', - }) - expect(blindEdit.error).toMatchObject({ info: { code: 'FS_NOT_OBSERVED' } }) - expect(await readFile(existing, 'utf8')).toBe('before') - - await call(ctx, owner, { command: 'view', path: existing }) - expect((await call(ctx, owner, { - command: 'str_replace', - path: existing, - old_str: 'before', - new_str: 'after', - })).isError).toBe(false) - expect(await readFile(existing, 'utf8')).toBe('after') - - expect((await call(ctx, owner, { - command: 'insert', - path: existing, - insert_line: 1, - new_str: 'tail', - })).isError).toBe(false) - expect(await readFile(existing, 'utf8')).toBe('after\ntail') - - expect((await call(ctx, owner, { - command: 'create', - path: created, - file_text: 'new', - })).isError).toBe(false) - expect(await readFile(created, 'utf8')).toBe('new') - }) - - it('passes the session sandbox policy to every mutation', async () => { - const { ctx, root, owner } = await setup({}, { sandboxMode: 'read-only' }) - const path = join(root, 'blocked.txt') - const result = await call(ctx, owner, { - command: 'create', - path, - file_text: 'blocked', - }) - expect(result.error).toMatchObject({ info: { code: 'FS_SANDBOX_DENIED' } }) - expect(text(result)).toContain('[sandbox: file access denied under read-only mode]') - - const ownerless = await call(ctx, undefined, { - command: 'create', - path: join(root, 'ownerless-blocked.txt'), - file_text: 'blocked', - }) - expect(ownerless.error).toMatchObject({ info: { code: 'FS_SANDBOX_DENIED' } }) - }) - - it('preserves tabs outside the edited region', async () => { - const { ctx, root, owner } = await setup() - const path = join(root, 'Makefile') - await writeFile(path, 'target:\n\told\nremove\n') - expect(text(await call(ctx, owner, { command: 'view', path }))) - .toContain(' 2 \told') - await call(ctx, owner, { - command: 'str_replace', - path, - old_str: '\told', - new_str: '\tnew', - }) - await call(ctx, owner, { - command: 'str_replace', - path, - old_str: 'remove\n', - }) - await call(ctx, owner, { - command: 'insert', - path, - insert_line: 1, - new_str: '\tkept', - }) - expect(await readFile(path, 'utf8')).toBe('target:\n\tkept\n\tnew\n') - }) - - it('reports missing sandbox-policy composition during plugin startup', async () => { - const root = await mkdtemp(join(tmpdir(), 'dsh-tool-str-replace-editor-missing-policy-')) - roots.push(root) - const ctx = new Context() - contexts.push(ctx) - await ctx.plugin(SystemPrompt) - await ctx.plugin(ToolRuntime) - await ctx.plugin(AgentRegistry) - await ctx.plugin(LocalFileSystem, { cwd: root }) - Object.defineProperty(ctx.fs, 'sandboxMode', { value: 'read-only' }) - - await expect(ctx.plugin(ToolStrReplaceEditor)) - .rejects.toThrow('the mounted filesystem confines but ctx.sandboxPolicy is missing') - }) - - it('maps unexpected backend write failures for replace and insert', async () => { - const { ctx, root, owner } = await setup() - const path = join(root, 'backend-error.txt') - await writeFile(path, 'old\n') - const failWrite = async (): Promise => { - throw new Error('backend write failed') - } - ctx.fs.writeText = failWrite - - const replace = await call(ctx, owner, { - command: 'str_replace', - path, - old_str: 'old', - new_str: 'new', - }) - expect(replace.isError).toBe(true) - expect(text(replace)).toContain('backend write failed') - - const insert = await call(ctx, owner, { - command: 'insert', - path, - insert_line: 1, - new_str: 'new', - }) - expect(insert.isError).toBe(true) - expect(text(insert)).toContain('backend write failed') - }) - - it('rejects invalid plugin config', () => { - expect(() => { - ToolStrReplaceEditor.apply(new Context(), { maxOutputChars: 0 }) - }).toThrow('maxOutputChars must be a positive safe integer') - expect(() => { - ToolStrReplaceEditor.apply(new Context(), { description: ' ' }) - }).toThrow('description must be non-empty') - }) -}) diff --git a/packages/fs/tool-str-replace-editor/tsconfig.json b/packages/fs/tool-str-replace-editor/tsconfig.json deleted file mode 100644 index b9dba81d5d..0000000000 --- a/packages/fs/tool-str-replace-editor/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "lib/types" - }, - "include": ["src"], - "references": [ - { "path": "../../../vendor/cordis" }, - { "path": "../../../vendor/schemastery" }, - { "path": "../../core/tools" }, - { "path": "../fs" }, - { "path": "../../sandbox/sandbox" }, - { "path": "../../sandbox/sandbox-policy" } - ] -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4388263d21..08cdf72672 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -298,9 +298,6 @@ importers: '@deepseek-ai/dsh-tool-skill': specifier: workspace:^ version: link:../../packages/skill/tool-skill - '@deepseek-ai/dsh-tool-str-replace-editor': - specifier: workspace:^ - version: link:../../packages/fs/tool-str-replace-editor '@deepseek-ai/dsh-tool-subagent': specifier: workspace:^ version: link:../../packages/subagent/tool-subagent @@ -1242,9 +1239,6 @@ importers: '@deepseek-ai/dsh-tool-skill': specifier: workspace:^ version: link:../../skill/tool-skill - '@deepseek-ai/dsh-tool-str-replace-editor': - specifier: workspace:^ - version: link:../../fs/tool-str-replace-editor '@deepseek-ai/dsh-tool-subagent': specifier: workspace:^ version: link:../../subagent/tool-subagent @@ -5324,52 +5318,6 @@ importers: specifier: workspace:^ version: link:../../core/tools - packages/fs/tool-str-replace-editor: - dependencies: - '@deepseek-ai/schemastery': - specifier: link:../../../vendor/schemastery - version: link:../../../vendor/schemastery - devDependencies: - '@deepseek-ai/cordis': - specifier: workspace:^ - version: link:../../../vendor/cordis - '@deepseek-ai/dsh-agent': - specifier: workspace:^ - version: link:../../core/agent - '@deepseek-ai/dsh-fs': - specifier: workspace:^ - version: link:../fs - '@deepseek-ai/dsh-fs-local': - specifier: workspace:^ - version: link:../fs-local - '@deepseek-ai/dsh-fs-observation-policy': - specifier: workspace:^ - version: link:../fs-observation-policy - '@deepseek-ai/dsh-fs-sandbox': - specifier: workspace:^ - version: link:../fs-sandbox - '@deepseek-ai/dsh-llm': - specifier: workspace:^ - version: link:../../llm/llm - '@deepseek-ai/dsh-sandbox': - specifier: workspace:^ - version: link:../../sandbox/sandbox - '@deepseek-ai/dsh-sandbox-policy': - specifier: workspace:^ - version: link:../../sandbox/sandbox-policy - '@deepseek-ai/dsh-session': - specifier: workspace:^ - version: link:../../core/session - '@deepseek-ai/dsh-session-projection': - specifier: workspace:^ - version: link:../../session/session-projection - '@deepseek-ai/dsh-system-prompt': - specifier: workspace:^ - version: link:../../core/system-prompt - '@deepseek-ai/dsh-tools': - specifier: workspace:^ - version: link:../../core/tools - packages/goal/command-goal: devDependencies: '@deepseek-ai/cordis': @@ -10037,9 +9985,6 @@ importers: '@deepseek-ai/dsh-tool-skill': specifier: workspace:^ version: link:../../packages/skill/tool-skill - '@deepseek-ai/dsh-tool-str-replace-editor': - specifier: workspace:^ - version: link:../../packages/fs/tool-str-replace-editor '@deepseek-ai/dsh-tool-subagent': specifier: workspace:^ version: link:../../packages/subagent/tool-subagent diff --git a/python/sdk-runtime/package.json b/python/sdk-runtime/package.json index 97c1a6bc68..762619c5d1 100644 --- a/python/sdk-runtime/package.json +++ b/python/sdk-runtime/package.json @@ -110,7 +110,6 @@ "@deepseek-ai/dsh-tool-pwsh-persistent": "workspace:^", "@deepseek-ai/dsh-tool-ralph": "workspace:^", "@deepseek-ai/dsh-tool-skill": "workspace:^", - "@deepseek-ai/dsh-tool-str-replace-editor": "workspace:^", "@deepseek-ai/dsh-tool-subagent": "workspace:^", "@deepseek-ai/dsh-tool-subagent-control": "workspace:^", "@deepseek-ai/dsh-tool-todo": "workspace:^", diff --git a/scripts/gen-tool-catalog.ts b/scripts/gen-tool-catalog.ts index 972eadc1b8..4535e3fef6 100644 --- a/scripts/gen-tool-catalog.ts +++ b/scripts/gen-tool-catalog.ts @@ -48,7 +48,6 @@ import CordisHostRunner from '@deepseek-ai/dsh-cordis-host-runner' import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis' import * as ToolFs from '@deepseek-ai/dsh-tool-fs' import * as ToolFsSearch from '@deepseek-ai/dsh-tool-fs-search' -import * as ToolStrReplaceEditor from '@deepseek-ai/dsh-tool-str-replace-editor' import TerminalSessionService from '@deepseek-ai/dsh-terminal' import * as ToolPty from '@deepseek-ai/dsh-tool-terminal' import * as ToolGoal from '@deepseek-ai/dsh-tool-goal' @@ -298,19 +297,6 @@ const TOOL_PACKAGES: ToolPackage[] = [ note: 'One owner-isolated persistent pwsh tool, the Windows counterpart of the persistent bash tool; deployment composition supplies a pwsh-dialect PTY backend and may override the model-facing environment description.', }, - { - pkg: '@deepseek-ai/dsh-tool-str-replace-editor', - dir: 'tool-str-replace-editor', - source: 'packages/fs/tool-str-replace-editor/src/index.ts', - requires: ['ctx.tools', 'ctx.fs'], - writes: ['tool/call', 'fs/observed after view presence/absence, edit absence, or successful mutation', 'tool/result'], - async mount(ctx) { - await ctx.plugin(LocalFileSystem) - await ctx.plugin(ToolStrReplaceEditor) - }, - note: - 'Standalone view/create/unique literal replace/line insert tool over the filesystem seam; it composes with any shell or terminal API.', - }, { pkg: '@deepseek-ai/dsh-tool-fs', dir: 'tool-fs', diff --git a/scripts/smoke-python-runtime.py b/scripts/smoke-python-runtime.py index b1969ab5b9..66e9221cae 100644 --- a/scripts/smoke-python-runtime.py +++ b/scripts/smoke-python-runtime.py @@ -79,7 +79,6 @@ LEGACY_CUSTOM_DISABLED_ROWS = ( "tool-goal", "tool-ralph", "tool-skill", - "tool-str-replace-editor", "tool-subagent-control", "tool-subagent-list-agents", "tool-subagent-fork", diff --git a/snapshots/sdk/bash-tool/tool-schemas.expected.json b/snapshots/sdk/bash-tool/tool-schemas.expected.json index 716b08cd1d..dd7a2da87e 100644 --- a/snapshots/sdk/bash-tool/tool-schemas.expected.json +++ b/snapshots/sdk/bash-tool/tool-schemas.expected.json @@ -357,91 +357,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This call waits for the subagent and returns its result.", diff --git a/snapshots/sdk/persistent-tools/cordis.snapshot.yml b/snapshots/sdk/persistent-tools/cordis.snapshot.yml deleted file mode 100644 index 0579e3881d..0000000000 --- a/snapshots/sdk/persistent-tools/cordis.snapshot.yml +++ /dev/null @@ -1,15 +0,0 @@ -# Replay layer for the TypeScript SDK minimal customization patch. - -- id: llm-deepseek - name: '@deepseek-ai/dsh-llm-deepseek' - disabled: true - -- insert: - - id: llm-replay - name: '@deepseek-ai/dsh-llm-replay' - config: - providers: - - id: deepseek-official - name: DeepSeek - models: - - id: deepseek-v4-flash diff --git a/snapshots/sdk/persistent-tools/cordis.yml b/snapshots/sdk/persistent-tools/cordis.yml deleted file mode 100644 index c78756cb69..0000000000 --- a/snapshots/sdk/persistent-tools/cordis.yml +++ /dev/null @@ -1,158 +0,0 @@ -# Per-launch SDK customization: retain only persistent Bash and the string -# editor as model-facing tools, with process-local providers and an explicit -# danger-full-access / never-ask policy. - -- id: llm-deepseek - name: '@deepseek-ai/dsh-llm-deepseek' - config: - apiKeyEnv: DEEPSEEK_API_KEY - streamIdleTimeoutMs: 172800000 - models: - - id: !!js process.env.DSH_MODEL ?? 'deepseek-v4-flash' - contextWindow: !!js Number(process.env.DSH_CONTEXT_WINDOW ?? 1000000) - -- id: sandbox-policy - name: '@deepseek-ai/dsh-sandbox-policy' - config: - mode: danger-full-access - workspaceRoot: !!js process.cwd() - -- id: approval - name: '@deepseek-ai/dsh-user-approval' - config: - policy: never - -- id: permission - name: '@deepseek-ai/dsh-permission-presets' - disabled: true - -- id: bash-sandbox - name: '@deepseek-ai/dsh-bash-sandbox' - disabled: true - -- id: pwsh-sandbox - name: '@deepseek-ai/dsh-pwsh-sandbox' - disabled: true - -- id: fs-sandbox - name: '@deepseek-ai/dsh-fs-sandbox' - disabled: true - -- id: tool-bash - name: '@deepseek-ai/dsh-tool-bash' - disabled: true - -- id: tool-pwsh - name: '@deepseek-ai/dsh-tool-pwsh' - disabled: true - -- id: agent-instructions - name: '@deepseek-ai/dsh-agent-instructions' - disabled: true - -- id: skill - name: '@deepseek-ai/dsh-skill' - disabled: true - -- id: skill-filesystem - name: '@deepseek-ai/dsh-skill-filesystem' - disabled: true - -- id: tool-skill - name: '@deepseek-ai/dsh-tool-skill' - disabled: true - -- id: tool-jobs - name: '@deepseek-ai/dsh-tool-jobs' - disabled: true - -- id: tool-fs - name: '@deepseek-ai/dsh-tool-fs' - disabled: true - -- id: tool-fs-search - name: '@deepseek-ai/dsh-tool-fs-search' - disabled: true - -- id: tool-subagent-control - name: '@deepseek-ai/dsh-tool-subagent-control' - disabled: true - -- id: tool-subagent-list-agents - name: '@deepseek-ai/dsh-tool-subagent-control/list-agents' - disabled: true - -- id: tool-subagent - name: '@deepseek-ai/dsh-tool-subagent' - disabled: true - -- id: tool-subagent-fork - name: '@deepseek-ai/dsh-tool-subagent' - disabled: true - -- id: tool-workflow - name: '@deepseek-ai/dsh-tool-workflow' - disabled: true - -- id: tool-todo - name: '@deepseek-ai/dsh-tool-todo' - disabled: true - -- id: tool-goal - name: '@deepseek-ai/dsh-tool-goal' - disabled: true - -- id: tool-ralph - name: '@deepseek-ai/dsh-tool-ralph' - disabled: true - -- id: tool-web - name: '@deepseek-ai/dsh-tool-web' - disabled: true - -- id: plan-mode - name: '@deepseek-ai/dsh-plan-mode' - disabled: true - -- id: system-prompt - name: '@deepseek-ai/dsh-system-prompt' - config: - includeHarnessIdentity: false - persona: !!js process.env.DSH_SYSTEM_PROMPT ?? 'You are a helpful software engineer assistant.' - -- id: session-persistence-jsonl - name: '@deepseek-ai/dsh-session-persistence-jsonl' - config: - root: !!js dshHomePath('sessions') - compression: none - -- insert: - - id: bash-local - name: '@deepseek-ai/dsh-bash-local' - - - id: fs-local - name: '@deepseek-ai/dsh-fs-local' - config: - cwd: !!js process.cwd() - - - id: terminal - name: '@deepseek-ai/dsh-terminal' - - - id: terminal-bash - name: '@deepseek-ai/dsh-terminal-bash' - config: - timeoutMs: 300000 - - - id: persistent-bash - name: '@deepseek-ai/dsh-tool-bash-persistent' - config: - timeoutMs: 300000 - description: |- - Run commands in a bash shell - * When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped. - * You don't have access to the internet via this tool. - * You do have access to a mirror of common linux and python packages via apt and pip. - * State is persistent across command calls and discussions with the user. - * To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'. - * Please avoid commands that may produce a very large amount of output. - * Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background. diff --git a/snapshots/sdk/persistent-tools/notifications.expected.jsonl b/snapshots/sdk/persistent-tools/notifications.expected.jsonl deleted file mode 100644 index fe4872b440..0000000000 --- a/snapshots/sdk/persistent-tools/notifications.expected.jsonl +++ /dev/null @@ -1,89 +0,0 @@ -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":0,"time":0,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Prove that bash state persists. After the persistence checks, run exactly `{ sleep 0.1; echo delayed; } | cat` as its own bash call and observe `delayed`. Then create {{cwd}}/note.txt with a tab-indented line, view it, replace that literal tab-indented line, and make the persistent shell exit with code 9."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}}}} -{"method":"session.status","params":{"sessionId":"{{sessionId}}","status":"running"}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/start","seq":1,"time":0,"data":{"turn":1}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":2,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":4,"time":0,"data":{"content":[{"type":"text","text":"Prove that bash state persists. After the persistence checks, run exactly `{ sleep 0.1; echo delayed; } | cat` as its own bash call and observe `delayed`. Then create {{cwd}}/note.txt with a tab-indented line, view it, replace that literal tab-indented line, and make the persistent shell exit with code 9."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":5,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"session/title","seq":6,"time":0,"data":{"title":"Prove that bash state persists.","messageSeqs":[4],"source":{"kind":"fallback"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":7,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/context","seq":8,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":9,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":10,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-1","name":"bash","argumentsDelta":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":12,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":13,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":14,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":15,"time":0,"data":{"turn":1,"step":1,"callId":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":16,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"bash-1"},"content":[{"type":"tool-result","toolCallId":"bash-1","content":[{"type":"text","text":"COUNT=1 CWD=/tmp"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":17,"time":0,"data":{"turn":1,"step":1}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":18,"time":0,"data":{"turn":1,"step":2}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":19,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":20,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-2","name":"bash","argumentsDelta":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":21,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":22,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":23,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":24,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":25,"time":0,"data":{"turn":1,"step":2,"callId":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":26,"time":0,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"bash-2"},"content":[{"type":"tool-result","toolCallId":"bash-2","content":[{"type":"text","text":"COUNT=2 CWD=/tmp"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[25],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":27,"time":0,"data":{"turn":1,"step":2}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":28,"time":0,"data":{"turn":1,"step":3}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":29,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":30,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-delayed-pipeline","name":"bash","argumentsDelta":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":31,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":32,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":33,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":34,"time":0,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[29,30,31,32,33],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":35,"time":0,"data":{"turn":1,"step":3,"callId":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":36,"time":0,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"bash-delayed-pipeline"},"content":[{"type":"tool-result","toolCallId":"bash-delayed-pipeline","content":[{"type":"text","text":"delayed"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[35],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":37,"time":0,"data":{"turn":1,"step":3}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":38,"time":0,"data":{"turn":1,"step":4}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":39,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":40,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-create","name":"str_replace_editor","argumentsDelta":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":41,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":42,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":43,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":44,"time":0,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[39,40,41,42,43],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":45,"time":0,"data":{"turn":1,"step":4,"callId":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":46,"time":0,"data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"editor-create"},"content":[{"type":"tool-result","toolCallId":"editor-create","content":[{"type":"text","text":"New file created successfully at: {{cwd}}/note.txt"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[45],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":47,"time":0,"data":{"turn":1,"step":4}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":48,"time":0,"data":{"turn":1,"step":5}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":49,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":50,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-view","name":"str_replace_editor","argumentsDelta":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":null,\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":51,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":null,\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":52,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":53,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":54,"time":0,"data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":null,\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[49,50,51,52,53],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":55,"time":0,"data":{"turn":1,"step":5,"callId":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":null,\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":56,"time":0,"data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"editor-view"},"content":[{"type":"tool-result","toolCallId":"editor-view","content":[{"type":"text","text":"Here's the content of {{cwd}}/note.txt with line numbers (which has a total of 3 lines):\n 1 target:\n 2 \told\n 3 \n"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[55],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":57,"time":0,"data":{"turn":1,"step":5}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":58,"time":0,"data":{"turn":1,"step":6}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":59,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":60,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-replace","name":"str_replace_editor","argumentsDelta":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\",\"file_text\":null,\"insert_line\":null,\"view_range\":null}"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":61,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\",\"file_text\":null,\"insert_line\":null,\"view_range\":null}"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":62,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":63,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":64,"time":0,"data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\",\"file_text\":null,\"insert_line\":null,\"view_range\":null}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[59,60,61,62,63],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":65,"time":0,"data":{"turn":1,"step":6,"callId":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\",\"file_text\":null,\"insert_line\":null,\"view_range\":null}"}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":66,"time":0,"data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"editor-replace"},"content":[{"type":"tool-result","toolCallId":"editor-replace","content":[{"type":"text","text":"The file {{cwd}}/note.txt has been edited successfully."}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[65],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":67,"time":0,"data":{"turn":1,"step":6}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":68,"time":0,"data":{"turn":1,"step":7}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":69,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":70,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-exit","name":"bash","argumentsDelta":"{\"command\":\"exit 9\"}"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":71,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":72,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":73,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":74,"time":0,"data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[69,70,71,72,73],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":75,"time":0,"data":{"turn":1,"step":7,"callId":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":76,"time":0,"data":{"turn":1,"step":7,"message":{"source":{"kind":"tool","callId":"bash-exit"},"content":[{"type":"tool-result","toolCallId":"bash-exit","content":[{"type":"text","text":"exit\n[shell exited: code 9]\nThe persistent bash shell was reset; the next bash call starts from the workspace with a fresh current directory and environment."}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[75],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":77,"time":0,"data":{"turn":1,"step":7}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":78,"time":0,"data":{"turn":1,"step":8}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":79,"time":0,"data":{"turn":1,"step":8,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":80,"time":0,"data":{"turn":1,"step":8,"chunk":{"type":"text-delta","index":0,"text":"PERSISTENT_TOOLS_OK"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":81,"time":0,"data":{"turn":1,"step":8,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PERSISTENT_TOOLS_OK"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":82,"time":0,"data":{"turn":1,"step":8,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":83,"time":0,"data":{"turn":1,"step":8,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":84,"time":0,"data":{"turn":1,"step":8,"message":{"role":"assistant","content":[{"type":"text","text":"PERSISTENT_TOOLS_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[79,80,81,82,83],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":85,"time":0,"data":{"turn":1,"step":8}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":86,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}}} -{"method":"session.status","params":{"sessionId":"{{sessionId}}","status":"idle"}} diff --git a/snapshots/sdk/persistent-tools/result.expected.json b/snapshots/sdk/persistent-tools/result.expected.json deleted file mode 100644 index 53a1342f7d..0000000000 --- a/snapshots/sdk/persistent-tools/result.expected.json +++ /dev/null @@ -1 +0,0 @@ -{"sessionId":"{{sessionId}}","finalResponse":"PERSISTENT_TOOLS_OK"} diff --git a/snapshots/sdk/persistent-tools/session.jsonl b/snapshots/sdk/persistent-tools/session.jsonl deleted file mode 100644 index b382309331..0000000000 --- a/snapshots/sdk/persistent-tools/session.jsonl +++ /dev/null @@ -1,88 +0,0 @@ -{"type":"session","version":0,"id":"{{session:1}}","createdAt":1785331618309,"cwd":"{{cwd}}","delegationDepth":0} -{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Prove that bash state persists. After the persistence checks, run exactly `{ sleep 0.1; echo delayed; } | cat` as its own bash call and observe `delayed`. Then create {{cwd}}/note.txt with a tab-indented line, view it, replace that literal tab-indented line, and make the persistent shell exit with code 9."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}} -{"type":"turn/start","data":{"turn":1}} -{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} -{"type":"step/start","data":{"turn":1,"step":1}} -{"type":"user/message","data":{"content":[{"type":"text","text":"Prove that bash state persists. After the persistence checks, run exactly `{ sleep 0.1; echo delayed; } | cat` as its own bash call and observe `delayed`. Then create {{cwd}}/note.txt with a tab-indented line, view it, replace that literal tab-indented line, and make the persistent shell exit with code 9."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:2}}"},"surfaceOp":"append"} -{"type":"session/title","data":{"title":"Prove that bash state persists.","messageSeqs":[4],"source":{"kind":"fallback"}}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} -{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} -{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-1","name":"bash","argumentsDelta":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}} -{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}} -{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} -{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"} -{"type":"tool/call","data":{"turn":1,"step":1,"callId":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}} -{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"bash-1"},"content":[{"type":"tool-result","toolCallId":"bash-1","content":[{"type":"text","text":"COUNT=1 CWD=/tmp"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"} -{"type":"step/end","data":{"turn":1,"step":1}} -{"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} -{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-2","name":"bash","argumentsDelta":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}} -{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}} -{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} -{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"} -{"type":"tool/call","data":{"turn":1,"step":2,"callId":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}} -{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"bash-2"},"content":[{"type":"tool-result","toolCallId":"bash-2","content":[{"type":"text","text":"COUNT=2 CWD=/tmp"}],"isError":false}],"role":"user","id":"{{message:6}}"}},"sourceEventSeqs":[25],"surfaceOp":"append"} -{"type":"step/end","data":{"turn":1,"step":2}} -{"type":"step/start","data":{"turn":1,"step":3}} -{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} -{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-delayed-pipeline","name":"bash","argumentsDelta":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}}} -{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}}}} -{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} -{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[29,30,31,32,33],"surfaceOp":"append"} -{"type":"tool/call","data":{"turn":1,"step":3,"callId":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}} -{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"bash-delayed-pipeline"},"content":[{"type":"tool-result","toolCallId":"bash-delayed-pipeline","content":[{"type":"text","text":"delayed"}],"isError":false}],"role":"user","id":"{{message:8}}"}},"sourceEventSeqs":[35],"surfaceOp":"append"} -{"type":"step/end","data":{"turn":1,"step":3}} -{"type":"step/start","data":{"turn":1,"step":4}} -{"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} -{"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-create","name":"str_replace_editor","argumentsDelta":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}} -{"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}}} -{"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} -{"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[39,40,41,42,43],"surfaceOp":"append"} -{"type":"tool/call","data":{"turn":1,"step":4,"callId":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}} -{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"editor-create"},"content":[{"type":"tool-result","toolCallId":"editor-create","content":[{"type":"text","text":"New file created successfully at: {{cwd}}/note.txt"}],"isError":false}],"role":"user","id":"{{message:10}}"}},"sourceEventSeqs":[45],"surfaceOp":"append"} -{"type":"step/end","data":{"turn":1,"step":4}} -{"type":"step/start","data":{"turn":1,"step":5}} -{"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} -{"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-view","name":"str_replace_editor","argumentsDelta":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":null,\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}} -{"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":null,\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}}} -{"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} -{"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":null,\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[49,50,51,52,53],"surfaceOp":"append"} -{"type":"tool/call","data":{"turn":1,"step":5,"callId":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":null,\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}} -{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"editor-view"},"content":[{"type":"tool-result","toolCallId":"editor-view","content":[{"type":"text","text":"Here's the content of {{cwd}}/note.txt with line numbers (which has a total of 3 lines):\n 1 target:\n 2 \told\n 3 \n"}],"isError":false}],"role":"user","id":"{{message:12}}"}},"sourceEventSeqs":[55],"surfaceOp":"append"} -{"type":"step/end","data":{"turn":1,"step":5}} -{"type":"step/start","data":{"turn":1,"step":6}} -{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} -{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-replace","name":"str_replace_editor","argumentsDelta":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\",\"file_text\":null,\"insert_line\":null,\"view_range\":null}"}}} -{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\",\"file_text\":null,\"insert_line\":null,\"view_range\":null}"}}}} -{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} -{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\",\"file_text\":null,\"insert_line\":null,\"view_range\":null}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:13}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[59,60,61,62,63],"surfaceOp":"append"} -{"type":"tool/call","data":{"turn":1,"step":6,"callId":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\",\"file_text\":null,\"insert_line\":null,\"view_range\":null}"}} -{"type":"tool/result","data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"editor-replace"},"content":[{"type":"tool-result","toolCallId":"editor-replace","content":[{"type":"text","text":"The file {{cwd}}/note.txt has been edited successfully."}],"isError":false}],"role":"user","id":"{{message:14}}"}},"sourceEventSeqs":[65],"surfaceOp":"append"} -{"type":"step/end","data":{"turn":1,"step":6}} -{"type":"step/start","data":{"turn":1,"step":7}} -{"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} -{"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-exit","name":"bash","argumentsDelta":"{\"command\":\"exit 9\"}"}}} -{"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}}}} -{"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} -{"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:15}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[69,70,71,72,73],"surfaceOp":"append"} -{"type":"tool/call","data":{"turn":1,"step":7,"callId":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}} -{"type":"tool/result","data":{"turn":1,"step":7,"message":{"source":{"kind":"tool","callId":"bash-exit"},"content":[{"type":"tool-result","toolCallId":"bash-exit","content":[{"type":"text","text":"exit\n[shell exited: code 9]\nThe persistent bash shell was reset; the next bash call starts from the workspace with a fresh current directory and environment."}],"isError":false}],"role":"user","id":"{{message:16}}"}},"sourceEventSeqs":[75],"surfaceOp":"append"} -{"type":"step/end","data":{"turn":1,"step":7}} -{"type":"step/start","data":{"turn":1,"step":8}} -{"type":"assistant/chunk","data":{"turn":1,"step":8,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} -{"type":"assistant/chunk","data":{"turn":1,"step":8,"chunk":{"type":"text-delta","index":0,"text":"PERSISTENT_TOOLS_OK"}}} -{"type":"assistant/chunk","data":{"turn":1,"step":8,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PERSISTENT_TOOLS_OK"}}}} -{"type":"assistant/chunk","data":{"turn":1,"step":8,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} -{"type":"assistant/chunk","data":{"turn":1,"step":8,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","data":{"turn":1,"step":8,"message":{"role":"assistant","content":[{"type":"text","text":"PERSISTENT_TOOLS_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:17}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[79,80,81,82,83],"surfaceOp":"append"} -{"type":"step/end","data":{"turn":1,"step":8}} -{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/sdk/persistent-tools/snapshot.yml b/snapshots/sdk/persistent-tools/snapshot.yml deleted file mode 100644 index b4876f0b17..0000000000 --- a/snapshots/sdk/persistent-tools/snapshot.yml +++ /dev/null @@ -1,10 +0,0 @@ -version: 1 -scenario: persistent-tools -profile: sdk -composition: sdk-minimal -recording: live -header: - class: sdk-minimal - pin: true -workspace: - final: true diff --git a/snapshots/sdk/persistent-tools/system-prompt.expected.md b/snapshots/sdk/persistent-tools/system-prompt.expected.md deleted file mode 100644 index 3066e65687..0000000000 --- a/snapshots/sdk/persistent-tools/system-prompt.expected.md +++ /dev/null @@ -1 +0,0 @@ -You are the environment-selected minimal software engineer. diff --git a/snapshots/sdk/persistent-tools/tool-schemas.expected.json b/snapshots/sdk/persistent-tools/tool-schemas.expected.json deleted file mode 100644 index 73234c5e49..0000000000 --- a/snapshots/sdk/persistent-tools/tool-schemas.expected.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "initial": [ - { - "name": "bash", - "description": "Run commands in a bash shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* You don't have access to the internet via this tool.\n* You do have access to a mirror of common linux and python packages via apt and pip.\n* State is persistent across command calls and discussions with the user.\n* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The bash command to run. Relative path is preferred in the command." - } - }, - "required": [ - "command" - ] - } - }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - } - ], - "changes": [] -} diff --git a/snapshots/sdk/persistent-tools/workspace.expected/note.txt b/snapshots/sdk/persistent-tools/workspace.expected/note.txt deleted file mode 100644 index 86b943efe7..0000000000 --- a/snapshots/sdk/persistent-tools/workspace.expected/note.txt +++ /dev/null @@ -1,2 +0,0 @@ -target: - new diff --git a/snapshots/sdk/sdk.snapshot.ts b/snapshots/sdk/sdk.snapshot.ts index 50ececf76a..3095c36575 100644 --- a/snapshots/sdk/sdk.snapshot.ts +++ b/snapshots/sdk/sdk.snapshot.ts @@ -54,16 +54,6 @@ import { const corpusRoot = fileURLToPath(new URL('../', import.meta.url)) -const MINIMAL_SYSTEM_PROMPT = 'You are the environment-selected minimal software engineer.' -const MINIMAL_BASH_DESCRIPTION = `Run commands in a bash shell -* When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped. -* You don't have access to the internet via this tool. -* You do have access to a mirror of common linux and python packages via apt and pip. -* State is persistent across command calls and discussions with the user. -* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'. -* Please avoid commands that may produce a very large amount of output. -* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.` - const mode = process.env.DSH_SNAPSHOT ?? 'replay' const recording = mode === 'record' const refreshing = mode === 'refresh' @@ -112,16 +102,6 @@ const SDK_ASSERTIONS: Readonly> = { 'subagent-dsh-sdk-diagnostic': { environment: { DSH_TEST_CHILD_PATCH: dshSdkDiagnosticChildPatch }, }, - 'persistent-tools': { - environment: { DSH_SYSTEM_PROMPT: MINIMAL_SYSTEM_PROMPT }, - expectedTools: { bash: ['command'], str_replace_editor: ['command', 'path'] }, - expectedSystem: MINIMAL_SYSTEM_PROMPT, - expectedToolDescriptions: { bash: MINIMAL_BASH_DESCRIPTION }, - runtimeContext: { - includes: ['Current DSH file policy: danger-full-access', 'Approval prompts are disabled in this session'], - excludes: ['workspace-write'], - }, - }, 'subagent-dsh-sdk-dynamic-route': { environment: { DSH_TEST_PARENT_PROVIDER: 'deepseek-official' }, dshSdkChild: { diff --git a/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json b/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json index 87cbb941a1..1531215c7f 100644 --- a/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json @@ -357,91 +357,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json b/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json index 87cbb941a1..1531215c7f 100644 --- a/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json @@ -357,91 +357,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json b/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json index ee4ad0e6b6..ba63548517 100644 --- a/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json +++ b/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json @@ -357,91 +357,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json index c21ad8a0f5..e7899283b4 100644 --- a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json @@ -357,91 +357,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json index 9cc82c6fb5..73c4ae509a 100644 --- a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json +++ b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json @@ -374,91 +374,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This call waits for the subagent and returns its result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and this provider's route defaults. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", diff --git a/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json b/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json index 87cbb941a1..1531215c7f 100644 --- a/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json @@ -357,91 +357,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json b/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json index 87cbb941a1..1531215c7f 100644 --- a/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json @@ -357,91 +357,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/sdk/text-turn/tool-schemas.expected.json b/snapshots/sdk/text-turn/tool-schemas.expected.json index 716b08cd1d..dd7a2da87e 100644 --- a/snapshots/sdk/text-turn/tool-schemas.expected.json +++ b/snapshots/sdk/text-turn/tool-schemas.expected.json @@ -357,91 +357,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This call waits for the subagent and returns its result.", diff --git a/snapshots/session/agent-instructions/tool-schemas.expected.json b/snapshots/session/agent-instructions/tool-schemas.expected.json index a3e1ee5238..40661b4472 100644 --- a/snapshots/session/agent-instructions/tool-schemas.expected.json +++ b/snapshots/session/agent-instructions/tool-schemas.expected.json @@ -357,91 +357,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", @@ -1074,91 +989,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/both-mode-turn/system-prompt.expected.md b/snapshots/session/both-mode-turn/system-prompt.expected.md index 22f4a66ba2..5c3c43a2e7 100644 --- a/snapshots/session/both-mode-turn/system-prompt.expected.md +++ b/snapshots/session/both-mode-turn/system-prompt.expected.md @@ -173,23 +173,6 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** Custom editing tool for viewing, creating and editing files * State is persistent across command calls and discussions with the user * If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep * The `create` command cannot be used if the specified `path` already exists as a file * If a `command` generates a long output, it will be truncated and marked with `` * A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match Notes for using the `str_replace` command: * The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces! * If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique * The `new_str` parameter should contain the edited lines that should replace the `old_str` */ - str_replace_editor: { - /** The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`. */ - command: "view" | "create" | "str_replace" | "insert"; - /** Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`. */ - path: string; - /** Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter. */ - file_text?: string | null; - /** Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter. */ - insert_line?: number | null; - /** Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter. */ - new_str?: string | null; - /** Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter. */ - old_str?: string | null; - /** Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file. */ - view_range?: number[] | null; - } & Record; /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result. */ subagent: { /** A short (3-5 word) description of the delegated task, for display. */ @@ -459,7 +442,6 @@ interface ToolOutputMap { }; content: string; }; - str_replace_editor: string; subagent: { kind: "background"; jobId: string; diff --git a/snapshots/session/both-mode-turn/tool-schemas.expected.json b/snapshots/session/both-mode-turn/tool-schemas.expected.json index b1a1179d18..fcaa91628e 100644 --- a/snapshots/session/both-mode-turn/tool-schemas.expected.json +++ b/snapshots/session/both-mode-turn/tool-schemas.expected.json @@ -378,91 +378,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/compaction-recovery/tool-schemas.expected.json b/snapshots/session/compaction-recovery/tool-schemas.expected.json index a3e1ee5238..40661b4472 100644 --- a/snapshots/session/compaction-recovery/tool-schemas.expected.json +++ b/snapshots/session/compaction-recovery/tool-schemas.expected.json @@ -357,91 +357,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", @@ -1074,91 +989,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/cordis-inspect-jsdoc/system-prompt.expected.md b/snapshots/session/cordis-inspect-jsdoc/system-prompt.expected.md index 8f0f468911..6bd67f58ab 100644 --- a/snapshots/session/cordis-inspect-jsdoc/system-prompt.expected.md +++ b/snapshots/session/cordis-inspect-jsdoc/system-prompt.expected.md @@ -340,23 +340,6 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** Custom editing tool for viewing, creating and editing files * State is persistent across command calls and discussions with the user * If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep * The `create` command cannot be used if the specified `path` already exists as a file * If a `command` generates a long output, it will be truncated and marked with `` * A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match Notes for using the `str_replace` command: * The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces! * If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique * The `new_str` parameter should contain the edited lines that should replace the `old_str` */ - str_replace_editor: { - /** The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`. */ - command: "view" | "create" | "str_replace" | "insert"; - /** Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`. */ - path: string; - /** Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter. */ - file_text?: string | null; - /** Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter. */ - insert_line?: number | null; - /** Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter. */ - new_str?: string | null; - /** Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter. */ - old_str?: string | null; - /** Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file. */ - view_range?: number[] | null; - } & Record; /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result. */ subagent: { /** A short (3-5 word) description of the delegated task, for display. */ @@ -645,7 +628,6 @@ interface ToolOutputMap { }; content: string; }; - str_replace_editor: string; subagent: { kind: "background"; jobId: string; diff --git a/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json b/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json index fbb01942ab..fe1f6ed30a 100644 --- a/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json +++ b/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json @@ -575,91 +575,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/fs-glob-sampling/tool-schemas.expected.json b/snapshots/session/fs-glob-sampling/tool-schemas.expected.json index 0dd86f7f77..5d3cd77ac0 100644 --- a/snapshots/session/fs-glob-sampling/tool-schemas.expected.json +++ b/snapshots/session/fs-glob-sampling/tool-schemas.expected.json @@ -261,91 +261,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/lsp-definition/tool-schemas.expected.json b/snapshots/session/lsp-definition/tool-schemas.expected.json index def66aefbd..393fe158eb 100644 --- a/snapshots/session/lsp-definition/tool-schemas.expected.json +++ b/snapshots/session/lsp-definition/tool-schemas.expected.json @@ -394,91 +394,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/product-subagent-both/tool-schemas.expected.json b/snapshots/session/product-subagent-both/tool-schemas.expected.json index 232c2778f2..a20d2a132f 100644 --- a/snapshots/session/product-subagent-both/tool-schemas.expected.json +++ b/snapshots/session/product-subagent-both/tool-schemas.expected.json @@ -357,91 +357,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/product-subagent-codex/tool-schemas.expected.json b/snapshots/session/product-subagent-codex/tool-schemas.expected.json index e8877690ec..f28699d048 100644 --- a/snapshots/session/product-subagent-codex/tool-schemas.expected.json +++ b/snapshots/session/product-subagent-codex/tool-schemas.expected.json @@ -357,91 +357,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json b/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json index 8303bc5845..9ba36a6336 100644 --- a/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json +++ b/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json @@ -357,91 +357,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/ptc-python-turn/system-prompt.expected.md b/snapshots/session/ptc-python-turn/system-prompt.expected.md index 375b87b4e0..f9eaff62cb 100644 --- a/snapshots/session/ptc-python-turn/system-prompt.expected.md +++ b/snapshots/session/ptc-python-turn/system-prompt.expected.md @@ -366,23 +366,6 @@ class SkillOutput(TypedDict): resourceBase: NotRequired[SkillOutputResourceBase1 | SkillOutputResourceBase2 | SkillOutputResourceBase3] content: str -class StrReplaceEditorArgs(TypedDict): - # The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`. - command: Literal["view", "create", "str_replace", "insert"] - # Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`. - path: str - # Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter. - file_text: NotRequired[str | None] - # Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter. - insert_line: NotRequired[int | None] - # Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter. - new_str: NotRequired[str | None] - # Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter. - old_str: NotRequired[str | None] - # Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file. - view_range: NotRequired[list[int] | None] - # Additional keys beyond those declared are allowed. - class SubagentArgs(TypedDict): # A short (3-5 word) description of the delegated task, for display. description: str @@ -607,8 +590,6 @@ class Tools(Protocol): """Send a message to a direct continuable child by its agent id. If you are a resident continuable child, you may also target your direct parent. If the target is still working, the message steers its nearest step; if it is idle, the message starts a turn. This call returns no answer from the agent — only confirmation that the message was delivered. A failure means the message was NOT delivered.""" async def skill(self, args: SkillArgs) -> SkillOutput: """Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.""" - async def str_replace_editor(self, args: StrReplaceEditorArgs) -> str: - """Custom editing tool for viewing, creating and editing files * State is persistent across command calls and discussions with the user * If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep * The `create` command cannot be used if the specified `path` already exists as a file * If a `command` generates a long output, it will be truncated and marked with `` * A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match Notes for using the `str_replace` command: * The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces! * If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique * The `new_str` parameter should contain the edited lines that should replace the `old_str`""" async def subagent(self, args: SubagentArgs) -> SubagentOutput1 | SubagentOutput2 | SubagentOutput3: """Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.""" async def subagent_fork(self, args: SubagentForkArgs) -> SubagentForkOutput1 | SubagentForkOutput2 | SubagentForkOutput3: diff --git a/snapshots/session/ptc-read-image/system-prompt.expected.md b/snapshots/session/ptc-read-image/system-prompt.expected.md index 004a681296..672242d92e 100644 --- a/snapshots/session/ptc-read-image/system-prompt.expected.md +++ b/snapshots/session/ptc-read-image/system-prompt.expected.md @@ -175,23 +175,6 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** Custom editing tool for viewing, creating and editing files * State is persistent across command calls and discussions with the user * If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep * The `create` command cannot be used if the specified `path` already exists as a file * If a `command` generates a long output, it will be truncated and marked with `` * A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match Notes for using the `str_replace` command: * The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces! * If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique * The `new_str` parameter should contain the edited lines that should replace the `old_str` */ - str_replace_editor: { - /** The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`. */ - command: "view" | "create" | "str_replace" | "insert"; - /** Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`. */ - path: string; - /** Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter. */ - file_text?: string | null; - /** Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter. */ - insert_line?: number | null; - /** Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter. */ - new_str?: string | null; - /** Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter. */ - old_str?: string | null; - /** Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file. */ - view_range?: number[] | null; - } & Record; /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result. */ subagent: { /** A short (3-5 word) description of the delegated task, for display. */ @@ -461,7 +444,6 @@ interface ToolOutputMap { }; content: string; }; - str_replace_editor: string; subagent: { kind: "background"; jobId: string; diff --git a/snapshots/session/ptc-turn/system-prompt.expected.md b/snapshots/session/ptc-turn/system-prompt.expected.md index 5c43f1c4db..dd648445e1 100644 --- a/snapshots/session/ptc-turn/system-prompt.expected.md +++ b/snapshots/session/ptc-turn/system-prompt.expected.md @@ -175,23 +175,6 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** Custom editing tool for viewing, creating and editing files * State is persistent across command calls and discussions with the user * If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep * The `create` command cannot be used if the specified `path` already exists as a file * If a `command` generates a long output, it will be truncated and marked with `` * A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match Notes for using the `str_replace` command: * The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces! * If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique * The `new_str` parameter should contain the edited lines that should replace the `old_str` */ - str_replace_editor: { - /** The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`. */ - command: "view" | "create" | "str_replace" | "insert"; - /** Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`. */ - path: string; - /** Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter. */ - file_text?: string | null; - /** Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter. */ - insert_line?: number | null; - /** Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter. */ - new_str?: string | null; - /** Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter. */ - old_str?: string | null; - /** Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file. */ - view_range?: number[] | null; - } & Record; /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result. */ subagent: { /** A short (3-5 word) description of the delegated task, for display. */ @@ -461,7 +444,6 @@ interface ToolOutputMap { }; content: string; }; - str_replace_editor: string; subagent: { kind: "background"; jobId: string; diff --git a/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json b/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json index fd18c5805a..3c9ef95371 100644 --- a/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json +++ b/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json @@ -357,91 +357,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/ralph-loop/tool-schemas.1.expected.json b/snapshots/session/ralph-loop/tool-schemas.1.expected.json index 08dce4ee40..1feee5af52 100644 --- a/snapshots/session/ralph-loop/tool-schemas.1.expected.json +++ b/snapshots/session/ralph-loop/tool-schemas.1.expected.json @@ -357,91 +357,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "structured_output", "description": "Report your final structured result. Call this exactly once, when your answer is complete; the arguments must match this tool's parameter schema exactly.", diff --git a/snapshots/session/ralph-loop/tool-schemas.2.expected.json b/snapshots/session/ralph-loop/tool-schemas.2.expected.json index 08dce4ee40..1feee5af52 100644 --- a/snapshots/session/ralph-loop/tool-schemas.2.expected.json +++ b/snapshots/session/ralph-loop/tool-schemas.2.expected.json @@ -357,91 +357,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "structured_output", "description": "Report your final structured result. Call this exactly once, when your answer is complete; the arguments must match this tool's parameter schema exactly.", diff --git a/snapshots/session/session-query-spill/tool-schemas.expected.json b/snapshots/session/session-query-spill/tool-schemas.expected.json index 5d8b78fc63..e59600ed46 100644 --- a/snapshots/session/session-query-spill/tool-schemas.expected.json +++ b/snapshots/session/session-query-spill/tool-schemas.expected.json @@ -561,91 +561,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json b/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json index cec2b16ef8..cb0c104048 100644 --- a/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json +++ b/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json @@ -357,91 +357,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json b/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json index ed2d4fa46f..0cbbda81fe 100644 --- a/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json +++ b/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json @@ -420,91 +420,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/text-turn/tool-schemas.expected.json b/snapshots/session/text-turn/tool-schemas.expected.json index 87cbb941a1..1531215c7f 100644 --- a/snapshots/session/text-turn/tool-schemas.expected.json +++ b/snapshots/session/text-turn/tool-schemas.expected.json @@ -357,91 +357,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/web-fetch/tool-schemas.expected.json b/snapshots/session/web-fetch/tool-schemas.expected.json index aa3f6cabd5..ed38c64b2b 100644 --- a/snapshots/session/web-fetch/tool-schemas.expected.json +++ b/snapshots/session/web-fetch/tool-schemas.expected.json @@ -357,91 +357,6 @@ ] } }, - { - "name": "str_replace_editor", - "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", - "enum": [ - "view", - "create", - "str_replace", - "insert" - ] - }, - "path": { - "type": "string", - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." - }, - "file_text": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "insert_line": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "new_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." - }, - "old_str": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." - }, - "view_range": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "integer" - } - }, - { - "type": "null" - } - ], - "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." - } - }, - "required": [ - "command", - "path" - ] - } - }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/tsconfig.base.json b/tsconfig.base.json index 68aab92144..ea2868ce93 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -394,7 +394,6 @@ "@deepseek-ai/dsh-tool-ralph": ["./packages/workflow/tool-ralph/src"], "@deepseek-ai/dsh-tool-session-query": ["./packages/session-query/tool-session-query/src"], "@deepseek-ai/dsh-tool-skill": ["./packages/skill/tool-skill/src"], - "@deepseek-ai/dsh-tool-str-replace-editor": ["./packages/fs/tool-str-replace-editor/src"], "@deepseek-ai/dsh-tool-subagent": ["./packages/subagent/tool-subagent/src"], "@deepseek-ai/dsh-tool-subagent/invariant": ["./packages/subagent/tool-subagent/src/invariant.ts"], "@deepseek-ai/dsh-tool-subagent-control": ["./packages/subagent/tool-subagent-control/src"], diff --git a/tsconfig.host.json b/tsconfig.host.json index d2a033266d..7f79fe8c98 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -248,7 +248,6 @@ { "path": "./packages/fs/fs-sandbox" }, { "path": "./packages/fs/tool-fs" }, { "path": "./packages/fs/tool-fs-search" }, - { "path": "./packages/fs/tool-str-replace-editor" }, { "path": "./packages/compaction/compaction" }, { "path": "./packages/compaction/compaction-basic" }, { "path": "./packages/compaction/command-compact" }, From 8d06ebb5ac9aa8424c1eaf6684feae4f2bd9f625 Mon Sep 17 00:00:00 2001 From: fz Date: Thu, 3 Sep 2026 21:05:38 +0800 Subject: [PATCH 03/58] Revert "refactor: remove str_replace_editor" This reverts commit d12b1146baa326e6facd3b10f81ae11db35ffd86. --- ...client-derived-tool-presentation.i18n.yaml | 4 +- ...-08-23-client-derived-tool-presentation.md | 7 + ...-23-client-derived-tool-presentation.zh.md | 7 + ...9-filesystem-absence-observation.i18n.yaml | 4 +- ...26-08-09-filesystem-absence-observation.md | 2 +- ...08-09-filesystem-absence-observation.zh.md | 2 +- ...rsistent-bash-str-replace-editor.i18n.yaml | 4 +- ...7-29-persistent-bash-str-replace-editor.md | 8 +- ...9-persistent-bash-str-replace-editor.zh.md | 8 +- ...31-even-out-shipped-tool-rosters.i18n.yaml | 4 +- ...026-07-31-even-out-shipped-tool-rosters.md | 4 +- ...-07-31-even-out-shipped-tool-rosters.zh.md | 4 +- ...6-07-31-web-workspace-file-links.i18n.yaml | 4 +- .../2026-07-31-web-workspace-file-links.md | 2 +- .../2026-07-31-web-workspace-file-links.zh.md | 2 +- ...10-default-presets-single-editor.i18n.yaml | 4 +- ...026-08-10-default-presets-single-editor.md | 8 +- ...-08-10-default-presets-single-editor.zh.md | 8 +- ...l-profiles-persistent-shell-only.i18n.yaml | 4 +- ...-minimal-profiles-persistent-shell-only.md | 10 +- ...nimal-profiles-persistent-shell-only.zh.md | 10 +- ...-09-03-remove-str-replace-editor.i18n.yaml | 6 - .../2026-09-03-remove-str-replace-editor.md | 33 - ...2026-09-03-remove-str-replace-editor.zh.md | 33 - apps/cli/composition.md | 3 + apps/cli/package.json | 1 + apps/cli/tests/web-agent-presets.e2e.ts | 3 + docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 18 + docs/config-catalog.zh.md | 18 + docs/event-producer-consumer.i18n.yaml | 4 +- docs/event-producer-consumer.md | 6 +- docs/event-producer-consumer.zh.md | 6 +- docs/module-graph.i18n.yaml | 4 +- docs/module-graph.md | 6 + docs/module-graph.zh.md | 6 + docs/subsystems/filesystem.i18n.yaml | 4 +- docs/subsystems/filesystem.md | 2 +- docs/subsystems/filesystem.zh.md | 2 +- docs/tool-catalog.i18n.yaml | 4 +- docs/tool-catalog.md | 107 +++ docs/tool-catalog.zh.md | 109 +++ packages/bundle/base/cordis.patch.yml | 5 + packages/bundle/base/package.json | 1 + .../sdk-minimal/tests/sdk-minimal.spec.ts | 1 + packages/bundle/web-app/cordis.patch.yml | 3 + .../client/ui-deliverables/README.i18n.yaml | 4 +- packages/client/ui-deliverables/README.md | 2 +- packages/client/ui-deliverables/README.zh.md | 2 +- .../src/client/turn-deliverables.ts | 35 +- .../tests/produced-files.client.spec.tsx | 73 +- packages/client/ui-tool/README.i18n.yaml | 4 +- packages/client/ui-tool/README.md | 2 +- packages/client/ui-tool/README.zh.md | 2 +- .../src/client/tool/models/diff-card-model.ts | 29 +- .../ui-tool/tests/diff-card.client.spec.tsx | 41 ++ .../core/tools/tests/gen-tool-catalog.spec.ts | 2 +- packages/fs/README.i18n.yaml | 4 +- packages/fs/README.md | 1 + packages/fs/README.zh.md | 1 + .../tool-str-replace-editor/README.i18n.yaml | 6 + packages/fs/tool-str-replace-editor/README.md | 149 ++++ .../fs/tool-str-replace-editor/README.zh.md | 149 ++++ .../fs/tool-str-replace-editor/package.json | 53 ++ .../fs/tool-str-replace-editor/src/index.ts | 531 +++++++++++++++ .../tests/tools.spec.ts | 643 ++++++++++++++++++ .../fs/tool-str-replace-editor/tsconfig.json | 16 + pnpm-lock.yaml | 55 ++ python/sdk-runtime/package.json | 1 + scripts/gen-tool-catalog.ts | 14 + scripts/smoke-python-runtime.py | 1 + .../sdk/bash-tool/tool-schemas.expected.json | 85 +++ .../sdk/persistent-tools/cordis.snapshot.yml | 15 + snapshots/sdk/persistent-tools/cordis.yml | 158 +++++ .../notifications.expected.jsonl | 89 +++ .../sdk/persistent-tools/result.expected.json | 1 + snapshots/sdk/persistent-tools/session.jsonl | 88 +++ snapshots/sdk/persistent-tools/snapshot.yml | 10 + .../system-prompt.expected.md | 1 + .../tool-schemas.expected.json | 106 +++ .../workspace.expected/note.txt | 2 + snapshots/sdk/sdk.snapshot.ts | 20 + .../tool-schemas.1.expected.json | 85 +++ .../tool-schemas.1.expected.json | 85 +++ .../tool-schemas.expected.json | 85 +++ .../tool-schemas.1.expected.json | 85 +++ .../tool-schemas.expected.json | 85 +++ .../tool-schemas.1.expected.json | 85 +++ .../tool-schemas.1.expected.json | 85 +++ .../sdk/text-turn/tool-schemas.expected.json | 85 +++ .../tool-schemas.expected.json | 170 +++++ .../both-mode-turn/system-prompt.expected.md | 18 + .../both-mode-turn/tool-schemas.expected.json | 85 +++ .../tool-schemas.expected.json | 170 +++++ .../system-prompt.expected.md | 18 + .../tool-schemas.expected.json | 85 +++ .../tool-schemas.expected.json | 85 +++ .../lsp-definition/tool-schemas.expected.json | 85 +++ .../tool-schemas.expected.json | 85 +++ .../tool-schemas.expected.json | 85 +++ .../tool-schemas.expected.json | 85 +++ .../ptc-python-turn/system-prompt.expected.md | 19 + .../ptc-read-image/system-prompt.expected.md | 18 + .../ptc-turn/system-prompt.expected.md | 18 + .../tool-schemas.expected.json | 85 +++ .../ralph-loop/tool-schemas.1.expected.json | 85 +++ .../ralph-loop/tool-schemas.2.expected.json | 85 +++ .../tool-schemas.expected.json | 85 +++ .../tool-schemas.expected.json | 85 +++ .../tool-schemas.expected.json | 85 +++ .../text-turn/tool-schemas.expected.json | 85 +++ .../web-fetch/tool-schemas.expected.json | 85 +++ tsconfig.base.json | 1 + tsconfig.host.json | 1 + 114 files changed, 5123 insertions(+), 159 deletions(-) delete mode 100644 .agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.i18n.yaml delete mode 100644 .agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.md delete mode 100644 .agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.zh.md create mode 100644 packages/fs/tool-str-replace-editor/README.i18n.yaml create mode 100644 packages/fs/tool-str-replace-editor/README.md create mode 100644 packages/fs/tool-str-replace-editor/README.zh.md create mode 100644 packages/fs/tool-str-replace-editor/package.json create mode 100644 packages/fs/tool-str-replace-editor/src/index.ts create mode 100644 packages/fs/tool-str-replace-editor/tests/tools.spec.ts create mode 100644 packages/fs/tool-str-replace-editor/tsconfig.json create mode 100644 snapshots/sdk/persistent-tools/cordis.snapshot.yml create mode 100644 snapshots/sdk/persistent-tools/cordis.yml create mode 100644 snapshots/sdk/persistent-tools/notifications.expected.jsonl create mode 100644 snapshots/sdk/persistent-tools/result.expected.json create mode 100644 snapshots/sdk/persistent-tools/session.jsonl create mode 100644 snapshots/sdk/persistent-tools/snapshot.yml create mode 100644 snapshots/sdk/persistent-tools/system-prompt.expected.md create mode 100644 snapshots/sdk/persistent-tools/tool-schemas.expected.json create mode 100644 snapshots/sdk/persistent-tools/workspace.expected/note.txt diff --git a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml index 9c49645780..dc2902d1ca 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md -2026-08-23-client-derived-tool-presentation.md: a5d395a4afb7b880ed4401de86296b0b2c85b0c5 -2026-08-23-client-derived-tool-presentation.zh.md: 8784e28dcb53fe96ff3ef0a21cfde82d23e457f7 +2026-08-23-client-derived-tool-presentation.md: 5598c1fbc6073a71f63a20274cd5a791b6b5e6b3 +2026-08-23-client-derived-tool-presentation.zh.md: 5a87433377af58b1ca9d378b76393dada4ca4a1e diff --git a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md index a5d395a4af..5598c1fbc6 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md +++ b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md @@ -324,7 +324,10 @@ Standard and persistent providers sharing the same tool name are a special compa |---|---| | running `write` | intended added-only diff from `file_path` and `content` | | running `edit` | intended replacement diff from `file_path`, `old_string`, and `new_string` | +| running `str_replace_editor create` | intended added-only diff from `path` and `file_text` | +| running `str_replace_editor str_replace` | intended replacement diff from `path`, `old_str`, and `new_str` | | successful settled `write`/`edit` | applied contextual hunks from `meta.diffs` | +| settled `str_replace_editor` | Generic, because the tool defines no result presenter | | write create or missing/malformed/empty applied metadata | current argument fallback | | error, malformed arguments, edit with malformed metadata, or Code Dispatch child | Generic | @@ -372,6 +375,8 @@ The Deliverables Definition observes root `tool/call` and successful `tool/resul |---|---|---| | `write` | any successful call | `file_path` | | `edit` | any successful call | `file_path` | +| `str_replace_editor` | `create`, `str_replace`, or `insert` | `path` | +| `str_replace_editor` | `view` | produces no path | | Other | no current first-party mutation semantics | produces no path | Failures, interruptions, orphan results, missing paths, and malformed arguments produce no deliverable. Paths retain first-seen deduplication, and results settled after the closing Assistant seq remain excluded. @@ -527,6 +532,8 @@ This change does not promise to preserve differences expressed only through a Ho ### Deliverables - Successful write/edit calls produce `file_path`. +- str_replace_editor create/str_replace/insert calls produce `path`. +- str_replace_editor view produces no path. - failure, interruption, malformed input, and orphan results produce no path. - First-seen deduplication and the closing-seq cutoff remain unchanged. diff --git a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md index 8784e28dcb..5a87433377 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md @@ -324,7 +324,10 @@ TerminalBlock 的 ANSI、光标重放、宽字符、行数上限、展开、复 |---|---| | running `write` | 从 `file_path` 与 `content` 生成 intended added-only diff | | running `edit` | 从 `file_path`、`old_string`、`new_string` 生成 intended replacement diff | +| running `str_replace_editor create` | 从 `path` 与 `file_text` 生成 intended added-only diff | +| running `str_replace_editor str_replace` | 从 `path`、`old_str` 与 `new_str` 生成 intended replacement diff | | settled `write`/`edit` success | 从 `meta.diffs` 生成 applied contextual hunks | +| settled `str_replace_editor` | Generic,因为该工具没有 result presenter | | write create 或 applied metadata 缺失、畸形、为空 | 当前 args fallback | | error、畸形 args、edit 的 metadata 畸形、Code Dispatch child | Generic | @@ -372,6 +375,8 @@ Deliverables Definition 按 callId 观察 root `tool/call` 与成功 `tool/resul |---|---|---| | `write` | 任意成功调用 | `file_path` | | `edit` | 任意成功调用 | `file_path` | +| `str_replace_editor` | `create`、`str_replace`、`insert` | `path` | +| `str_replace_editor` | `view` | 不产生 path | | 其他 | 无当前第一方 mutation 语义 | 不产生 path | 失败、interrupted、orphan result、缺失 path 与畸形 args 不产生 deliverable。同一路径保持 first-seen 去重,closing Assistant seq 之后落定的结果继续排除。 @@ -527,6 +532,8 @@ Host registry 允许不同 scope 为同一 tool name 提供不同定义;Sessio ### Deliverables - write/edit 成功产生 `file_path`。 +- str_replace_editor create/str_replace/insert 产生 `path`。 +- str_replace_editor view 不产生 path。 - failure、interrupted、malformed 与 orphan 不产生 path。 - first-seen 去重与 closing seq cut 不变。 diff --git a/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.i18n.yaml index d2d1256907..2656ce9926 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.md -2026-08-09-filesystem-absence-observation.md: 5f34ec29c85424a9979bfe48bbb71fd0149f676d -2026-08-09-filesystem-absence-observation.zh.md: 1ea214ddeb72b25331e0a60157b28029e6e346a7 +2026-08-09-filesystem-absence-observation.md: 40338c4806607b262b93222b4a73a34538414e51 +2026-08-09-filesystem-absence-observation.zh.md: 4544424800844e61f839d5af1f92cef3b3655a27 diff --git a/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.md b/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.md index 5f34ec29c8..40338c4806 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.md +++ b/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.md @@ -12,7 +12,7 @@ Treating a failed read as permission to create also exposes a second boundary. B ## Decision -`dsh-fs` owns an explicit observation union: `{ kind: 'present', version: FsVersion } | { kind: 'absent' }`. The `fs/observed` event carries that union. Successful reads and mutations emit present; a metadata miss from `read` emits absent synchronously before returning `FS_NOT_FOUND`. Other read failures do not manufacture absence. +`dsh-fs` owns an explicit observation union: `{ kind: 'present', version: FsVersion } | { kind: 'absent' }`. The `fs/observed` event carries that union. Successful reads and mutations emit present; a metadata miss from `read` or the `str_replace_editor` `view`, `str_replace`, or `insert` command emits absent synchronously before returning `FS_NOT_FOUND`. Other read failures do not manufacture absence. `dsh-fs-observation-policy` stores three logical states per owner and target without injecting or calling `ctx.fs`: missing map entry is unseen, `absent` is confirmed absence, and `present(version)` is a replacement/edit basis. Write maps unseen and absent to the existing `createIfAbsent` intent and present to `replaceIfVersion`. Edit maps unseen to `FS_NOT_OBSERVED`, absent to `FS_NOT_FOUND`, and present to its version guard. A successful create or mutation replaces absence with its produced present version. diff --git a/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.zh.md b/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.zh.md index 1ea214ddeb..4544424800 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.zh.md @@ -12,7 +12,7 @@ Status: implemented ## 决策 -`dsh-fs` 拥有一个显式观测联合类型:`{ kind: 'present', version: FsVersion } | { kind: 'absent' }`。`fs/observed` 事件携带该联合类型。成功的读取与变更发出存在观测;`read` 的元数据未命中时,会在返回 `FS_NOT_FOUND` 前同步发出缺失观测。其他读取失败不会产生缺失观测。 +`dsh-fs` 拥有一个显式观测联合类型:`{ kind: 'present', version: FsVersion } | { kind: 'absent' }`。`fs/observed` 事件携带该联合类型。成功的读取与变更发出存在观测;`read` 的元数据未命中,或 `str_replace_editor` 的 `view`、`str_replace`、`insert` 命令发生元数据未命中时,都会在返回 `FS_NOT_FOUND` 前同步发出缺失观测。其他读取失败不会产生缺失观测。 `dsh-fs-observation-policy` 按所有者与目标存储三种逻辑状态,既不注入也不调用 `ctx.fs`:映射中无条目即未见,`absent` 表示确认缺失,`present(version)` 是替换/编辑基准。写入把未见和缺失映射到现有 `createIfAbsent` 意图,把存在映射到 `replaceIfVersion`。编辑把未见映射到 `FS_NOT_OBSERVED`,把缺失映射到 `FS_NOT_FOUND`,把存在映射到其版本守卫。成功创建或变更后,系统会用其产生的存在版本取代缺失状态。 diff --git a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.i18n.yaml b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.i18n.yaml index 23fff50840..9ba413c09f 100644 --- a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md -2026-07-29-persistent-bash-str-replace-editor.md: a1289d3f2c7c42851ce6eb5d0ec071786e797614 -2026-07-29-persistent-bash-str-replace-editor.zh.md: 49e315fc789f0f031bf09dd96d2de035cf7dd609 +2026-07-29-persistent-bash-str-replace-editor.md: 02ad2bfa8ab8051eff2c408c8dbbca7acca2daaa +2026-07-29-persistent-bash-str-replace-editor.zh.md: beb2e17d2d342e621e0f1f829910ecf48e35ac80 diff --git a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md index a1289d3f2c..02ad2bfa8a 100644 --- a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md +++ b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md @@ -14,13 +14,11 @@ Some deployments need a one-call Bash schema whose shell state survives across m `@deepseek-ai/dsh-tool-str-replace-editor` independently consumes `ctx.fs` and registers `str_replace_editor` with `view`, `create`, `str_replace`, and `insert`. It provides numbered text views, filtered two-level directory listings, unique literal replacement, canonical insertion boundaries, and bounded output. Paths are absolute; file views preserve content tabs so copied text remains valid literal replacement input; mutations preserve tabs outside the requested edit; and the public schema and failures use only `old_str`. Command-specific fields accept `null` placeholders: execution treats them as omitted when the selected command does not use them, preserves required-field checks, treats `view_range: null` as a full view, and rejects `str_replace.new_str: null` so only omission requests deletion. The plugin can compose with persistent Bash, one-shot Bash, sandboxed Bash, or no shell. -The [complete-removal decision](../simplification/2026-09-03-remove-str-replace-editor.md) later supersedes the preceding editor half and deletes that package. This note retains authority only for the persistent-shell behavior and the historical reason the capabilities were originally separate. - `dsh-system-prompt` accepts `includeHarnessIdentity: false`, and profile compositions own that row together with their selected shell-tool rows. `sdk-minimal` sets the value to `false` and mounts only its platform-selected persistent shell, so a deployment can own an exact persona without duplicate prompt or tool registrations. Existing defaults remain unchanged. -The persistent-shell plugin is included in the Python runtime closure together with the PTY service/local backend and the sandbox services required by that backend. Because `node-pty` executes a native `spawn-helper` on macOS, each packaged macOS runtime executable ships with a `-spawn-helper` sibling; Linux uses `forkpty` directly. A pinned `node-pty` patch checks `DSH_NODE_PTY_SPAWN_HELPER` first, so it remains a true override for a current external consumer that supplies a non-sibling helper. When the override is unset, the patch resolves the packaged executable sibling if present and otherwise preserves upstream lookup in ordinary Node runs. The macOS builders fail before publication when the helper is absent or not executable. +Both plugins are included in the Python runtime closure. The persistent Bash closure also includes the PTY service/local backend and the sandbox services required by that backend. Because `node-pty` executes a native `spawn-helper` on macOS, each packaged macOS runtime executable ships with a `-spawn-helper` sibling; Linux uses `forkpty` directly. A pinned `node-pty` patch checks `DSH_NODE_PTY_SPAWN_HELPER` first, so it remains a true override for a current external consumer that supplies a non-sibling helper. When the override is unset, the patch resolves the packaged executable sibling if present and otherwise preserves upstream lookup in ordinary Node runs. The macOS builders fail before publication when the helper is absent or not executable. -The shipped [`minimal` agent preset](../../../../packages/preset/agent-presets/presets/minimal/agent.cordis.yml) composes only the persistent-shell plugin for the minimal model contract. Its entry-local PTY realm carries the registry, local backend, and persistent Bash or PowerShell tool. The preset fixes the complete system prompt, follows the deployment tool-presentation mode, omits every other model-facing consumer, and leaves browser, Workspace, persistence, sandbox, and permission services on the shared Web host. The local PTY backend resolves the effective session sandbox mode when it creates the shell. While that owner has an open shell or a spawn in progress, a different permission mode is rejected before its session event commits. The [persistent-shell-only decision](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.md) owns the exact minimal composition; the complete-removal decision owns the editor package's absence from the distribution. +The shipped [`minimal` agent preset](../../../../packages/preset/agent-presets/presets/minimal/agent.cordis.yml) composes only the persistent-shell plugin for the minimal model contract. Its entry-local PTY realm carries the registry, local backend, and persistent Bash or PowerShell tool. The preset fixes the complete system prompt, follows the deployment tool-presentation mode, omits every other model-facing consumer, and leaves browser, Workspace, persistence, sandbox, and permission services on the shared Web host. The local PTY backend resolves the effective session sandbox mode when it creates the shell. While that owner has an open shell or a spawn in progress, a different permission mode is rejected before its session event commits. The [persistent-shell-only decision](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.md) owns the editor's absence from minimal compositions; the editor remains independently mountable elsewhere. ## Alternatives considered @@ -36,4 +34,4 @@ The shipped [`minimal` agent preset](../../../../packages/preset/agent-presets/p ## Consequences -Profiles can reproduce an external agent by configuring persona and descriptions while the persistent-shell package remains general. Persistent Bash requires an owning Agent and real PTY backend. Shell exit, timeout, or cancellation loses state. A minimal Web agent retains Web permissions but must close its persistent shell before changing modes. Runtime-wheel consumers still need no Node installation; Linux wheels contain one executable, while macOS wheels also contain its private native helper. The editor-specific consequences no longer apply after its package removal. +Profiles can reproduce an external agent by configuring persona and descriptions while the underlying packages remain general. Persistent Bash requires an owning Agent and real PTY backend. Shell exit, timeout, or cancellation loses state. The editor delegates security and mutation policy to the mounted filesystem stack. Nullable branches increase the command-specific fields' schema cost so unused placeholders do not force retries; execution keeps the selected command's required and deletion semantics explicit. A minimal Web agent retains Web permissions but must close its persistent shell before changing modes. Runtime-wheel consumers still need no Node installation; Linux wheels contain one executable, while macOS wheels also contain its private native helper. diff --git a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.zh.md b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.zh.md index 49e315fc78..beb2e17d2d 100644 --- a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.zh.md +++ b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.zh.md @@ -14,13 +14,11 @@ Status: implemented `@deepseek-ai/dsh-tool-str-replace-editor` 独立消费 `ctx.fs`,注册包含 `view`、`create`、`str_replace` 与 `insert` 的 `str_replace_editor`。它提供带行号文本查看、过滤后的两层目录列表、唯一字面量替换、规范插入边界和有界输出。路径必须为绝对路径;文件查看会保留内容中的制表符,因此复制的文本仍可作为有效的字面量替换输入;变更会保留请求编辑范围之外的制表符;公开 schema 与错误则只使用 `old_str`。命令专属字段接受 `null` 占位参数:当前命令不使用该字段时,执行会将其视为未提供;必填检查保持不变;`view_range: null` 表示查看完整文件;`str_replace.new_str: null` 会被拒绝,只有省略该字段才表示删除。它可以与持久 Bash、一次性 Bash、沙箱 Bash 或无 shell 组合。 -后续的[完整移除决策](../simplification/2026-09-03-remove-str-replace-editor.zh.md)取代了前述 editor 部分并删除该包。本 note 只继续负责持久 shell 行为,以及两个能力最初分离的历史理由。 - `dsh-system-prompt` 接受 `includeHarnessIdentity: false`,profile 组合同时拥有该配置行与所选 shell 工具配置行。`sdk-minimal` 将该值设为 `false`,且只挂载按平台选择的持久 shell,因此部署可以拥有精确 persona,而不会重复注册提示词或工具。既有默认值不变。 -持久 shell 插件进入 Python runtime 闭包,并携带 PTY 服务/本地后端以及该后端要求的沙箱服务。由于 `node-pty` 在 macOS 上会执行原生 `spawn-helper`,每个打包后的 macOS 运行时可执行文件都会携带一个 `-spawn-helper` 伴随文件;Linux 直接使用 `forkpty`。固定版本的 `node-pty` 补丁会先检查 `DSH_NODE_PTY_SPAWN_HELPER`,因此对当前提供非伴随 helper 的外部消费方而言,该变量仍是真正的覆盖项。未设置该覆盖时,补丁会在打包可执行文件的伴随文件存在时解析它,否则在普通 Node 运行中保留上游查找方式。若 helper 缺失或不可执行,macOS 构建器会在发布前失败。 +两个插件都进入 Python runtime 闭包。持久 Bash 的闭包还包含 PTY 服务/本地后端,以及该后端要求的沙箱服务。由于 `node-pty` 在 macOS 上会执行原生 `spawn-helper`,每个打包后的 macOS 运行时可执行文件都会携带一个 `-spawn-helper` 伴随文件;Linux 直接使用 `forkpty`。固定版本的 `node-pty` 补丁会先检查 `DSH_NODE_PTY_SPAWN_HELPER`,因此对当前提供非伴随 helper 的外部消费方而言,该变量仍是真正的覆盖项。未设置该覆盖时,补丁会在打包可执行文件的伴随文件存在时解析它,否则在普通 Node 运行中保留上游查找方式。若 helper 缺失或不可执行,macOS 构建器会在发布前失败。 -随附的 [`minimal` agent preset](../../../../packages/preset/agent-presets/presets/minimal/agent.cordis.yml)只为极简模型约定组合持久 shell 插件。其 entry 本地 PTY realm 持有注册表、本地后端和持久 Bash 或 PowerShell 工具。Preset 会固定完整系统提示词、跟随部署的工具呈现模式,省略其他所有面向模型的消费方,并将浏览器、Workspace、持久化、沙箱与权限服务留在共享 Web 宿主上。本地 PTY 后端会在创建 shell 时解析会话的有效沙箱模式。只要该所有者仍有打开的 shell 或仍在进行中的 spawn,另一种权限模式就会在对应的会话事件提交前遭到拒绝。[仅持久 shell 决策](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md)负责精确极简组合;完整移除决策负责发行物不再包含 editor 包。 +随附的 [`minimal` agent preset](../../../../packages/preset/agent-presets/presets/minimal/agent.cordis.yml)只为极简模型约定组合持久 shell 插件。其 entry 本地 PTY realm 持有注册表、本地后端和持久 Bash 或 PowerShell 工具。Preset 会固定完整系统提示词、跟随部署的工具呈现模式,省略其他所有面向模型的消费方,并将浏览器、Workspace、持久化、沙箱与权限服务留在共享 Web 宿主上。本地 PTY 后端会在创建 shell 时解析会话的有效沙箱模式。只要该所有者仍有打开的 shell 或仍在进行中的 spawn,另一种权限模式就会在对应的会话事件提交前遭到拒绝。[仅持久 shell 决策](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md)负责极简组合不包含 editor 的事实;其他组合仍可独立挂载 editor。 ## 考虑过的替代方案 @@ -36,4 +34,4 @@ Status: implemented ## 后果 -Profile 可以通过配置 persona 和描述复现外部 Agent,而持久 shell 包保持通用。持久 Bash 需要拥有它的 Agent 与真实 PTY 后端;shell 退出、超时或取消会丢失状态。minimal Web agent 保留 Web 权限,但必须先关闭持久 shell 才能更改权限模式。运行时 wheel 包的消费方仍无需安装 Node;Linux wheel 包包含一个可执行文件,macOS wheel 包还包含其私有原生 helper。删除 editor 包后,其专属后果不再适用。 +Profile 可以通过配置 persona 和描述复现外部 Agent,而底层包保持通用。持久 Bash 需要拥有它的 Agent 与真实 PTY 后端;shell 退出、超时或取消会丢失状态。编辑器把安全与变更策略委托给挂载的文件系统栈。可为 `null` 的分支增加了命令专属字段的 schema 成本,使未使用的占位参数不会迫使模型重试;执行仍明确保留当前命令的必填与删除语义。minimal Web agent 保留 Web 权限,但必须先关闭持久 shell 才能更改权限模式。运行时 wheel 包的消费方仍无需安装 Node;Linux wheel 包包含一个可执行文件,macOS wheel 包还包含其私有原生 helper。 diff --git a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml index e8b141e733..33e4ac93a8 100644 --- a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md -2026-07-31-even-out-shipped-tool-rosters.md: c16b8676f666f648f7e6ed87d24ebc9661cbc48a -2026-07-31-even-out-shipped-tool-rosters.zh.md: 2784ca5bdc86eb48729134b5fc8bf24fd865cda2 +2026-07-31-even-out-shipped-tool-rosters.md: 4fea5fb30860f3254aec6178068158b4e7756493 +2026-07-31-even-out-shipped-tool-rosters.zh.md: 32c188f6419b9ede74838215848d77f139280cde diff --git a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md index c16b8676f6..4fea5fb308 100644 --- a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md +++ b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md @@ -12,11 +12,11 @@ The result was a user-visible difference nobody had decided: the same model, ask ## Decision -The rows that are not surface-specific move into [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml), and three more join them: `tool-session-query`, `tool-str-replace-editor`, and `repeat-tool-reminder`. Web search moves there too; its [deployment decision](2026-07-31-web-default-search.md) owns the security boundary while the shared base owns its surface-neutral mount. Both surfaces assemble the same roster, including fixed `glob` and `grep` members because `dsh-tool-fs-search` spawns the [packaged ripgrep binary](../architecture/2026-08-01-packaged-ripgrep-search.md). Later decisions narrow that roster: the [session-search decision](2026-08-02-session-search-not-shipped-default.md) keeps `tool-session-query` opt-in, the [single-editor decision](../simplification/2026-08-10-default-presets-single-editor.md) removes `tool-str-replace-editor` from general-purpose presets, the [persistent-shell-only decision](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.md) removes it from the minimal compositions, and the [complete-removal decision](../simplification/2026-09-03-remove-str-replace-editor.md) later deletes the package and shared-base row recorded here. +The rows that are not surface-specific move into [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml), and three more join them: `tool-session-query`, `tool-str-replace-editor`, and `repeat-tool-reminder`. Web search moves there too; its [deployment decision](2026-07-31-web-default-search.md) owns the security boundary while the shared base owns its surface-neutral mount. Both surfaces assemble the same roster, including fixed `glob` and `grep` members because `dsh-tool-fs-search` spawns the [packaged ripgrep binary](../architecture/2026-08-01-packaged-ripgrep-search.md). Later decisions narrow that roster: the [session-search decision](2026-08-02-session-search-not-shipped-default.md) keeps `tool-session-query` opt-in, the [single-editor decision](../simplification/2026-08-10-default-presets-single-editor.md) removes `tool-str-replace-editor` from general-purpose presets, and the [persistent-shell-only decision](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.md) removes it from the minimal compositions. Two rows stay surface-specific. `tmux-context` is TUI-only because a browser surface has no terminal multiplexer to describe. `session-reference` is TUI-only because it drives the shared session-query index from the launcher's process-local path, and the browser sidebar reconciles that index on its own first search. -**This roster decision added only at the time.** No tool row was removed from either surface when it landed, and a catalog comparison found additions and nothing else. The later session-search, single-editor, and complete-removal decisions own their respective current exceptions. The shared executors, sandbox composition, and access default are owned independently by the [workspace-write default decision](2026-07-31-workspace-write-surface-default.md). +**This roster decision added only at the time.** No tool row was removed from either surface when it landed, and a catalog comparison found additions and nothing else. The later session-search and single-editor decisions own their respective default-roster exceptions. The shared executors, sandbox composition, and access default are owned independently by the [workspace-write default decision](2026-07-31-workspace-write-surface-default.md). ### What stays unmounted, and why diff --git a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md index 2784ca5bdc..32c188f641 100644 --- a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md +++ b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md @@ -12,11 +12,11 @@ Status: implemented ## 决策 -那些并非 surface 专属的行移入 [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml),另有三行加入:`tool-session-query`、`tool-str-replace-editor` 和 `repeat-tool-reminder`。Web 搜索也一并移入;其[部署决策](2026-07-31-web-default-search.zh.md)负责安全边界,共享 base 则负责与 surface 无关的挂载。两个 surface 组装同一份清单,其中 `glob` 和 `grep` 是固定成员,因为 `dsh-tool-fs-search` 直接 spawn [打包的 ripgrep 二进制](../architecture/2026-08-01-packaged-ripgrep-search.zh.md)。后续决策收窄了这份清单:[session-search 决策](2026-08-02-session-search-not-shipped-default.zh.md)让 `tool-session-query` 保持需显式启用,[单一 editor 决策](../simplification/2026-08-10-default-presets-single-editor.zh.md)从通用 preset 移除 `tool-str-replace-editor`,[仅持久 shell 决策](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md)从极简组合移除它,随后[完整移除决策](../simplification/2026-09-03-remove-str-replace-editor.zh.md)删除了本记录中的包与共享 base 行。 +那些并非 surface 专属的行移入 [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml),另有三行加入:`tool-session-query`、`tool-str-replace-editor` 和 `repeat-tool-reminder`。Web 搜索也一并移入;其[部署决策](2026-07-31-web-default-search.zh.md)负责安全边界,共享 base 则负责与 surface 无关的挂载。两个 surface 组装同一份清单,其中 `glob` 和 `grep` 是固定成员,因为 `dsh-tool-fs-search` 直接 spawn [打包的 ripgrep 二进制](../architecture/2026-08-01-packaged-ripgrep-search.zh.md)。后续决策收窄了这份清单:[session-search 决策](2026-08-02-session-search-not-shipped-default.zh.md)让 `tool-session-query` 保持需显式启用,[单一 editor 决策](../simplification/2026-08-10-default-presets-single-editor.zh.md)从通用 preset 移除 `tool-str-replace-editor`,[仅持久 shell 决策](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md)则从极简组合移除它。 有两行仍是 surface 专属。`tmux-context` 只在 TUI,因为浏览器 surface 没有终端复用器可描述。`session-reference` 只在 TUI,因为它以 launcher 的进程本地路径驱动共享的 session-query 索引,而浏览器侧边栏会在自己的首次搜索里重建该索引。 -**本次工具清单决策当时只做加法。** 落地时两个 surface 均未移除任何工具行,目录对比只发现了新增,别无其他。后续的 session-search、单一编辑器与完整移除决策分别负责当前对应的例外。共享执行器、沙箱组合与访问默认值独立归属[workspace-write 默认值决策](2026-07-31-workspace-write-surface-default.zh.md)。 +**本次工具清单决策当时只做加法。** 落地时两个 surface 均未移除任何工具行,目录对比只发现了新增,别无其他。后续的 session-search 与单一编辑器决策分别负责对应的默认清单例外。共享执行器、沙箱组合与访问默认值独立归属[workspace-write 默认值决策](2026-07-31-workspace-write-surface-default.zh.md)。 ### 什么保持不挂,以及为什么 diff --git a/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.i18n.yaml index b12f647e47..e8023fd069 100644 --- a/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.md -2026-07-31-web-workspace-file-links.md: ce0ca591cbe0ffc17d33e56f7a680912f3d86231 -2026-07-31-web-workspace-file-links.zh.md: e801c2e67a4e9c21f7d39f6644f0bf94fe8f1fa1 +2026-07-31-web-workspace-file-links.md: 944d7bef9b83eb34895c5f74e511600a713d786b +2026-07-31-web-workspace-file-links.zh.md: 71f0051204e0cb8b65995fc88a9edb230db8f5be diff --git a/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.md b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.md index ce0ca591cb..944d7bef9b 100644 --- a/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.md +++ b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.md @@ -14,7 +14,7 @@ Two distinct defects sat behind that. The transcript never said what a turn had ## Decision -**A finished turn ends with the files it produced.** The row is its own plugin, `@deepseek-ai/dsh-client-ui-deliverables`, registered into the `conversation.chat.turnTail` hole the chat view renders between a closing message's body and its IconActions — ui-conversation owns the hole and the owner currency (nodes, closing seq, `openFile`), the plugin owns every policy. `producedForClosing` reads the paths off the mutation tools' own follow-along `locations` — a diff card or a generic card whose `kind` is `edit` — so a turn's output is listed whether or not the closing message named it, and a new mutation tool joins by declaring what it does rather than by being added to a list. Reads, deletes, and failed calls contribute nothing; a path appears once per turn in first-seen order; accumulation resets on the turn boundary, so a turn that mutates and then ends without content text cannot spill into the next turn's row. CSS container-width bands select a prefix of up to six paths and its matching `+ N files` label, while flexbox shrinks and ellipsizes the visible basenames; the component performs no JavaScript layout observation ([decision](../simplification/2026-09-01-css-produced-file-layout.md)). One cordis.yml line composes the surface in or out; the unregistered hole renders nothing. +**A finished turn ends with the files it produced.** The row is its own plugin, `@deepseek-ai/dsh-client-ui-deliverables`, registered into the `conversation.chat.turnTail` hole the chat view renders between a closing message's body and its IconActions — ui-conversation owns the hole and the owner currency (nodes, closing seq, `openFile`), the plugin owns every policy. `producedForClosing` reads the paths off the mutation tools' own follow-along `locations` — a diff card, or a generic card whose `kind` is `edit` (the shape `str_replace_editor`'s insert presents) — so a turn's output is listed whether or not the closing message named it, and a new mutation tool joins by declaring what it does rather than by being added to a list. Reads, deletes, and failed calls contribute nothing; a path appears once per turn in first-seen order; accumulation resets on the turn boundary, so a turn that mutates and then ends without content text cannot spill into the next turn's row. CSS container-width bands select a prefix of up to six paths and its matching `+ N files` label, while flexbox shrinks and ellipsizes the visible basenames; the component performs no JavaScript layout observation ([decision](../simplification/2026-09-01-css-produced-file-layout.md)). One cordis.yml line composes the surface in or out; the unregistered hole renders nothing. **The path link reads as a link.** Underlined at rest, not only on hover. This is the smaller half of the diff and the larger half of the fix. diff --git a/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.zh.md b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.zh.md index e801c2e67a..71f0051204 100644 --- a/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.zh.md +++ b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.zh.md @@ -14,7 +14,7 @@ Status: implemented ## 决策 -**完成的一轮以它产出的文件收尾。** 该行是独立插件 `@deepseek-ai/dsh-client-ui-deliverables`,注册进 chat 视图在收尾消息正文与其 IconActions 之间渲染的 `conversation.chat.turnTail` 空位——ui-conversation 拥有空位与 owner 通货(节点、收尾 seq、`openFile`),插件拥有全部策略。`producedForClosing` 从改写工具自身的跟随文件 `locations` 中读出路径——diff 卡片或 `kind` 为 `edit` 的 generic 卡片——因此无论收尾消息是否点名,这一轮的产出都会被列出;新的改写工具靠声明自己做了什么加入,而不是靠被加进某张名单。read、删除与失败的调用不贡献任何条目;同一路径在一轮内按首见顺序只出现一次;累积在 turn 边界重置,因此一轮若先改写文件、随后没有正文内容就结束,不会溢进下一轮的行里。CSS 容器宽度档位会选择前六条路径中的一个前缀及其匹配的 `+ N 个文件` 标签,flexbox 则收缩可见的 basename 并用 ellipsis 省略;组件不执行 JavaScript 布局观察([决策](../simplification/2026-09-01-css-produced-file-layout.zh.md))。cordis.yml 中的一行即可把该交互面组合进来或去掉;未注册的空位什么也不渲染。 +**完成的一轮以它产出的文件收尾。** 该行是独立插件 `@deepseek-ai/dsh-client-ui-deliverables`,注册进 chat 视图在收尾消息正文与其 IconActions 之间渲染的 `conversation.chat.turnTail` 空位——ui-conversation 拥有空位与 owner 通货(节点、收尾 seq、`openFile`),插件拥有全部策略。`producedForClosing` 从改写工具自身的跟随文件 `locations` 中读出路径——diff 卡片,或 `kind` 为 `edit` 的 generic 卡片(即 `str_replace_editor` 的 insert 所呈现的形状)——因此无论收尾消息是否点名,这一轮的产出都会被列出;新的改写工具靠声明自己做了什么加入,而不是靠被加进某张名单。read、删除与失败的调用不贡献任何条目;同一路径在一轮内按首见顺序只出现一次;累积在 turn 边界重置,因此一轮若先改写文件、随后没有正文内容就结束,不会溢进下一轮的行里。CSS 容器宽度档位会选择前六条路径中的一个前缀及其匹配的 `+ N 个文件` 标签,flexbox 则收缩可见的 basename 并用 ellipsis 省略;组件不执行 JavaScript 布局观察([决策](../simplification/2026-09-01-css-produced-file-layout.zh.md))。cordis.yml 中的一行即可把该交互面组合进来或去掉;未注册的空位什么也不渲染。 **路径链接读得出是链接。** 静止状态下就带下划线,而不只在悬停时。这是本次改动中更小的那一半,却是修复中更大的那一半。 diff --git a/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.i18n.yaml b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.i18n.yaml index 858227b5e5..5d39c528e2 100644 --- a/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.i18n.yaml +++ b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.md -2026-08-10-default-presets-single-editor.md: 02d0ddddacbb045aa704890e01b9d710179f6eac -2026-08-10-default-presets-single-editor.zh.md: 0a4942d81a4bc58adcdf8c54e09b349cf2954377 +2026-08-10-default-presets-single-editor.md: ef18f8d7dd2242c8af53af970df0a06b8d7d09d3 +2026-08-10-default-presets-single-editor.zh.md: c9b57e1ab3fa8d40ac587590f374f1f3747b7e7f diff --git a/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.md b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.md index 02d0ddddac..ef18f8d7dd 100644 --- a/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.md +++ b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.md @@ -10,16 +10,16 @@ The `standard`, `code`, and `cordis` presets exposed both the `read`/`write`/`ed ## Decision -The `standard`, `code`, and `cordis` preset configurations mount `dsh-tool-fs` and `dsh-tool-fs-search`, but do not mount the standalone editor. PTC mode therefore omits that editor schema from both its registry and generated SDK. The later [persistent-shell-only decision](2026-09-03-minimal-profiles-persistent-shell-only.md) removes the editor from the shipped `minimal` and `sdk-minimal` compositions. The [complete-removal decision](2026-09-03-remove-str-replace-editor.md) subsequently deletes the standalone package and its runtime support. +The `standard`, `code`, and `cordis` preset configurations mount `dsh-tool-fs` and `dsh-tool-fs-search`, but do not mount `dsh-tool-str-replace-editor`. PTC mode therefore omits `str_replace_editor` from both its registry and generated SDK. The later [persistent-shell-only decision](2026-09-03-minimal-profiles-persistent-shell-only.md) removes the editor from the shipped `minimal` and `sdk-minimal` compositions. Deployments and user-authored presets may still mount the plugin explicitly. -This decision originally narrowed only the preset roster. The later complete-removal decision owns the package and Python runtime deletion. The earlier [shared-roster decision](../feature/2026-07-31-even-out-shipped-tool-rosters.md) continues to own why surface-neutral tools live in preset composition; this note owns the earlier preset exception. +This decision narrows the preset roster rather than removing the tool package or its Python runtime support. The earlier [shared-roster decision](../feature/2026-07-31-even-out-shipped-tool-rosters.md) continues to own why surface-neutral tools live in preset composition; this note owns the editor exception. ## Alternatives considered **Keep both editing interfaces in the general-purpose presets.** Rejected because the overlapping model-visible schemas increase tool choice without supplying a separate default operation. -**Remove the editor package from the distribution.** Rejected at the time because explicit deployments were treated as valid consumers. The later complete-removal decision accepts that compatibility break for v41. +**Remove the `str_replace_editor` package from the distribution.** Rejected because explicit deployments remain valid consumers of the standalone plugin. ## Consequences -General-purpose agents use `read`, `write`, and `edit` for filesystem mutations, while minimal agents use their persistent shell. The complete-removal decision now guarantees the obsolete editor cannot be mounted from the distribution. +General-purpose agents use `read`, `write`, and `edit` for filesystem mutations, while minimal agents use their persistent shell. Preset composition tests pin `str_replace_editor`'s absence from every shipped preset and the PTC mode SDK. diff --git a/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.zh.md b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.zh.md index 0a4942d81a..c9b57e1ab3 100644 --- a/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.zh.md +++ b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.zh.md @@ -10,16 +10,16 @@ Status: implemented ## 决策 -`standard`、`code` 和 `cordis` preset 配置挂载 `dsh-tool-fs` 与 `dsh-tool-fs-search`,但不挂载独立 editor。因此 PTC mode 的注册表和生成 SDK 均不包含该 editor schema。后续的[仅持久 shell 决策](2026-09-03-minimal-profiles-persistent-shell-only.zh.md)从随附的 `minimal` 与 `sdk-minimal` 组合中移除 editor;再后的[完整移除决策](2026-09-03-remove-str-replace-editor.zh.md)删除独立包及其 runtime 支持。 +`standard`、`code` 和 `cordis` preset 配置挂载 `dsh-tool-fs` 与 `dsh-tool-fs-search`,但不挂载 `dsh-tool-str-replace-editor`。因此 PTC mode 的注册表和生成的 SDK 均不包含 `str_replace_editor`。后续的[仅持久 shell 决策](2026-09-03-minimal-profiles-persistent-shell-only.zh.md)会从随附的 `minimal` 与 `sdk-minimal` 组合中移除 editor。部署配置与用户自定义 preset 仍可显式挂载该插件。 -此决策最初只收窄 preset 工具清单;后续完整移除决策负责删除包与 Python runtime。较早的[共享清单决策](../feature/2026-07-31-even-out-shipped-tool-rosters.zh.md)继续说明与 surface 无关的工具为何归 preset 组合所有;本记录说明早期的编辑器例外。 +此决策收窄 preset 工具清单,不移除工具包及其 Python 运行时支持。较早的[共享清单决策](../feature/2026-07-31-even-out-shipped-tool-rosters.zh.md)继续说明与 surface 无关的工具为何归 preset 组合所有;本记录说明编辑器例外。 ## 曾考虑的替代方案 **在通用 preset 中保留两套编辑接口。** 不予采用,因为重叠的模型可见 schema 增加了工具选择,却没有提供不同的默认操作。 -**从发行物中删除 editor 包。** 当时不予采用,因为显式部署被视为有效消费方。后续完整移除决策为 v41 接受了这一兼容性破坏。 +**从发行物中删除 `str_replace_editor` 包。** 不予采用,因为显式部署仍是该独立插件的有效消费方。 ## 后果 -通用 agent 使用 `read`、`write` 与 `edit` 完成文件系统修改,minimal agent 则使用持久 shell。完整移除决策现在保证发行物无法再挂载过时 editor。 +通用 agent 使用 `read`、`write` 与 `edit` 完成文件系统修改,minimal agent 则使用持久 shell。Preset 组合测试会固定每份随附 preset 及 PTC mode SDK 都不包含 `str_replace_editor`。 diff --git a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml index f3fb2579cd..077fec32c1 100644 --- a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml +++ b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md -2026-09-03-minimal-profiles-persistent-shell-only.md: 8814cd94dc85f3b56d8ab55b8f7c00367ff3d0b6 -2026-09-03-minimal-profiles-persistent-shell-only.zh.md: 9b3ab9a756a9ef38410bbc2b1e9337070ef28583 +2026-09-03-minimal-profiles-persistent-shell-only.md: fbbdd63c3bd8928be49e9eb78f76c5c2824637a0 +2026-09-03-minimal-profiles-persistent-shell-only.zh.md: 1a7cc625a2ce279da96b1d765151f17e63ce4201 diff --git a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md index 8814cd94dc..fbbdd63c3b 100644 --- a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md +++ b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md @@ -14,20 +14,20 @@ The next-generation model's minimal runtime requires one model-facing tool. Leav The shipped minimal compositions expose exactly one platform-selected persistent shell: `bash` on Linux and macOS, or `pwsh` on Windows. Neither composition mounts `@deepseek-ai/dsh-tool-str-replace-editor`, a filesystem tool, or the `fs-local` service that supported the editor. The fixed complete persona, absence of runtime context and compaction, shell timeout, and launch-specific host services remain unchanged. -The later [complete-removal decision](2026-09-03-remove-str-replace-editor.md) deletes the standalone editor package and all consumers. This note continues to own the exact one-shell composition of the shipped `minimal` and `sdk-minimal` defaults. +The standalone editor package and its consumers outside the two minimal compositions remain supported. A trusted user-authored preset or higher profile patch can mount it explicitly; the shipped `minimal` and `sdk-minimal` defaults never insert it. -Exact composition tests assert the single tool and the absence of a preset-local filesystem service. The `sdk-minimal` bundle test and built config dump assert that their row and dependency allowlists contain no unused filesystem provider. Web and packaged-Python model-visible snapshots pin the one-tool schema roster. +Exact composition tests assert the single tool and the absence of a preset-local filesystem service. The `sdk-minimal` bundle test and built config dump assert that its row and dependency allowlists contain neither `fs-local` nor `dsh-tool-str-replace-editor`. Web and packaged-Python model-visible snapshots pin the one-tool schema roster. -This decision partially supersedes the minimal exceptions in [one editor family in general-purpose presets](2026-08-10-default-presets-single-editor.md), [the minimal preset composition](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md), [the bare minimal runtime](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md), and [the standalone sdk-minimal profile](../architecture/2026-08-24-standalone-sdk-minimal-profile.md). Those notes retain authority for prompt ownership, no-compaction behavior, profile launch, and bundle layering; the complete-removal decision owns the package deletion. +This decision partially supersedes the minimal exceptions in [one editor family in general-purpose presets](2026-08-10-default-presets-single-editor.md), [the minimal preset composition](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md), [the bare minimal runtime](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md), and [the standalone sdk-minimal profile](../architecture/2026-08-24-standalone-sdk-minimal-profile.md). Those notes retain authority for the independent editor package, prompt ownership, no-compaction behavior, profile launch, and bundle layering. ## Alternatives considered **Keep the editor row and hide its schema.** Rejected because a presentation or restriction layer would leave the capability in the minimal composition and make its absence depend on another setting. -**Remove the editor package from the distribution.** Rejected at the time because explicit custom compositions were treated as valid consumers. The later complete-removal decision accepts the v41 compatibility break. +**Remove the editor package from the distribution.** Rejected because full profiles and explicit custom compositions remain valid consumers. The requirement concerns the two shipped minimal defaults. **Keep the editor only in `sdk-minimal`.** Rejected because the two minimal paths would present different tool contracts to the same model class, and the packaged SDK path would retain the schema cost and unused filesystem service. ## Consequences -Minimal agents inspect and modify files through their persistent shell. Their model requests carry one tool schema, and their compositions own no filesystem service. General-purpose profiles keep the native filesystem tools; the obsolete editor package no longer ships. +Minimal agents inspect and modify files through their persistent shell. Their model requests carry one tool schema, and their compositions own no filesystem service. Full profiles, the editor package, and explicit custom compositions keep their existing behavior. diff --git a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md index 9b3ab9a756..1a7cc625a2 100644 --- a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md +++ b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md @@ -14,20 +14,20 @@ Status: implemented 随附的极简组合只提供一个按平台选择的持久 shell:Linux 与 macOS 使用 `bash`,Windows 使用 `pwsh`。两份组合都不挂载 `@deepseek-ai/dsh-tool-str-replace-editor`、文件系统工具或支撑 editor 的 `fs-local` 服务。固定的 complete persona、运行时上下文与 compaction 的缺失、shell 超时和各启动路径的宿主服务保持不变。 -后续的[完整移除决策](2026-09-03-remove-str-replace-editor.zh.md)删除独立 editor 包及其全部消费方。本 note 继续负责随附 `minimal` 与 `sdk-minimal` 默认组合的精确单 shell 组成。 +独立 editor 包及其在两份极简组合之外的消费方继续受支持。受信任的用户自定义 preset 或更高优先级的 profile patch 可以显式挂载它;随附的 `minimal` 与 `sdk-minimal` 默认组合不会插入它。 -精确组合测试会断言单工具清单以及 preset 内不存在文件系统服务。`sdk-minimal` bundle 测试与构建后配置转储会断言配置项和依赖 allowlist 都不含未使用的文件系统 provider。Web 与打包 Python 的模型可见快照会固定单工具 schema 清单。 +精确组合测试会断言单工具清单以及 preset 内不存在文件系统服务。`sdk-minimal` bundle 测试与构建后配置转储会断言配置项和依赖 allowlist 都不含 `fs-local` 或 `dsh-tool-str-replace-editor`。Web 与打包 Python 的模型可见快照会固定单工具 schema 清单。 -该决策部分取代[通用 preset 使用一种 editor](2026-08-10-default-presets-single-editor.zh.md)、[minimal preset 组合](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md)、[极简裸运行时](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md)与[独立 sdk-minimal profile](../architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md)中的极简例外。这些 Agent Note 继续负责提示词所有权、无 compaction 行为、profile 启动与 bundle 分层;完整移除决策负责删除包。 +该决策部分取代[通用 preset 使用一种 editor](2026-08-10-default-presets-single-editor.zh.md)、[minimal preset 组合](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md)、[极简裸运行时](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md)与[独立 sdk-minimal profile](../architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md)中的极简例外。这些 Agent Note 继续负责独立 editor 包、提示词所有权、无 compaction 行为、profile 启动与 bundle 分层。 ## 考虑过的替代方案 **保留 editor 配置项并隐藏其 schema。** 不予采用,因为呈现层或限制层会让该能力继续留在极简组合中,并使其缺失依赖另一项设置。 -**从发行物中删除 editor 包。** 当时不予采用,因为显式自定义组合被视为有效消费方。后续完整移除决策接受了 v41 兼容性破坏。 +**从发行物中删除 editor 包。** 不予采用,因为完整 profile 与显式自定义组合仍是有效消费方。本需求只涉及两份随附的极简默认组合。 **只在 `sdk-minimal` 中保留 editor。** 不予采用,因为两条极简路径会向同类模型提供不同的工具约定,而且打包 SDK 路径仍会承担 schema 成本和未被其他配置项使用的文件系统服务。 ## 后果 -极简 agent 通过持久 shell 检查和修改文件。模型请求只携带一个工具 schema,组合不拥有文件系统服务。通用 profile 保留原生文件系统工具;过时 editor 包不再发布。 +极简 agent 通过持久 shell 检查和修改文件。模型请求只携带一个工具 schema,组合不拥有文件系统服务。完整 profile、editor 包与显式自定义组合保持现有行为。 diff --git a/.agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.i18n.yaml b/.agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.i18n.yaml deleted file mode 100644 index e0a984690a..0000000000 --- a/.agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# side as of the last confirmed-consistent state. Both languages carry equal authority; -# after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.md -2026-09-03-remove-str-replace-editor.md: ac3f173d06af5db28f04839ae0551129ed09c586 -2026-09-03-remove-str-replace-editor.zh.md: 4f6ad91573e734dd463a1fcc5f3b0739ea1607be diff --git a/.agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.md b/.agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.md deleted file mode 100644 index ac3f173d06..0000000000 --- a/.agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.md +++ /dev/null @@ -1,33 +0,0 @@ -# Agent Note: Remove the string-replacement editor - -Status: implemented - -English | [中文](2026-09-03-remove-str-replace-editor.zh.md) - -## Problem - -No shipped profile still needs the standalone `str_replace_editor` interface. General-purpose profiles already expose `read`, `write`, and `edit`, while the next-generation minimal contract exposes only its persistent shell. Keeping the dormant package would still ship its implementation, dependency closure, configuration surface, generated catalog entries, client presentation branches, and model-schema snapshots. - -A hidden or unmounted tool is not a complete removal. User-authored configuration could still mount it, and future composition changes could accidentally restore the schema. - -## Decision - -Delete `@deepseek-ai/dsh-tool-str-replace-editor` from the workspace and distribution. Remove its shared-base row, disabled Web override, CLI and Python runtime dependencies, TypeScript project aliases, tool-catalog registration, client-specific diff and produced-file handling, package documentation, tests, and recorded scenario. Regenerated configuration catalogs, module graphs, composition diagrams, and request snapshots must contain no package or model-visible tool entry. - -No compatibility alias, hidden registration, or replacement package is retained. General-purpose profiles keep `read`, `write`, `edit`, `glob`, and `grep`; minimal profiles keep their platform-selected persistent shell. Historical unknown tool events can use the existing generic client fallback, but the project does not advertise or test the removed contract. - -This decision supersedes the editor half of [the original persistent-tool decision](../feature/2026-07-29-persistent-bash-str-replace-editor.md), [the standalone-package exception](2026-08-10-default-presets-single-editor.md), and [the minimal-profile scope boundary](2026-09-03-minimal-profiles-persistent-shell-only.md). The original note continues to own persistent Bash behavior; the minimal-profile note continues to own the one-shell contract. - -## Alternatives considered - -**Keep the package for explicit custom compositions.** Rejected because there is no current shipped consumer, and an opt-in package still expands the supported and published surface. - -**Leave the registration in place and filter it from model requests.** Rejected because presentation settings could restore the capability and every runtime would continue carrying inactive code. - -**Provide an alias or compatibility shim over `read`/`write`/`edit`.** Rejected because the project is pre-release and a shim would preserve the schema and maintenance cost this change removes. - -## Consequences - -Existing user-authored configurations that name the deleted package fail loudly and must migrate to `read`, `write`, `edit`, or a shell. This is an accepted v41 pre-release compatibility break. - -The removed interface can return only through a new decision with a distinct current consumer, explicit composition owner, package and runtime closure, model-visible documentation, and tests. Until then, source, published manifests, generated catalogs, and current snapshots contain no `str_replace_editor` tool. diff --git a/.agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.zh.md b/.agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.zh.md deleted file mode 100644 index 4f6ad91573..0000000000 --- a/.agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.zh.md +++ /dev/null @@ -1,33 +0,0 @@ -# Agent Note:移除字符串替换编辑器 - -状态:已实现 - -[English](2026-09-03-remove-str-replace-editor.md) | 中文 - -## 问题 - -现有随附 profile 都不再需要独立的 `str_replace_editor` 接口。通用 profile 已提供 `read`、`write` 与 `edit`,下一代模型的 minimal contract 则只提供持久 shell。继续保留这个闲置包,仍会发布其实现、依赖闭包、配置面、生成目录条目、客户端展示分支和模型 schema 快照。 - -隐藏或不挂载工具不等于完整移除。用户自定义配置仍可挂载它,未来的组合改动也可能意外恢复该 schema。 - -## 决策 - -从 workspace 与发行物中删除 `@deepseek-ai/dsh-tool-str-replace-editor`。移除共享 base 行、已禁用的 Web override、CLI 与 Python runtime 依赖、TypeScript project alias、工具目录注册、客户端专用 diff 与 produced-file 处理、包文档、测试和录制场景。重新生成的配置目录、模块图、组合图和请求快照不得包含该包或面向模型的工具条目。 - -不保留兼容 alias、隐藏注册或替代包。通用 profile 保留 `read`、`write`、`edit`、`glob` 与 `grep`;minimal profile 保留按平台选择的持久 shell。历史中的未知工具事件可以使用现有客户端 generic fallback,但项目不再宣传或测试已移除的 contract。 - -本决策取代[原始持久工具决策](../feature/2026-07-29-persistent-bash-str-replace-editor.zh.md)中 editor 的部分、[独立包例外](2026-08-10-default-presets-single-editor.zh.md)以及[极简 profile 范围边界](2026-09-03-minimal-profiles-persistent-shell-only.zh.md)。原始 note 继续负责持久 Bash 行为;极简 profile note 继续负责单 shell contract。 - -## 考虑过的替代方案 - -**为显式自定义组合保留该包。** 不予采用,因为目前没有随附消费方,而 opt-in 包仍会扩大受支持与发布的表面。 - -**保留注册并从模型请求中过滤。** 不予采用,因为展示设置可能恢复该能力,而且每个 runtime 都会继续携带闲置代码。 - -**在 `read`/`write`/`edit` 上提供 alias 或兼容 shim。** 不予采用,因为项目仍处于发布前阶段,shim 会保留本次改动要删除的 schema 与维护成本。 - -## 后果 - -点名已删除包的用户自定义配置会明确失败,必须迁移到 `read`、`write`、`edit` 或 shell。这是 v41 发布前适配中接受的兼容性破坏。 - -只有新决策给出不同且当前存在的消费方、明确的组合 owner、包与 runtime 闭包、面向模型的文档和测试后,已移除接口才能重新引入。在此之前,源码、发布 manifest、生成目录与当前快照均不包含 `str_replace_editor` 工具。 diff --git a/apps/cli/composition.md b/apps/cli/composition.md index 406edb4557..f0f2dadeab 100644 --- a/apps/cli/composition.md +++ b/apps/cli/composition.md @@ -156,6 +156,8 @@ flowchart LR cfg --> plugin_dsh_base_tool_goal plugin_dsh_base_tool_ralph["tool-ralph
@deepseek-ai/dsh-tool-ralph"] cfg --> plugin_dsh_base_tool_ralph + plugin_dsh_base_tool_str_replace_editor["tool-str-replace-editor
@deepseek-ai/dsh-tool-str-replace-editor"] + cfg --> plugin_dsh_base_tool_str_replace_editor plugin_dsh_base_repeat_tool_reminder["repeat-tool-reminder
@deepseek-ai/dsh-repeat-tool-reminder"] cfg --> plugin_dsh_base_repeat_tool_reminder plugin_dsh_base_web["web
@deepseek-ai/dsh-web"] @@ -254,6 +256,7 @@ flowchart LR | `tool-todo` | `@deepseek-ai/dsh-tool-todo` | | `tool-goal` | `@deepseek-ai/dsh-tool-goal` | | `tool-ralph` | `@deepseek-ai/dsh-tool-ralph` | +| `tool-str-replace-editor` | `@deepseek-ai/dsh-tool-str-replace-editor` | | `repeat-tool-reminder` | `@deepseek-ai/dsh-repeat-tool-reminder` | | `web` | `@deepseek-ai/dsh-web` | | `web-search-deepseek` | `@deepseek-ai/dsh-web-search-deepseek` | diff --git a/apps/cli/package.json b/apps/cli/package.json index 4f88ba712f..7a389053d4 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -84,6 +84,7 @@ "@deepseek-ai/dsh-tool-pwsh-persistent": "workspace:^", "@deepseek-ai/dsh-tool-ralph": "workspace:^", "@deepseek-ai/dsh-tool-skill": "workspace:^", + "@deepseek-ai/dsh-tool-str-replace-editor": "workspace:^", "@deepseek-ai/dsh-tool-subagent": "workspace:^", "@deepseek-ai/dsh-tool-subagent-control": "workspace:^", "@deepseek-ai/dsh-tool-todo": "workspace:^", diff --git a/apps/cli/tests/web-agent-presets.e2e.ts b/apps/cli/tests/web-agent-presets.e2e.ts index cfd7abd500..482412b93a 100644 --- a/apps/cli/tests/web-agent-presets.e2e.ts +++ b/apps/cli/tests/web-agent-presets.e2e.ts @@ -335,6 +335,7 @@ describe('the shipped Web composition', () => { ])) // And it keeps the standard agent's own tools rather than replacing them. expect(tools).toEqual(expect.arrayContaining(['bash', 'read', 'edit', 'skill'])) + expect(tools).not.toContain('str_replace_editor') expect(ctx.commands.find(handle.agent, 'goal')).toBeDefined() // The preset's own authoring skill registers into ITS layer of the host @@ -362,8 +363,10 @@ describe('the shipped Web composition', () => { // the capabilities — so the assembly is what carries the claim. const assembly = await ctx.systemPrompt.assemble({ scope: coded.agent }) expect(assembly.tools.map(tool => tool.name)).toEqual(['run_code']) + expect(toolNames(ctx, coded.agent)).not.toContain('str_replace_editor') expect(ctx.commands.find(coded.agent, 'goal')).toBeDefined() const sdk = assembly.sections.find(section => section.name === 'tools:sdk')?.text ?? '' + expect(sdk).not.toContain('str_replace_editor') expect(sdk).toContain('web_search') // The presentation is this agent's alone: the deployment default is diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index fdbe59332d..55c4409121 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-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/config-catalog.md -config-catalog.md: a7b98aac4695b5c1337cfa8e9d6266239a92f68f -config-catalog.zh.md: 82d6a5eeb76fbbfa35b01e41ea7af2b159a9c550 +config-catalog.md: 14aedbe0c0de0d0bc6779d3993c8a03135dc93e3 +config-catalog.zh.md: 4388de1391a273e1e088f1590d0ea1da98258927 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index a7b98aac46..14aedbe0c0 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -2871,6 +2871,24 @@ export interface Config { Source: [`packages/skill/tool-skill/src/index.ts:61`](../packages/skill/tool-skill/src/index.ts) + + +## `@deepseek-ai/dsh-tool-str-replace-editor` + +Requires: `tools` · `fs` + +```ts config-catalog +/** Configuration for the string-replacement editor tool. */ +export interface Config { + /** Maximum returned view characters before clipping (default 16000). */ + maxOutputChars?: number + /** Model-facing tool description. */ + description?: string +} +``` + +Source: [`packages/fs/tool-str-replace-editor/src/index.ts:505`](../packages/fs/tool-str-replace-editor/src/index.ts) + ## `@deepseek-ai/dsh-tool-subagent` diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 82d6a5eeb7..4388de1391 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -2873,6 +2873,24 @@ export interface Config { 来源:[`packages/skill/tool-skill/src/index.ts:61`](../packages/skill/tool-skill/src/index.ts) + + +## `@deepseek-ai/dsh-tool-str-replace-editor` + +需要:`tools` · `fs` + +```ts config-catalog +/** Configuration for the string-replacement editor tool. */ +export interface Config { + /** Maximum returned view characters before clipping (default 16000). */ + maxOutputChars?: number + /** Model-facing tool description. */ + description?: string +} +``` + +来源:[`packages/fs/tool-str-replace-editor/src/index.ts:505`](../packages/fs/tool-str-replace-editor/src/index.ts) + ## `@deepseek-ai/dsh-tool-subagent` diff --git a/docs/event-producer-consumer.i18n.yaml b/docs/event-producer-consumer.i18n.yaml index 6b307d229d..571af289d6 100644 --- a/docs/event-producer-consumer.i18n.yaml +++ b/docs/event-producer-consumer.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/event-producer-consumer.md -event-producer-consumer.md: 02c3c268298d04cc372ef8343c6f5a071b86320b -event-producer-consumer.zh.md: a0de0add0d6777b3f830fe214d48ca3580d6aeae +event-producer-consumer.md: d04a37ee71756619b36de192011a0053e5ceccb8 +event-producer-consumer.zh.md: 226c368517d989cba2eeac515ee2c544151e8005 diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 02c3c26829..d04a37ee71 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -38,9 +38,9 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:102`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) | | `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:90`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`credentials`](../packages/credentials/credentials), `remotes` | | `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace), `workspace-controller` | -| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:66`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | -| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem) | -| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:58`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | +| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:66`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | +| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem) | +| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:58`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | | `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) | | `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` | | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:67`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index a0de0add0d..226c368517 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -40,9 +40,9 @@ | `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:96`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) | | `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:84`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`credentials`](../packages/credentials/credentials), `remotes` | | `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace), `workspace-controller` | -| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:66`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | -| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem) | -| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:58`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | +| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:66`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | +| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem) | +| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:58`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | | `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) | | `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` | | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:67`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) | diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index 11eee3a26b..303dd0f885 100644 --- a/docs/module-graph.i18n.yaml +++ b/docs/module-graph.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/module-graph.md -module-graph.md: 1ad7b4348e6f79865c34d395d01ffb30367e8191 -module-graph.zh.md: d9e1bb8f9eab7a8ac8fdcaad3b2a292743b4cb03 +module-graph.md: e6ca3e11a28b70310ecbd3118e1c736227626abf +module-graph.zh.md: 9025dc6cce64d89cceaded2e92e3e5b465f79c7a diff --git a/docs/module-graph.md b/docs/module-graph.md index 1ad7b4348e..e6ca3e11a2 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -54,6 +54,7 @@ flowchart TD pkg_fs_sandbox["fs-sandbox"] pkg_tool_fs["tool-fs"] pkg_tool_fs_search["tool-fs-search"] + pkg_tool_str_replace_editor["tool-str-replace-editor"] end subgraph group_skill["packages/skill"] pkg_skill["skill"] @@ -720,6 +721,10 @@ flowchart TD pkg_tool_fs_search --> pkg_system_prompt pkg_tool_fs_search --> pkg_timeout pkg_tool_fs_search --> pkg_tools + pkg_tool_str_replace_editor --> pkg_fs + pkg_tool_str_replace_editor --> pkg_sandbox + pkg_tool_str_replace_editor --> pkg_sandbox_policy + pkg_tool_str_replace_editor --> pkg_tools pkg_tool_skill --> pkg_agent pkg_tool_skill --> pkg_llm pkg_tool_skill --> pkg_skill @@ -1322,6 +1327,7 @@ flowchart TD | [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | +| [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) | | [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) | | [`tool-web`](../packages/web/tool-web) | `web` | [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) | | [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`tools`](../packages/core/tools) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index d9e1bb8f9e..9025dc6cce 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -56,6 +56,7 @@ flowchart TD pkg_fs_sandbox["fs-sandbox"] pkg_tool_fs["tool-fs"] pkg_tool_fs_search["tool-fs-search"] + pkg_tool_str_replace_editor["tool-str-replace-editor"] end subgraph group_skill["packages/skill"] pkg_skill["skill"] @@ -722,6 +723,10 @@ flowchart TD pkg_tool_fs_search --> pkg_system_prompt pkg_tool_fs_search --> pkg_timeout pkg_tool_fs_search --> pkg_tools + pkg_tool_str_replace_editor --> pkg_fs + pkg_tool_str_replace_editor --> pkg_sandbox + pkg_tool_str_replace_editor --> pkg_sandbox_policy + pkg_tool_str_replace_editor --> pkg_tools pkg_tool_skill --> pkg_agent pkg_tool_skill --> pkg_llm pkg_tool_skill --> pkg_skill @@ -1324,6 +1329,7 @@ flowchart TD | [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | +| [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) | | [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) | | [`tool-web`](../packages/web/tool-web) | `web` | [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) | | [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`tools`](../packages/core/tools) | diff --git a/docs/subsystems/filesystem.i18n.yaml b/docs/subsystems/filesystem.i18n.yaml index 1b4c450319..7b592e60fa 100644 --- a/docs/subsystems/filesystem.i18n.yaml +++ b/docs/subsystems/filesystem.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/filesystem.md -filesystem.md: 2d64c0abacd12133be0cca275c301f361661d031 -filesystem.zh.md: e563fafc6f4b3bd5b30786c88501f9c813218097 +filesystem.md: 06fb92f453f8a09f87aee817ab56ef147be9a91f +filesystem.zh.md: ea5b6f7a0d6f45b0d7960298ea10417c0b6c2dd3 diff --git a/docs/subsystems/filesystem.md b/docs/subsystems/filesystem.md index 2d64c0abac..06fb92f453 100644 --- a/docs/subsystems/filesystem.md +++ b/docs/subsystems/filesystem.md @@ -239,7 +239,7 @@ interface FileReadOutcome { ## Observed-file state (policy plugin) -Observed state is a `WeakMap>` held inside the `dsh-fs-observation-policy` plugin. Missing map entry means unseen; `{ kind: 'absent' }` means a `read` metadata miss confirmed absence; `{ kind: 'present', version }` means a read, write, or edit observed that version. The write decision maps unseen and absent to `createIfAbsent`, while present maps to `replaceIfVersion`; the edit decision maps unseen to `FS_NOT_OBSERVED`, absent to `FS_NOT_FOUND`, and present to its version guard. The owner is derived from the event actor (normally `exec.agent.session`), treated as opaque and never read. Disposal drops everything (HMR safety), and the policy performs no filesystem I/O. +Observed state is a `WeakMap>` held inside the `dsh-fs-observation-policy` plugin. Missing map entry means unseen; `{ kind: 'absent' }` means a `read` or `str_replace_editor` `view`, `str_replace`, or `insert` metadata miss confirmed absence; `{ kind: 'present', version }` means a read, write, or edit observed that version. The write decision maps unseen and absent to `createIfAbsent`, while present maps to `replaceIfVersion`; the edit decision maps unseen to `FS_NOT_OBSERVED`, absent to `FS_NOT_FOUND`, and present to its version guard. The owner is derived from the event actor (normally `exec.agent.session`), treated as opaque and never read. Disposal drops everything (HMR safety), and the policy performs no filesystem I/O. ## Error taxonomy (provider contract) diff --git a/docs/subsystems/filesystem.zh.md b/docs/subsystems/filesystem.zh.md index e563fafc6f..ea5b6f7a0d 100644 --- a/docs/subsystems/filesystem.zh.md +++ b/docs/subsystems/filesystem.zh.md @@ -239,7 +239,7 @@ interface FileReadOutcome { ## 已观测文件状态(策略插件) -已观测状态是 `dsh-fs-observation-policy` 插件内部持有的 `WeakMap>`。映射中没有条目表示未见;`{ kind: 'absent' }` 表示 `read` 的元数据未命中确认了缺失;`{ kind: 'present', version }` 表示 read、write 或 edit 观测到该版本。写入决策把未见和缺失映射到 `createIfAbsent`,把存在映射到 `replaceIfVersion`;编辑决策把未见映射到 `FS_NOT_OBSERVED`,把缺失映射到 `FS_NOT_FOUND`,把存在映射到其版本守卫。所有者从事件 actor 推导(通常是 `exec.agent.session`),被视为不透明且从不读取。dispose(资源释放)时丢弃全部数据(HMR(热模块替换)安全),策略不执行任何文件系统 I/O。 +已观测状态是 `dsh-fs-observation-policy` 插件内部持有的 `WeakMap>`。映射中没有条目表示未见;`{ kind: 'absent' }` 表示 `read` 的元数据未命中,或 `str_replace_editor` 的 `view`、`str_replace`、`insert` 命令发生元数据未命中,从而确认缺失;`{ kind: 'present', version }` 表示 read、write 或 edit 观测到该版本。写入决策把未见和缺失映射到 `createIfAbsent`,把存在映射到 `replaceIfVersion`;编辑决策把未见映射到 `FS_NOT_OBSERVED`,把缺失映射到 `FS_NOT_FOUND`,把存在映射到其版本守卫。所有者从事件 actor 推导(通常是 `exec.agent.session`),被视为不透明且从不读取。dispose(资源释放)时丢弃全部数据(HMR(热模块替换)安全),策略不执行任何文件系统 I/O。 ## 错误分类体系(提供方约定) diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml index 9b4b0aa4e4..a70f3d6e35 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: 5242b761b860daf5259c49281170cbaf24d3f86c -tool-catalog.zh.md: d1b4d67ce7bcb2290de763cd9957a8ba7d122e20 +tool-catalog.md: 416585106de28296a3105fafbbe6e4edc0d0f16b +tool-catalog.zh.md: 89d2e467863c09b78805fd9db8ec785ad7936a9d diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index 5242b761b8..416585106d 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -23,6 +23,7 @@ This table connects model-visible tool names to the plugin package and service s | `@deepseek-ai/dsh-tool-cordis` | `cordis_define`, `cordis_inspect_list`, `cordis_inspect_query`, `cordis_inspect_self`, `cordis_run`, `cordis_stop`, `cordis_undefine` | `ctx.tools`, `ctx.dynamicCordisRunner` | `tool/call`, `tool/result`, `process-local dynamic package lifecycle` | - | Not in any shipped tree (a deliberate opt-in — dynamic package code reaches the real runtime, see .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). The toolset injects `ctx.dynamicCordisRunner` from `@deepseek-ai/dsh-cordis-host-runner`, which owns the definition registry and the vm sandbox; a composition missing it never activates the tools. A running package may register ADDITIONAL model-visible tools until it is stopped, undefined, or DSH restarts; a full changed request header logs those tool-set changes. | | `@deepseek-ai/dsh-tool-bash-persistent` | `bash` | `ctx.tools`, `ctx.terminals`, `an owning Agent at execution time` | `tool/call`, `PTY shell state`, `tool/result` | - | One owner-isolated persistent bash tool; deployment composition supplies the PTY backend and may override the model-facing environment description. | | `@deepseek-ai/dsh-tool-pwsh-persistent` | `pwsh` | `ctx.tools`, `ctx.terminals`, `an owning Agent at execution time` | `tool/call`, `PTY shell state`, `tool/result` | - | One owner-isolated persistent pwsh tool, the Windows counterpart of the persistent bash tool; deployment composition supplies a pwsh-dialect PTY backend and may override the model-facing environment description. | +| `@deepseek-ai/dsh-tool-str-replace-editor` | `str_replace_editor` | `ctx.tools`, `ctx.fs` | `tool/call`, `fs/observed after view presence/absence, edit absence, or successful mutation`, `tool/result` | - | Standalone view/create/unique literal replace/line insert tool over the filesystem seam; it composes with any shell or terminal API. | | `@deepseek-ai/dsh-tool-fs` | `edit`, `read`, `read_image`, `write` | `ctx.tools`, `ctx.fs`, `ctx.systemPrompt`, `ctx.attachments (image-tool registration)`, `ctx.llm + an image-capable route (image-tool execution)` | `tool/call`, `fs/write-intent or fs/edit-intent for mutations`, `fs/observed after read presence/absence or successful file operation`, `durable attachment (read_image)`, `tool/result` | - | The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-observation-policy` (an `fs/*` event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. The image tool is not registered without `ctx.attachments`; its schema is route-independent, and execution refuses unless the exact routed model declares image input. | | `@deepseek-ai/dsh-tool-fs-search` | `glob`, `grep` | `ctx.tools`, `ctx.subprocess`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | glob and grep are unconditional discovery tools that spawn the packaged ripgrep binary (`@vscode/ripgrep`) through ctx.subprocess as ordinary foreground calls (never background jobs) — no host `rg` install and no shell layer. The catalog uses `sampleOverCapGlobResults: true`; deployments must choose that behavior explicitly. Capped results save the complete formatted list through the optional ctx.spillStore backend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments. | | `@deepseek-ai/dsh-tool-terminal` | `terminal_close`, `terminal_list`, `terminal_open`, `terminal_read`, `terminal_send`, `terminal_signal` | `ctx.tools`, `ctx.terminals`, `ctx.systemPrompt`, `ctx.jobs at call time for run_in_background` | `tool/call`, `tool/result` | - | The six terminal tools are opt-in and complement one-shot shell/filesystem tools. `terminal_send(run_in_background: true)` registers with `ctx.jobs`; TUI, named key sequences, BEL, resize, auto-start, and cross-agent sharing are absent from the schema. | @@ -553,6 +554,112 @@ Source: [`packages/shell/tool-pwsh-persistent/src/index.ts`](../packages/shell/t One owner-isolated persistent pwsh tool, the Windows counterpart of the persistent bash tool; deployment composition supplies a pwsh-dialect PTY backend and may override the model-facing environment description. + + +## `@deepseek-ai/dsh-tool-str-replace-editor` + +### `str_replace_editor` + +Custom editing tool for viewing, creating and editing files +* State is persistent across command calls and discussions with the user +* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep +* The `create` command cannot be used if the specified `path` already exists as a file +* If a `command` generates a long output, it will be truncated and marked with `` +* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match + +Notes for using the `str_replace` command: +* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces! +* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique +* The `new_str` parameter should contain the edited lines that should replace the `old_str` + +```json +{ + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] +} +``` + +Source: [`packages/fs/tool-str-replace-editor/src/index.ts`](../packages/fs/tool-str-replace-editor/src/index.ts) + +Standalone view/create/unique literal replace/line insert tool over the filesystem seam; it composes with any shell or terminal API. + ## `@deepseek-ai/dsh-tool-fs` diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md index d1b4d67ce7..89d2e46786 100644 --- a/docs/tool-catalog.zh.md +++ b/docs/tool-catalog.zh.md @@ -27,6 +27,7 @@ | `@deepseek-ai/dsh-tool-cordis` | `cordis_define`、`cordis_inspect_list`、`cordis_inspect_query`、`cordis_inspect_self`、`cordis_run`、`cordis_stop`、`cordis_undefine` | `ctx.tools`、`ctx.dynamicCordisRunner` | `tool/call`、`tool/result`、`process-local dynamic package lifecycle` | - | 不在任何随产品发布的树中,需要显式选择启用;动态 Package 代码可以访问真实运行时,见 .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md。该工具集注入 `@deepseek-ai/dsh-cordis-host-runner` 提供的 `ctx.dynamicCordisRunner`,后者拥有定义注册表和 vm 沙箱;组合缺少它时这些工具不会激活。运行中的 Package 在停止、undefine 或 DSH 重启前可以注册**额外的**模型可见工具;发生这类工具集变化时,系统会记录完整且有变动的请求头。 | | `@deepseek-ai/dsh-tool-bash-persistent` | `bash` | `ctx.tools`、`ctx.terminals`、`an owning Agent at execution time` | `tool/call`、`PTY shell state`、`tool/result` | - | 一个按所有者隔离的持久 bash 工具;部署组合提供 PTY 后端,并可覆盖面向模型的环境描述。 | | `@deepseek-ai/dsh-tool-pwsh-persistent` | `pwsh` | `ctx.tools`、`ctx.terminals`、`an owning Agent at execution time` | `tool/call`、`PTY shell state`、`tool/result` | - | 一个按所有者隔离的持久 pwsh 工具,持久 bash 工具的 Windows 对应物;部署组合提供 pwsh 方言的 PTY 后端,并可覆盖面向模型的环境描述。 | +| `@deepseek-ai/dsh-tool-str-replace-editor` | `str_replace_editor` | `ctx.tools`、`ctx.fs` | `tool/call`、`fs/observed after view presence/absence, edit absence, or successful mutation`、`tool/result` | - | 基于文件系统 seam 的独立查看/创建/唯一字面量替换/按行插入工具;可与任何 shell 或终端接口组合。 | | `@deepseek-ai/dsh-tool-fs` | `edit`、`read`、`read_image`、`write` | `ctx.tools`、`ctx.fs`、`ctx.systemPrompt`、`ctx.attachments (image-tool registration)`、`ctx.llm + an image-capable route (image-tool execution)` | `tool/call`、`fs/write-intent or fs/edit-intent for mutations`、`fs/observed after read presence/absence or successful file operation`、`durable attachment (read_image)`、`tool/result` | - | 先读后写/编辑策略由 `@deepseek-ai/dsh-fs-observation-policy` 添加;它是一个 `fs/*` 事件门禁插件,不会改变 schema。加载这些工具的部署按预期也应加载该插件。没有 `ctx.attachments` 时图片工具不会注册;其 schema 与路由无关,执行时除非确切路由的模型声明图片输入,否则拒绝。 | | `@deepseek-ai/dsh-tool-fs-search` | `glob`、`grep` | `ctx.tools`、`ctx.subprocess`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | glob 和 grep 是无条件可用的发现工具,通过 ctx.subprocess spawn 随包提供的 ripgrep 二进制文件(`@vscode/ripgrep`),并作为普通前台调用运行,绝不作为后台任务;无需在宿主机安装 `rg`,也不经过 shell 层。本目录使用 `sampleOverCapGlobResults: true`;部署必须显式选择该行为。结果超过上限时,会通过可选的 ctx.spillStore 后端保存完整的格式化列表;在共置部署中,如果后端公开本地路径,返回的定位信息可供后续读取/搜索。 | | `@deepseek-ai/dsh-tool-terminal` | `terminal_close`、`terminal_list`、`terminal_open`、`terminal_read`、`terminal_send`、`terminal_signal` | `ctx.tools`、`ctx.terminals`、`ctx.systemPrompt`、`ctx.jobs at call time for run_in_background` | `tool/call`、`tool/result` | - | 这 6 个终端工具需要选择启用,用于补充一次性 bash/文件系统工具。`terminal_send(run_in_background: true)` 会注册到 `ctx.jobs`;schema 不包含 TUI、具名按键序列、BEL、调整尺寸、自动启动和跨 agent 共享。 | @@ -557,6 +558,114 @@ pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费 一个按所有者隔离的持久 pwsh 工具,持久 bash 工具的 Windows 对应物;部署组合提供 pwsh 方言的 PTY 后端,并可覆盖面向模型的环境描述。 + + +## `@deepseek-ai/dsh-tool-str-replace-editor` + +### `str_replace_editor` + +用于查看、创建和编辑文件的自定义编辑工具: + +* 状态会在命令调用以及与用户的讨论之间持久保留 +* 如果 `path` 是文件,`view` 会显示应用 `cat -n` 后的结果。如果 `path` 是目录,`view` 会列出最多向下 2 层的非隐藏文件和目录 +* 如果指定的 `create` 命令目标 `path` 已作为文件存在,则不能使用该命令 +* 如果 `command` 产生较长输出,输出会被截断并标记为 `` +* 当前命令不使用某个参数时,值为 `null` 的占位参数视为未提供。必填参数仍须提供值;删除匹配内容时应省略 `str_replace.new_str`,而不是将其设为 `null` + +使用 `str_replace` 命令时请注意: + +* `old_str` 参数应与原文件中一行或多行连续内容**完全**匹配。请留意空白字符! +* 如果 `old_str` 参数在文件中不唯一,则不会执行替换。请确保在 `old_str` 中包含足够的上下文,使其唯一 +* `new_str` 参数应包含用于替换 `old_str` 的已编辑行 + +```json +{ + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] +} +``` + +来源:[`packages/fs/tool-str-replace-editor/src/index.ts`](../packages/fs/tool-str-replace-editor/src/index.ts) + +基于文件系统 seam 的独立查看/创建/唯一字面量替换/按行插入工具;可与任何 shell 或终端接口组合。 + ## `@deepseek-ai/dsh-tool-fs` diff --git a/packages/bundle/base/cordis.patch.yml b/packages/bundle/base/cordis.patch.yml index 826e0d3639..94b5d23204 100644 --- a/packages/bundle/base/cordis.patch.yml +++ b/packages/bundle/base/cordis.patch.yml @@ -421,6 +421,11 @@ subagentProvider: spawn maxRounds: 64 + - id: tool-str-replace-editor + name: '@deepseek-ai/dsh-tool-str-replace-editor' + config: + maxOutputChars: 16000 + # Consecutive-repeat reminders on the tool chain. - id: repeat-tool-reminder name: '@deepseek-ai/dsh-repeat-tool-reminder' diff --git a/packages/bundle/base/package.json b/packages/bundle/base/package.json index bc400c3250..24ff47b915 100644 --- a/packages/bundle/base/package.json +++ b/packages/bundle/base/package.json @@ -101,6 +101,7 @@ "@deepseek-ai/dsh-tool-pwsh": "workspace:^", "@deepseek-ai/dsh-tool-ralph": "workspace:^", "@deepseek-ai/dsh-tool-skill": "workspace:^", + "@deepseek-ai/dsh-tool-str-replace-editor": "workspace:^", "@deepseek-ai/dsh-tool-subagent": "workspace:^", "@deepseek-ai/dsh-tool-subagent-control": "workspace:^", "@deepseek-ai/dsh-tool-jobs": "workspace:^", diff --git a/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts b/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts index bdcdd4bc0a..c98cf87881 100644 --- a/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts +++ b/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts @@ -82,6 +82,7 @@ describe('dsh-sdk-minimal bundle', () => { config: { shellDialect: 'pwsh', timeoutMs: 300000 }, }) expect(manifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-fs-local') + expect(manifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-tool-str-replace-editor') expect(Object.keys(manifest.dependencies ?? {}).sort()).toEqual( [...new Set(rows.map(row => row.name).filter((name): name is string => name !== undefined).map(packageName))].sort(), ) diff --git a/packages/bundle/web-app/cordis.patch.yml b/packages/bundle/web-app/cordis.patch.yml index 74a6de9d21..2c9bd25990 100644 --- a/packages/bundle/web-app/cordis.patch.yml +++ b/packages/bundle/web-app/cordis.patch.yml @@ -347,6 +347,9 @@ - id: tool-fs-search disabled: true +- id: tool-str-replace-editor + disabled: true + # The `skill` REGISTRY stays in the host plane. It is host+per-scope layered # (the tools-registry shape): deployment-level providers — repository plugins, # a host skill-filesystem row — register into its global layer, while a preset's diff --git a/packages/client/ui-deliverables/README.i18n.yaml b/packages/client/ui-deliverables/README.i18n.yaml index 016b0db83a..3ccef63ed5 100644 --- a/packages/client/ui-deliverables/README.i18n.yaml +++ b/packages/client/ui-deliverables/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-deliverables/README.md -README.md: dc8f0fb1f923b5df981700ed7b33b8d13432dd2d -README.zh.md: bb207b7e51b0769404711e4c45776b74bd0befd4 +README.md: b228106848549dcddcde84be07c47392dc92a9db +README.zh.md: 0a9d5207fe3c09b89ac34592f04c7eeaeed9b59a diff --git a/packages/client/ui-deliverables/README.md b/packages/client/ui-deliverables/README.md index dc8f0fb1f9..b228106848 100644 --- a/packages/client/ui-deliverables/README.md +++ b/packages/client/ui-deliverables/README.md @@ -43,7 +43,7 @@ The closing prose carries the same vocabulary: an inline-code token resolves by
Implementation internals — click to expand -The Node half registers the static `ui:deliverable-file-references` system-prompt section asking the model to mention primary files from successful creation or modification calls and to write those and any other changed-file references as Markdown inline code. The browser half registers `ProducedFiles` into the chat view's `conversation.chat.turnTail` hole. `deliverablesDefinition` folds each Turn's successful first-party mutation calls into `DeliverablesTurnData` from the validated raw arguments of `write` and `edit`. Reads, unsupported tools, malformed calls, and failed results contribute nothing. A new mutation tool needs an explicit Client contribution before it joins the list. The package also provides the `chatFileMentions` service the chat view consults per closing message; composing the plugin out removes both surfaces and leaves the view's empty chain at zero cost. +The Node half registers the static `ui:deliverable-file-references` system-prompt section asking the model to mention primary files from successful creation or modification calls and to write those and any other changed-file references as Markdown inline code. The browser half registers `ProducedFiles` into the chat view's `conversation.chat.turnTail` hole. `deliverablesDefinition` folds each Turn's successful first-party mutation calls into `DeliverablesTurnData` from the validated raw arguments of `write`, `edit`, and mutating `str_replace_editor` commands. Reads, deletes, unsupported tools, malformed calls, and failed results contribute nothing. A new mutation tool needs an explicit Client contribution before it joins the list. The package also provides the `chatFileMentions` service the chat view consults per closing message; composing the plugin out removes both surfaces and leaves the view's empty chain at zero cost.
diff --git a/packages/client/ui-deliverables/README.zh.md b/packages/client/ui-deliverables/README.zh.md index bb207b7e51..0a9d5207fe 100644 --- a/packages/client/ui-deliverables/README.zh.md +++ b/packages/client/ui-deliverables/README.zh.md @@ -43,7 +43,7 @@ kind: "package-reference"
实现细节——点击展开 -Node 半部注册静态 `ui:deliverable-file-references` 系统提示词段,要求模型点名成功创建或修改的主要文件,并把这些文件以及正文中提到的其他本轮变更文件写成 Markdown 行内代码。浏览器半部把 `ProducedFiles` 注册进 chat 视图的 `conversation.chat.turnTail` 洞。`deliverablesDefinition` 根据 `write` 与 `edit` 中经过校验的原始参数,把每个轮次成功的第一方修改调用折叠进 `DeliverablesTurnData`。读取、不受支持的工具、格式错误的调用和失败结果不贡献任何条目。新的修改工具必须增加显式 Client contribution 才能加入列表。本包还提供 chat 视图按收尾消息查询的 `chatFileMentions` 服务;把插件组合出去会同时移除两个表面,视图的空链以零成本留下。 +Node 半部注册静态 `ui:deliverable-file-references` 系统提示词段,要求模型点名成功创建或修改的主要文件,并把这些文件以及正文中提到的其他本轮变更文件写成 Markdown 行内代码。浏览器半部把 `ProducedFiles` 注册进 chat 视图的 `conversation.chat.turnTail` 洞。`deliverablesDefinition` 根据 `write`、`edit` 和有修改作用的 `str_replace_editor` 命令中经过校验的原始参数,把每个轮次成功的第一方修改调用折叠进 `DeliverablesTurnData`。读取、删除、不受支持的工具、格式错误的调用和失败结果不贡献任何条目。新的修改工具必须增加显式 Client contribution 才能加入列表。本包还提供 chat 视图按收尾消息查询的 `chatFileMentions` 服务;把插件组合出去会同时移除两个表面,视图的空链以零成本留下。
diff --git a/packages/client/ui-deliverables/src/client/turn-deliverables.ts b/packages/client/ui-deliverables/src/client/turn-deliverables.ts index ef6e3468aa..90e3248516 100644 --- a/packages/client/ui-deliverables/src/client/turn-deliverables.ts +++ b/packages/client/ui-deliverables/src/client/turn-deliverables.ts @@ -51,6 +51,8 @@ function mutationPath(name: string, argsRaw: string): string | null { return typeof args.content === 'string' ? pathValue(args.file_path) : null case 'edit': return validEditArgs(args) ? pathValue(args.file_path) : null + case 'str_replace_editor': + return editorMutationPath(args) default: return null } @@ -65,6 +67,31 @@ function validEditArgs(args: Readonly>): boolean { && (args.replace_all === undefined || typeof args.replace_all === 'boolean') } +/** Extract a path only from a complete mutating editor command. */ +function editorMutationPath(args: Readonly>): string | null { + const path = pathValue(args.path) + if (path === null) return null + switch (args.command) { + case 'create': + return typeof args.file_text === 'string' ? path : null + case 'str_replace': + return typeof args.old_str === 'string' + && args.old_str.length > 0 + && (args.new_str === undefined || typeof args.new_str === 'string') + ? path + : null + case 'insert': + return typeof args.insert_line === 'number' + && Number.isInteger(args.insert_line) + && args.insert_line >= 0 + && typeof args.new_str === 'string' + ? path + : null + default: + return null + } +} + /** A non-blank path preserves the exact spelling supplied to the tool. */ function pathValue(value: unknown): string | null { return typeof value === 'string' && value.trim().length > 0 ? value : null @@ -78,10 +105,10 @@ function isRecord(value: unknown): value is Record { /** * Files produced by one Turn data value. * - * The source is the arguments of successful `write` and `edit` calls, not the - * closing prose: a produced file must be listed whether or not the model - * remembered to name it. Reads, unsupported tools, malformed calls, and failed - * results contribute nothing. Paths keep + * The source is the arguments of successful `write`, `edit`, and mutating + * `str_replace_editor` calls, not the closing prose: a produced file must be + * listed whether or not the model remembered to name it. Reads, unsupported + * tools, malformed calls, and failed results contribute nothing. Paths keep * first-seen order and appear once, so a file written and then edited in the * same turn is one entry. * diff --git a/packages/client/ui-deliverables/tests/produced-files.client.spec.tsx b/packages/client/ui-deliverables/tests/produced-files.client.spec.tsx index 061f76194b..49a93b4ade 100644 --- a/packages/client/ui-deliverables/tests/produced-files.client.spec.tsx +++ b/packages/client/ui-deliverables/tests/produced-files.client.spec.tsx @@ -202,11 +202,31 @@ describe('produced-file Turn data', () => { replace_all: false, }), result(5, 'edit'), + call(6, 'create', 'str_replace_editor', { + command: 'create', path: 'notes/new.md', file_path: 'wrong-create.txt', file_text: 'new', + }), + result(7, 'create'), + call(8, 'replace', 'str_replace_editor', { + command: 'str_replace', path: 'notes/existing.md', old_str: 'old', new_str: 'new', + }), + result(9, 'replace'), + call(10, 'delete-text', 'str_replace_editor', { + command: 'str_replace', path: 'notes/deleted-text.md', old_str: 'remove me', + }), + result(11, 'delete-text'), + call(12, 'insert', 'str_replace_editor', { + command: 'insert', path: 'notes/inserted.md', insert_line: 1, new_str: 'line', + }), + result(13, 'insert'), ]) expect(producedForClosing(deliverablesOf(value))).toEqual([ 'out/index.html', 'out/app.css', + 'notes/new.md', + 'notes/existing.md', + 'notes/deleted-text.md', + 'notes/inserted.md', ]) }) @@ -233,6 +253,42 @@ describe('produced-file Turn data', () => { caseName: 'edit has a non-boolean replace_all', name: 'edit', args: { file_path: 'edit.txt', old_string: 'old', new_string: 'new', replace_all: 'yes' }, }, + { + caseName: 'editor create omits file_text', name: 'str_replace_editor', + args: { command: 'create', path: 'create.txt' }, + }, + { + caseName: 'editor create has non-string file_text', name: 'str_replace_editor', + args: { command: 'create', path: 'create.txt', file_text: 1 }, + }, + { + caseName: 'editor replace omits old_str', name: 'str_replace_editor', + args: { command: 'str_replace', path: 'replace.txt', new_str: 'new' }, + }, + { + caseName: 'editor replace has an empty old_str', name: 'str_replace_editor', + args: { command: 'str_replace', path: 'replace.txt', old_str: '' }, + }, + { + caseName: 'editor replace has non-string new_str', name: 'str_replace_editor', + args: { command: 'str_replace', path: 'replace.txt', old_str: 'old', new_str: 1 }, + }, + { + caseName: 'editor insert omits insert_line', name: 'str_replace_editor', + args: { command: 'insert', path: 'insert.txt', new_str: 'new' }, + }, + { + caseName: 'editor insert has a fractional insert_line', name: 'str_replace_editor', + args: { command: 'insert', path: 'insert.txt', insert_line: 1.5, new_str: 'new' }, + }, + { + caseName: 'editor insert has a negative insert_line', name: 'str_replace_editor', + args: { command: 'insert', path: 'insert.txt', insert_line: -1, new_str: 'new' }, + }, + { + caseName: 'editor insert omits new_str', name: 'str_replace_editor', + args: { command: 'insert', path: 'insert.txt', insert_line: 1 }, + }, ])('ignores a successful result when $caseName', ({ name, args }) => { const value = assembler([ at(1, 'turn/start', { turn: 1 }), @@ -243,9 +299,12 @@ describe('produced-file Turn data', () => { expect(producedForClosing(deliverablesOf(value))).toEqual([]) }) - it('ignores unsupported tools, failures, interruptions, malformed calls, and orphan results', () => { + it('ignores editor views, unsupported tools, failures, interruptions, malformed calls, and orphan results', () => { + const replacement = result(25, 'replacement') const value = assembler([ at(1, 'turn/start', { turn: 1 }), + call(2, 'view', 'str_replace_editor', { command: 'view', path: 'viewed.txt' }), + result(3, 'view'), call(4, 'read', 'read', { file_path: 'input.txt' }), result(5, 'read'), call(6, 'unknown', 'custom_edit', { file_path: 'custom.txt', path: 'custom.txt' }), @@ -267,7 +326,19 @@ describe('produced-file Turn data', () => { file_path: ' ', old_string: 'old', new_string: 'new', }), result(20, 'blank-path'), + call(21, 'missing-editor-path', 'str_replace_editor', { command: 'create', file_text: 'x' }), + result(22, 'missing-editor-path'), result(23, 'orphan'), + call(24, 'replacement', 'str_replace_editor', { + command: 'insert', path: 'replaced.txt', insert_line: 0, new_str: 'new', + }), + { + ...replacement, + event: { + ...replacement.event, + surfaceOp: { op: 'replace', start: 1, end: 1 }, + } as SessionEvent, + }, at(26, 'turn/end', { turn: 1, reason: { kind: 'interrupted' } }), ]) diff --git a/packages/client/ui-tool/README.i18n.yaml b/packages/client/ui-tool/README.i18n.yaml index dff7d298b2..4b0b8c7bbc 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: e5a6c85e21b9383b228ae439912cd991b043febb -README.zh.md: 1bbc43e3c4a51dab111328791300f2dc5fb8fae4 +README.md: 2fe410425935aed46ab95844c81568037ac929e9 +README.zh.md: 83440881fe16582a75fd8dd762577dc7b4d5335b diff --git a/packages/client/ui-tool/README.md b/packages/client/ui-tool/README.md index e5a6c85e21..2fe4104259 100644 --- a/packages/client/ui-tool/README.md +++ b/packages/client/ui-tool/README.md @@ -43,7 +43,7 @@ The owner payload is `ToolCallOwnerProps`: `callId`, `toolName`, the frozen `blo ### Built-in views -This package owns the generic fallback and the built-in shell/pwsh, read, read_image, write/edit, grep/glob, web, todo, question, and Code Dispatch presentations. Structured cards derive directly from first-party raw event fields; Host `presentCall` and `presentResult` values never enter the Client. Foreground one-shot shell results use terminal cards. Settled persistent-shell results use the expandable generic input/output card because reset and partial-output diagnostics do not always describe one process exit status; background acknowledgements remain collapsed. A successful question row pairs call questions with result answers by their stable ids and shows readable question/answer lines when expanded. A cancelled or interrupted row shows its verdict and original questions without inventing answers. Unsupported, malformed, or ambiguous inputs fall back to flattened Tool input/result text. `ui-skill` demonstrates a business-owned registration for `skill`. +This package owns the generic fallback and the built-in shell/pwsh, read, read_image, write/edit, running `str_replace_editor` `create`/`str_replace`, grep/glob, web, todo, question, and Code Dispatch presentations. Structured cards derive directly from first-party raw event fields; Host `presentCall` and `presentResult` values never enter the Client. Foreground one-shot shell results use terminal cards. Settled persistent-shell results use the expandable generic input/output card because reset and partial-output diagnostics do not always describe one process exit status; background acknowledgements remain collapsed. A successful question row pairs call questions with result answers by their stable ids and shows readable question/answer lines when expanded. A cancelled or interrupted row shows its verdict and original questions without inventing answers. Unsupported, malformed, or ambiguous inputs fall back to flattened Tool input/result text. `ui-skill` demonstrates a business-owned registration for `skill`. ----- diff --git a/packages/client/ui-tool/README.zh.md b/packages/client/ui-tool/README.zh.md index 1bbc43e3c4..83440881fe 100644 --- a/packages/client/ui-tool/README.zh.md +++ b/packages/client/ui-tool/README.zh.md @@ -43,7 +43,7 @@ owner 载荷为 `ToolCallOwnerProps`:`callId`、`toolName`、冻结的 `block` ### 内置视图 -本包拥有 generic fallback,以及 shell/pwsh、read、read_image、write/edit、grep/glob、web、todo、question 与 Code Dispatch 的内置展示。结构化卡片直接从第一方原始 event 字段派生;Host `presentCall` 与 `presentResult` 值不会进入 Client。前台一次性 shell 结果使用 terminal 卡片。已完成的持久 shell 结果使用可展开的 generic 输入/输出卡片,因为 reset 与部分输出诊断不一定描述单个进程的退出状态;后台启动回执保持折叠。成功的问题行按稳定 id 配对调用中的问题与结果中的回答,展开后显示可读的问答行。已取消或已中断的问题行显示其裁决与原始问题,不虚构回答。不受支持、格式错误或含糊的输入回退为压平的工具输入/结果文本。`ui-skill` 展示了业务包自行拥有的 `skill` 注册项。 +本包拥有 generic fallback,以及 shell/pwsh、read、read_image、write/edit、running `str_replace_editor` `create`/`str_replace`、grep/glob、web、todo、question 与 Code Dispatch 的内置展示。结构化卡片直接从第一方原始 event 字段派生;Host `presentCall` 与 `presentResult` 值不会进入 Client。前台一次性 shell 结果使用 terminal 卡片。已完成的持久 shell 结果使用可展开的 generic 输入/输出卡片,因为 reset 与部分输出诊断不一定描述单个进程的退出状态;后台启动回执保持折叠。成功的问题行按稳定 id 配对调用中的问题与结果中的回答,展开后显示可读的问答行。已取消或已中断的问题行显示其裁决与原始问题,不虚构回答。不受支持、格式错误或含糊的输入回退为压平的工具输入/结果文本。`ui-skill` 展示了业务包自行拥有的 `skill` 注册项。 ----- diff --git a/packages/client/ui-tool/src/client/tool/models/diff-card-model.ts b/packages/client/ui-tool/src/client/tool/models/diff-card-model.ts index 439cdbbc1c..03eaf04a3a 100644 --- a/packages/client/ui-tool/src/client/tool/models/diff-card-model.ts +++ b/packages/client/ui-tool/src/client/tool/models/diff-card-model.ts @@ -47,11 +47,31 @@ function narrowDiffs(diffs: unknown): DiffHunk[] | null { return out } -type IntendedDiff = { tool: 'write' | 'edit'; diff: DiffHunk } +type IntendedDiff = { tool: 'write' | 'edit' | 'str_replace_editor'; diff: DiffHunk } function intendedDiff(block: ToolCallBlock): IntendedDiff | null { const parsed = parsedToolCall(block) if (parsed === null) return null + if (parsed.name === 'str_replace_editor') { + const { command, path, file_text: fileText, old_str: oldText, new_str: newText } = parsed.args + if (typeof path !== 'string' || path.trim() === '') return null + if (command === 'create') { + if (fileText !== undefined && typeof fileText !== 'string') return null + return { + tool: 'str_replace_editor', + diff: { path, oldText: null, newText: fileText ?? '' }, + } + } + if (command === 'str_replace') { + if (oldText !== undefined && typeof oldText !== 'string') return null + if (newText !== undefined && typeof newText !== 'string') return null + return { + tool: 'str_replace_editor', + diff: { path, oldText: oldText ?? null, newText: newText ?? '' }, + } + } + return null + } const { file_path: path } = parsed.args if (typeof path !== 'string' || path.trim() === '') return null if (!validEscalationFields(parsed.args)) return null @@ -77,9 +97,11 @@ function appliedDiffs(meta: unknown): DiffHunk[] | 'empty' | null { } /** - * Derive running diffs and applied settled diffs for root write/edit calls. + * Derive running diffs for root write/edit and `str_replace_editor` + * create/replace calls, plus applied settled diffs for root write/edit calls. * A successful write with valid empty metadata uses its argument-derived - * whole-file diff, matching create and identical-overwrite presentation. + * whole-file diff, matching create and identical-overwrite presentation; + * `str_replace_editor` settles through Generic because it has no result view. * @param block - running or settled Tool block. * @returns the diff-card props, or null for the generic path. */ @@ -88,6 +110,7 @@ export function diffCardModel(block: ToolCallBlock): DiffCardModel | null { const intended = intendedDiff(block) if (intended === null) return null if (!('kind' in block)) return { card: { diffs: [intended.diff] } } + if (intended.tool === 'str_replace_editor') return null if (block.isError) return null const applied = appliedDiffs(block.meta) if (applied === null || applied === 'empty') { diff --git a/packages/client/ui-tool/tests/diff-card.client.spec.tsx b/packages/client/ui-tool/tests/diff-card.client.spec.tsx index 8388e5dcf4..ed386b8321 100644 --- a/packages/client/ui-tool/tests/diff-card.client.spec.tsx +++ b/packages/client/ui-tool/tests/diff-card.client.spec.tsx @@ -63,6 +63,47 @@ describe('diffCardModel', () => { }) }) + it.each([ + { + command: 'create', + args: { command: 'create', path: 'notes/new.txt', file_text: 'new file\n' }, + diff: { path: 'notes/new.txt', oldText: null, newText: 'new file\n' }, + }, + { + command: 'str_replace', + args: { command: 'str_replace', path: 'notes/demo.txt', old_str: 'old', new_str: 'new' }, + diff: { path: 'notes/demo.txt', oldText: 'old', newText: 'new' }, + }, + ])('preserves the running str_replace_editor $command diff', ({ args, diff }) => { + expect(diffCardModel(running({ + name: 'str_replace_editor', + argsRaw: JSON.stringify(args), + }))).toEqual({ card: { diffs: [diff] } }) + }) + + it('preserves str_replace_editor defaults and its settled Generic result', () => { + const argsRaw = JSON.stringify({ command: 'str_replace', path: 'notes/demo.txt' }) + expect(diffCardModel(running({ name: 'str_replace_editor', argsRaw }))).toEqual({ + card: { diffs: [{ path: 'notes/demo.txt', oldText: null, newText: '' }] }, + }) + expect(diffCardModel(settled({ + call: { name: 'str_replace_editor', argsRaw }, + meta: { diffs: [{ path: 'notes/demo.txt', oldText: 'old', newText: 'new' }] }, + }))).toBeNull() + }) + + it('keeps unsupported or malformed str_replace_editor calls generic', () => { + const editor = (args: Record) => running({ + name: 'str_replace_editor', argsRaw: JSON.stringify(args), + }) + expect(diffCardModel(editor({ command: 'view', path: 'notes/demo.txt' }))).toBeNull() + expect(diffCardModel(editor({ command: 'insert', path: 'notes/demo.txt', new_str: 'x' }))).toBeNull() + expect(diffCardModel(editor({ command: 'create', path: '', file_text: 'x' }))).toBeNull() + expect(diffCardModel(editor({ command: 'create', path: 'notes/demo.txt', file_text: 1 }))).toBeNull() + expect(diffCardModel(editor({ command: 'str_replace', path: 'notes/demo.txt', old_str: 1 }))).toBeNull() + expect(diffCardModel(editor({ command: 'str_replace', path: 'notes/demo.txt', new_str: 1 }))).toBeNull() + }) + it('derives a settled card from result metadata, which replaces the intended diff', () => { expect(diffCardModel(settled({ meta: { diffs: [{ path: 'notes/demo.txt', oldText: 'a', newText: 'b' }] }, diff --git a/packages/core/tools/tests/gen-tool-catalog.spec.ts b/packages/core/tools/tests/gen-tool-catalog.spec.ts index 33f970a180..0630cfaa8e 100644 --- a/packages/core/tools/tests/gen-tool-catalog.spec.ts +++ b/packages/core/tools/tests/gen-tool-catalog.spec.ts @@ -34,7 +34,7 @@ describe('gen-tool-catalog collectToolCatalog', () => { 'read', 'read_image', 'run_code', 'schedule_create', 'schedule_delete', 'schedule_list', 'send_message', 'send_message', 'session_event_read', 'session_event_search', 'session_event_trace', 'session_search', 'session_trace', 'skill', 'spawn_teammate', - 'subagent', 'team_task_create', + 'str_replace_editor', 'subagent', 'team_task_create', 'team_task_get', 'team_task_list', 'team_task_update', 'terminal_close', 'terminal_list', 'terminal_open', 'terminal_read', 'terminal_send', 'terminal_signal', 'todo_write', 'update_goal', 'wait_agent', 'web_fetch', 'web_search', 'workflow', 'write', diff --git a/packages/fs/README.i18n.yaml b/packages/fs/README.i18n.yaml index 7675ae6897..5311da4c5b 100644 --- a/packages/fs/README.i18n.yaml +++ b/packages/fs/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/fs/README.md -README.md: 797cdf1697a088958f56c9e4ec2e7de0bd854835 -README.zh.md: 900619ca1e53f2561e23dc3e7f1efe559739a4be +README.md: c8cfc5f8d5dd5d225149abbf192d23df1b51cf71 +README.zh.md: 9596f72e76241de43072ee590c141b043af6a861 diff --git a/packages/fs/README.md b/packages/fs/README.md index 797cdf1697..c8cfc5f8d5 100644 --- a/packages/fs/README.md +++ b/packages/fs/README.md @@ -33,6 +33,7 @@ Seven packages plus the remote sibling `fs-e2b` play the filesystem roles; the s | [`fs-observation-policy/`](fs-observation-policy/README.md) | Read-before-edit policy: records observed presence or absence and guards write/edit through the `fs/*` events | `fs/*` listeners | | [`tool-fs/`](tool-fs/README.md) | Model-facing `read`, `read_image`, `write`, and `edit` tools plus their executor | registers on `ctx.tools` | | [`tool-fs-search/`](tool-fs-search/README.md) | Model-facing `glob` and `grep` discovery tools backed by the packaged ripgrep binary | registers on `ctx.tools` | +| [`tool-str-replace-editor/`](tool-str-replace-editor/README.md) | Standalone `str_replace_editor` tool: `view`, `create`, `str_replace`, and `insert` over `ctx.fs` | registers on `ctx.tools` | The policy is a plugin, not a service the tools inject: removing it leaves the bare provider's unconditional mutation behavior instead of breaking the tools. The mode fence in `fs-sandbox` and the read-before-edit gate compose. `tool-fs-search` deliberately does not extend the provider contract — search is a process-backed ripgrep workflow, so filesystem backends stay free of a universal search API. diff --git a/packages/fs/README.zh.md b/packages/fs/README.zh.md index 900619ca1e..9596f72e76 100644 --- a/packages/fs/README.zh.md +++ b/packages/fs/README.zh.md @@ -33,6 +33,7 @@ kind: "package-group" | [`fs-observation-policy/`](fs-observation-policy/README.zh.md) | 编辑前读取策略:记录观测到的存在或缺失,并通过 `fs/*` 事件防护写入/编辑 | `fs/*` 监听器 | | [`tool-fs/`](tool-fs/README.zh.md) | 面向模型的 `read`、`read_image`、`write` 与 `edit` 工具及其执行器 | 注册到 `ctx.tools` | | [`tool-fs-search/`](tool-fs-search/README.zh.md) | 由打包 ripgrep 二进制支持的面向模型 `glob` 与 `grep` 发现工具 | 注册到 `ctx.tools` | +| [`tool-str-replace-editor/`](tool-str-replace-editor/README.zh.md) | 独立的 `str_replace_editor` 工具:基于 `ctx.fs` 的 `view`、`create`、`str_replace` 与 `insert` | 注册到 `ctx.tools` | 策略是插件,不是工具注入的服务:移除它只会让工具回到裸提供方的无条件变更行为,而不会破坏工具。`fs-sandbox` 的模式围栏与编辑前读取门禁可以组合。`tool-fs-search` 有意不扩展提供方约定——搜索是由进程支持的 ripgrep 工作流,因此文件系统后端无需承担通用搜索 API。 diff --git a/packages/fs/tool-str-replace-editor/README.i18n.yaml b/packages/fs/tool-str-replace-editor/README.i18n.yaml new file mode 100644 index 0000000000..d753b957de --- /dev/null +++ b/packages/fs/tool-str-replace-editor/README.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write packages/fs/tool-str-replace-editor/README.md +README.md: 66663b2ab19585c30f00e6e6d91ce759b4fb8d81 +README.zh.md: 60c63b25cb5c5960aaafcf4f91b3ce23e8911cd7 diff --git a/packages/fs/tool-str-replace-editor/README.md b/packages/fs/tool-str-replace-editor/README.md new file mode 100644 index 0000000000..66663b2ab1 --- /dev/null +++ b/packages/fs/tool-str-replace-editor/README.md @@ -0,0 +1,149 @@ +--- +description: "The standalone str_replace_editor tool over ctx.fs for users and maintainers composing Claude-Code-style file editing for agents." +kind: "package-reference" +--- + +# @deepseek-ai/dsh-tool-str-replace-editor + +English | [中文](README.zh.md) + +## Summary + +`dsh-tool-str-replace-editor` provides a standalone model-facing `str_replace_editor` tool over `ctx.fs`: `view` shows numbered file content or a shallow directory listing, `create` makes a new file, `str_replace` applies a unique literal replacement, and `insert` adds lines at a chosen boundary. It is composable with persistent Bash, one-shot Bash, sandboxed Bash, or another terminal surface. Mutations obey the same read-before-edit policy and sandbox fence as the rest of the fs family, enforced by whichever backend and policy plugins are mounted. Choose it when a deployment wants the Claude-Code-style single editor tool with absolute paths; the `dsh-tool-fs` package provides the alternative `read`/`write`/`edit` suite. + +## Table of Contents + +- [Use this package](#use-this-package) +- [Understand the implementation](#understand-the-implementation) +- [Further Exploration](#further-exploration) +- [Model Experience](#model-experience) +- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work) +- [Dev Note](#dev-note) + +----- + + +## Use this package + +Mount the tool alongside a `ctx.fs` backend (and, for guarded mutations, the policy plugin) when the model should edit files through the familiar `view`/`create`/`str_replace`/`insert` command vocabulary on absolute paths. + +### Minimal composition + +A backend, optionally the policy plugin, then the tool; the editor composes with any terminal surface. + +```yaml +- name: '@deepseek-ai/dsh-fs-local' +- name: '@deepseek-ai/dsh-fs-observation-policy' +- name: '@deepseek-ai/dsh-tool-str-replace-editor' +``` + +### Configuration + +| Key | Default | Meaning | +|---|---|---| +| `maxOutputChars` | `16000` | Prefix characters retained for file and directory views | +| `description` | `Custom editing tool for viewing, creating and editing files` (multi-line) | Model-facing tool description | + +### The commands + +`view` returns one-based numbered file content (tabs preserved, so displayed text stays valid literal replacement input) or a two-level directory listing that omits hidden, dependency, and Python-cache entries. `create` makes a new file and refuses to overwrite an existing one. Command-specific fields may contain `null` placeholders when the selected command does not use them; required fields stay required, `view_range: null` selects the full view, and `str_replace.new_str: null` is rejected so deletion requires omission. `str_replace` requires one unique literal match, with errors reported in the public `old_str` vocabulary; `insert` follows the selected zero-based insertion boundary without adding an implicit trailing newline. Mutations preserve tabs outside the requested edit. + +### Failures and recovery + +A metadata miss from `view`, `str_replace`, or `insert` records confirmed absence before returning `FS_NOT_FOUND`, so a later `create` can recover an externally deleted path through the mounted policy's guarded-create flow; absence never authorizes `str_replace` or `insert`. Guarded mutations inherit the policy plugin's codes and remedies — `FS_NOT_OBSERVED` (read the file, then retry), `FS_STALE_VERSION` (re-read, then retry) — and sandbox denials surface as the `[sandbox: file access denied under mode]` marker. Paths must be absolute; a relative path is refused with a hint. + +----- + + +## Understand the implementation + +
+Implementation internals — click to expand + +This section explains the design decisions behind the editor tool and points at the code that realizes them; the observable behavior is fully covered in [Use this package](#use-this-package). + +### Design concept + +The tool is one schema with four commands over `ctx.fs`. Mutations never touch the provider directly with their own assumptions: each one runs the `fs/write-intent` or `fs/edit-intent` waterfall to obtain the policy plugin's guard, resolves the per-call sandbox policy when the mounted `ctx.fs` confines, and delegates enforcement to the provider. `str_replace` and `insert` additionally re-read the file and use the observed version as the compare-and-swap basis when no policy plugin supplies a guard. + +### Source map + +| File | Role | +|---|---| +| [`src/index.ts`](src/index.ts) | The whole tool: schema, command dispatch, view rendering, mutation policy | + +### How each command runs + +Every command resolves the absolute path first; mutations then follow one shared flow — policy guard, provider enforcement, then an `fs/observed` record on success — while `view` only stats and renders. The entire tool — schema, command dispatch, and view rendering — lives in `src/index.ts`. + +
+ +----- + + +## Further Exploration + +Read these pages when the package-level contract is not enough. They move from the tool to the contract, policy, and backends it composes with. + +- [Filesystem subsystem](../../../docs/subsystems/filesystem.md) — exhaustive provider contract, policy events, and error taxonomy. +- [dsh-fs](../fs/README.md) — the `ctx.fs` contract this tool consumes. +- [tool-fs](../tool-fs/README.md) — the alternative `read`/`write`/`edit` tool suite. +- [fs-observation-policy](../fs-observation-policy/README.md) — the policy plugin that guards mutations through the `fs/*` events. +- [fs-sandbox](../fs-sandbox/README.md) — the sandbox-enforcing backend that fences mutations. +- [Generated tool catalog](../../../docs/tool-catalog.md#deepseek-aidsh-tool-str-replace-editor) — the exhaustive schema this package registers. + +----- + + +## Model Experience + +### Tool schema + +#### What the model sees + +The generated [`str_replace_editor` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-str-replace-editor), including the configured `description`. The plugin contributes no standalone system-prompt section. + +#### Token effect + +Fixed schema cost while `str_replace_editor` is visible. + +#### KV Cache effect + +Prefix-stable while the configured description and schema remain unchanged. + +### Tool results + +#### What the model sees + +Views return numbered text or a shallow directory listing. Calls expose file locations, and create/replace calls expose diff cards to presentation surfaces. Mutations return concise confirmations. Long views keep their prefix and append a clipping notice. + +#### Token effect + +Data-dependent and bounded by `maxOutputChars` plus the fixed clipping notice. + +#### KV Cache effect + +Append-only tool results follow the reusable request prefix. + +## Known Limitations and Deferred Work + + + + +These limits define when the editor tool is a poor fit or needs special operational care. They are current package constraints, not a general editor comparison or a task backlog. + +- **Operations target UTF-8 text** — binary files are unsupported. +- **`str_replace` intentionally rejects zero or multiple matches** — it has no `replace_all` argument. +- **Every mutation goes through the mounted policy and sandbox** — `fs/write-intent` or `fs/edit-intent` resolves the current session sandbox policy and delegates enforcement to the mounted filesystem and policy plugins, so a deployment without them gets unconditional mutations. + + +### Dev Note + +
+Working context for maintainers — click to expand + +None. + +
+ +**Runtime invariant:** No companion is published. The tool adapter owns no independent durable state; filesystem mutation relations stay with the provider and policy plugins. diff --git a/packages/fs/tool-str-replace-editor/README.zh.md b/packages/fs/tool-str-replace-editor/README.zh.md new file mode 100644 index 0000000000..60c63b25cb --- /dev/null +++ b/packages/fs/tool-str-replace-editor/README.zh.md @@ -0,0 +1,149 @@ +--- +description: "基于 `ctx.fs` 的独立 str_replace_editor 工具:供组合 Claude Code 风格文件编辑能力的用户与维护者使用。" +kind: "package-reference" +--- + +# @deepseek-ai/dsh-tool-str-replace-editor + +[English](README.md) | 中文 + +## 概述 + +`dsh-tool-str-replace-editor` 提供基于 `ctx.fs` 的独立面向模型 `str_replace_editor` 工具:`view` 显示带行号的文件内容或浅层目录列表,`create` 创建新文件,`str_replace` 应用唯一的字面量替换,`insert` 在选定的边界处插入行。它可以与持久 Bash、一次性 Bash、沙箱 Bash 或其他终端接口组合。修改操作遵守与 fs 家族其余部分相同的编辑前读取策略与沙箱围栏,具体由所挂载的后端与策略插件强制执行。当部署需要 Claude Code 风格、使用绝对路径的单一编辑器工具时选择它;`dsh-tool-fs` 包提供替代的 `read`/`write`/`edit` 套件。 + +## 目录 + +- [使用本包](#use-this-package) +- [理解实现](#understand-the-implementation) +- [进一步探索](#further-exploration) +- [模型体验](#model-experience) +- [已知限制与延期工作](#known-limitations-and-deferred-work) +- [开发备注](#dev-note) + +----- + + +## 使用本包 + +当模型应通过熟悉的 `view`/`create`/`str_replace`/`insert` 命令词汇在绝对路径上编辑文件时,把工具与 `ctx.fs` 后端(以及需要防护变更时的策略插件)一起挂载。 + +### 最小组合 + +一个后端、可选地加策略插件,然后是工具;编辑器可与任何终端接口组合。 + +```yaml +- name: '@deepseek-ai/dsh-fs-local' +- name: '@deepseek-ai/dsh-fs-observation-policy' +- name: '@deepseek-ai/dsh-tool-str-replace-editor' +``` + +### 配置 + +| 键 | 默认值 | 含义 | +|---|---|---| +| `maxOutputChars` | `16000` | 文件和目录查看结果保留的前缀字符数 | +| `description` | `Custom editing tool for viewing, creating and editing files`(多行) | 面向模型的工具描述 | + +### 命令 + +`view` 返回从 1 开始编号的文件内容(保留制表符,因此显示的文本仍可作为有效的字面量替换输入)或忽略隐藏、依赖与 Python 缓存条目的两层目录列表。`create` 创建新文件,并拒绝覆盖现有文件。选定命令不使用的专用字段可以包含 `null` 占位符;必填字段仍保持必填,`view_range: null` 选择完整视图,`str_replace.new_str: null` 会被拒绝,因此删除内容必须省略该字段。`str_replace` 要求字面量唯一匹配,错误只使用公开的 `old_str` 词汇;`insert` 遵循所选的零基插入边界,不会隐式补尾换行。修改操作会保留请求编辑范围之外的制表符。 + +### 失败与恢复 + +`view`、`str_replace` 或 `insert` 发生元数据未命中时,工具会在返回 `FS_NOT_FOUND` 前记录确认缺失,因此后续 `create` 可以通过已挂载策略的防护创建流程恢复外部删除的路径;缺失状态绝不会授权 `str_replace` 或 `insert`。防护变更继承策略插件的错误码与恢复指令——`FS_NOT_OBSERVED`(先读取文件再重试)、`FS_STALE_VERSION`(先重新读取再重试)——沙箱拒绝则表现为 `[sandbox: file access denied under mode]` 标记。路径必须是绝对路径;相对路径会被拒绝并给出提示。 + +----- + + +## 理解实现 + +
+实现细节——点击展开 + +本节解释编辑器工具背后的设计决策,并指出实现它们的代码位置;可观察行为已在[使用本包](#use-this-package)中完整说明。 + +### 设计理念 + +该工具是基于 `ctx.fs` 的单一 schema、四个命令。修改操作绝不带着自己的假设直接触碰提供方:每个操作都运行 `fs/write-intent` 或 `fs/edit-intent` waterfall 以取得策略插件的防护,在已挂载的 `ctx.fs` 实施沙箱限制时解析按调用沙箱策略,并把强制执行委托给提供方。`str_replace` 与 `insert` 还会重新读取文件,并在没有策略插件提供防护时把观察到的版本作为比较并交换的基础。 + +### 源码地图 + +| 文件 | 职责 | +|---|---| +| [`src/index.ts`](src/index.ts) | 整个工具:schema、命令分派、查看渲染、修改策略 | + +### 各命令如何运行 + +每个命令都先解析绝对路径;修改操作随后遵循同一条共享流程——策略防护、提供方强制执行、成功后记录 `fs/observed`——而 `view` 只执行 stat 并渲染。整个工具——schema、命令分派与查看渲染——都位于 `src/index.ts`。 + +
+ +----- + + +## 进一步探索 + +当包级约定不够用时阅读以下页面。它们从工具逐步进入它所组合的约定、策略与后端。 + +- [文件系统子系统](../../../docs/subsystems/filesystem.zh.md)——穷尽式提供方约定、策略事件与错误分类体系。 +- [dsh-fs](../fs/README.zh.md)——本工具消费的 `ctx.fs` 约定。 +- [tool-fs](../tool-fs/README.zh.md)——替代的 `read`/`write`/`edit` 工具套件。 +- [fs-observation-policy](../fs-observation-policy/README.zh.md)——通过 `fs/*` 事件防护变更的策略插件。 +- [fs-sandbox](../fs-sandbox/README.zh.md)——围栏变更的沙箱强制后端。 +- [生成工具目录](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-str-replace-editor)——本包注册的穷尽式 schema。 + +----- + + +## 模型体验 + +### 工具 schema + +#### 模型看到的内容 + +生成的 [`str_replace_editor` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-str-replace-editor),包含配置的 `description`。本插件不贡献独立系统提示词段。 + +#### Token 影响 + +`str_replace_editor` 可见时产生固定的 schema 成本。 + +#### KV Cache 影响 + +配置的描述与 schema 不变时前缀稳定。 + +### 工具结果 + +#### 模型看到的内容 + +查看操作返回带行号文本或浅层目录列表。调用会提供文件位置,创建/替换调用还会向展示层提供 diff 卡片。修改操作返回简洁确认。长查看结果保留前缀并追加截断提示。 + +#### Token 影响 + +随数据变化,并受 `maxOutputChars` 与固定截断提示约束。 + +#### KV Cache 影响 + +工具结果以追加方式位于可复用请求前缀之后。 + +## 已知限制与延期工作 + + + + +这些限制说明编辑器工具何时不合适,或何时需要特别的运维注意。它们是当前包约束,不是通用编辑器对比或任务积压。 + +- **操作面向 UTF-8 文本**——不支持二进制文件。 +- **`str_replace` 刻意拒绝零匹配或多匹配**——它没有 `replace_all` 参数。 +- **每个修改操作都会经过已挂载的策略与沙箱**——`fs/write-intent` 或 `fs/edit-intent` 解析当前会话的沙箱策略,并把强制执行委托给已挂载的文件系统与策略插件,因此未挂载它们的部署会得到无条件变更。 + + +### 开发备注 + +
+维护者的工作上下文——点击展开 + +无。 + +
+ +**运行时不变式:** 不发布伴生入口。tool adapter 不持有独立持久状态;filesystem mutation 关系属于 provider 与 policy 插件。 diff --git a/packages/fs/tool-str-replace-editor/package.json b/packages/fs/tool-str-replace-editor/package.json new file mode 100644 index 0000000000..d989328728 --- /dev/null +++ b/packages/fs/tool-str-replace-editor/package.json @@ -0,0 +1,53 @@ +{ + "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.2-rc.1", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", + "directory": "packages/fs/tool-str-replace-editor" + }, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "lib/index.js", + "lib/types/**/*.d.ts" + ], + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/dsh-fs": "workspace:^", + "@deepseek-ai/dsh-sandbox": "workspace:^", + "@deepseek-ai/dsh-sandbox-policy": "workspace:^", + "@deepseek-ai/dsh-tools": "workspace:^", + "@deepseek-ai/cordis": "workspace:^" + }, + "dependencies": { + "@deepseek-ai/schemastery": "workspace:^" + }, + "devDependencies": { + "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-fs": "workspace:^", + "@deepseek-ai/dsh-fs-local": "workspace:^", + "@deepseek-ai/dsh-fs-observation-policy": "workspace:^", + "@deepseek-ai/dsh-fs-sandbox": "workspace:^", + "@deepseek-ai/dsh-llm": "workspace:^", + "@deepseek-ai/dsh-sandbox": "workspace:^", + "@deepseek-ai/dsh-sandbox-policy": "workspace:^", + "@deepseek-ai/dsh-session": "workspace:^", + "@deepseek-ai/dsh-system-prompt": "workspace:^", + "@deepseek-ai/dsh-tools": "workspace:^", + "@deepseek-ai/cordis": "workspace:^", + "@deepseek-ai/dsh-session-projection": "workspace:^" + } +} diff --git a/packages/fs/tool-str-replace-editor/src/index.ts b/packages/fs/tool-str-replace-editor/src/index.ts new file mode 100644 index 0000000000..f77736685e --- /dev/null +++ b/packages/fs/tool-str-replace-editor/src/index.ts @@ -0,0 +1,531 @@ +/** + * Model-facing `str_replace_editor` over the Harness filesystem seam. + * @module @deepseek-ai/dsh-tool-str-replace-editor + */ + +import { isAbsolute } from 'node:path' +import type { Context } from '@deepseek-ai/cordis' +import z from '@deepseek-ai/schemastery' +import { FsError } from '@deepseek-ai/dsh-fs' +import type { FsInfo, FsTarget, FsWriteIntent } from '@deepseek-ai/dsh-fs' +import { sandboxDenialMarker } from '@deepseek-ai/dsh-sandbox' +import type { SandboxExecutionPolicy } from '@deepseek-ai/dsh-sandbox' +import type { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy' +import { defineTool } from '@deepseek-ai/dsh-tools' +import type { ToolCallView, ToolRunContext } from '@deepseek-ai/dsh-tools' + +const TRUNCATED_MESSAGE = 'To save on context only part of this file has been shown to you. You should retry this tool after you have searched inside the file with `grep -n` in order to find the line numbers of what you are looking for.' + +const DEFAULT_DESCRIPTION = ` +Custom editing tool for viewing, creating and editing files +* State is persistent across command calls and discussions with the user +* If \`path\` is a file, \`view\` displays the result of applying \`cat -n\`. If \`path\` is a directory, \`view\` lists non-hidden files and directories up to 2 levels deep +* The \`create\` command cannot be used if the specified \`path\` already exists as a file +* If a \`command\` generates a long output, it will be truncated and marked with \`\` +* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit \`str_replace.new_str\` rather than setting it to null when deleting a match + +Notes for using the \`str_replace\` command: +* The \`old_str\` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces! +* If the \`old_str\` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in \`old_str\` to make it unique +* The \`new_str\` parameter should contain the edited lines that should replace the \`old_str\` +`.trim() + +function maybeTruncate(content: string, maxOutputChars: number): string { + return content.length <= maxOutputChars + ? content + : content.slice(0, maxOutputChars) + TRUNCATED_MESSAGE +} + +function codepointCompare(left: string, right: string): number { + return left < right ? -1 : left > right ? 1 : 0 +} + +function matchOffsets(content: string, search: string): number[] { + const offsets: number[] = [] + let offset = 0 + while (true) { + const match = content.indexOf(search, offset) + if (match < 0) return offsets + offsets.push(match) + offset = match + search.length + } +} + +function lineNumbersAt(content: string, offsets: readonly number[]): number[] { + let line = 1 + let cursor = 0 + return offsets.map((offset) => { + while (cursor < offset) { + if (content[cursor] === '\n') line += 1 + cursor += 1 + } + return line + }) +} + +class MutationPolicy { + private readonly policy: SandboxPolicyService | undefined + + constructor(ctx: Context) { + this.policy = ctx.fs.sandboxMode === undefined ? undefined : ctx.get('sandboxPolicy') + if (ctx.fs.sandboxMode !== undefined && this.policy === undefined) { + throw new Error('tool-str-replace-editor: the mounted filesystem confines but ctx.sandboxPolicy is missing') + } + } + + resolve(exec: ToolRunContext): SandboxExecutionPolicy | undefined { + return this.policy?.resolve({ + ...exec.agent === undefined ? {} : { session: exec.agent.session }, + }) + } + + mapError(error: unknown, policy: SandboxExecutionPolicy | undefined): unknown { + if (!(error instanceof FsError) || error.code !== 'FS_SANDBOX_DENIED') return error + const mode = (policy as SandboxExecutionPolicy).mode + return new FsError(sandboxDenialMarker(mode), 'FS_SANDBOX_DENIED', { cause: error }) + } +} + +async function resolveTarget( + ctx: Context, + path: string, + signal: AbortSignal, +): Promise { + if (path.trim().length === 0) throw new Error('path must be a non-empty string') + if (!isAbsolute(path)) { + throw new Error(`The path ${path} is not an absolute path, it should start with \`/\`. Maybe you meant /${path}?`) + } + return ctx.fs.resolve(path, { signal }) +} + +async function statExisting( + ctx: Context, + target: FsTarget, + command: 'view' | 'str_replace' | 'insert', + exec: ToolRunContext, +): Promise { + const info = await ctx.fs.stat(target, exec.signal) + if (info === undefined) { + ctx.emit('fs/observed', target, { kind: 'absent' }, exec) + throw new FsError( + `The path ${target.displayPath} does not exist. Please provide a valid path.`, + 'FS_NOT_FOUND', + ) + } + if (info.type === 'directory' && command !== 'view') { + throw new FsError( + `The path ${target.displayPath} is a directory and only the \`view\` command can be used on directories`, + 'FS_NOT_REGULAR_FILE', + ) + } + return info +} + +function requiredForCommand( + value: string | undefined, + parameter: string, + command: string, + allowEmpty = true, +): string { + if (value === undefined) throw new Error(`Parameter \`${parameter}\` is required for command: ${command}`) + if (!allowEmpty && value.length === 0) { + throw new Error(`Parameter \`${parameter}\` is empty for command: ${command}`) + } + return value +} + +function formatFileView( + path: string, + content: string, + maxOutputChars: number, + viewRange?: number[], +): string { + const allLines = content.split('\n') + let lines = allLines + let initialLine = 1 + let finalLine: number | undefined + let prompt = `Here's the content of ${path} with line numbers (which has a total of ${allLines.length} lines)` + if (viewRange !== undefined) { + const [requestedInitialLine, requestedFinalLine] = viewRange + if ( + viewRange.length !== 2 + || requestedInitialLine === undefined + || requestedFinalLine === undefined + || !viewRange.every(Number.isInteger) + ) { + throw new Error('Invalid `view_range`. It should be a list of two integers.') + } + initialLine = requestedInitialLine + finalLine = requestedFinalLine + if (initialLine < 1 || initialLine > allLines.length) { + throw new Error( + `Invalid \`view_range\`: [${viewRange.join(', ')}]. Its first element \`${initialLine}\` should be within the range of lines of the file: [1, ${allLines.length}]`, + ) + } + if (finalLine > allLines.length) { + throw new Error( + `Invalid \`view_range\`: [${viewRange.join(', ')}]. Its second element \`${finalLine}\` should be smaller than the number of lines in the file: \`${allLines.length}\``, + ) + } + if (finalLine !== -1 && finalLine < initialLine) { + throw new Error( + `Invalid \`view_range\`: [${viewRange.join(', ')}]. Its second element \`${finalLine}\` should be larger or equal than its first \`${initialLine}\``, + ) + } + lines = finalLine === -1 + ? allLines.slice(initialLine - 1) + : allLines.slice(initialLine - 1, finalLine) + prompt += ` with view_range=[${initialLine}, ${finalLine}]` + } + const numbered = lines + .map((line, index) => `${String(initialLine + index).padStart(6, ' ')} ${line}`) + .join('\n') + return maybeTruncate(`${prompt}:\n${numbered}\n`, maxOutputChars) +} + +async function listDirectory( + ctx: Context, + target: FsTarget, + maxOutputChars: number, + exec: ToolRunContext, +): Promise { + async function visit(dir: FsTarget, depth: number): Promise { + const entries = await ctx.fs.listDir(dir, exec.signal) + const rows: string[] = [] + for (const entry of entries.filter(candidate => + !candidate.name.startsWith('.') + && candidate.name !== 'node_modules' + && candidate.name !== '__pycache__')) { + const type = entry.type === 'directory' ? 'd' : entry.type === 'file' ? 'f' : '?' + rows.push(`${type}\t${entry.target.displayPath}`) + if (entry.type === 'directory' && depth < 2) { + rows.push(...await visit(entry.target, depth + 1)) + } + } + return rows + } + const rows = [`d\t${target.displayPath}`, ...await visit(target, 1)] + rows.sort((left, right) => { + const leftPath = left.slice(left.indexOf('\t') + 1) + const rightPath = right.slice(right.indexOf('\t') + 1) + return codepointCompare(leftPath, rightPath) + }) + const listing = maybeTruncate(rows.join('\n') + '\n', maxOutputChars) + return `Here're the files and directories up to 2 levels deep in ${target.displayPath}, excluding hidden items, node_modules, and Python cache directories:\n${listing}\n` +} + +async function viewPath( + ctx: Context, + path: string, + viewRange: number[] | undefined, + maxOutputChars: number, + exec: ToolRunContext, +): Promise { + const target = await resolveTarget(ctx, path, exec.signal) + const info = await statExisting(ctx, target, 'view', exec) + if (info.type === 'directory') { + if (viewRange !== undefined) { + throw new Error('The `view_range` parameter is not allowed when `path` points to a directory.') + } + return listDirectory(ctx, target, maxOutputChars, exec) + } + if (info.type !== 'file') { + throw new FsError(`cannot view "${target.displayPath}": not a regular file or directory`, 'FS_NOT_REGULAR_FILE') + } + const content = await ctx.fs.readText(target, exec.signal) + ctx.emit('fs/observed', target, { kind: 'present', version: info.version }, exec) + return formatFileView(target.displayPath, content, maxOutputChars, viewRange) +} + +async function createFile( + ctx: Context, + policy: MutationPolicy, + path: string, + fileText: string | undefined, + exec: ToolRunContext, +): Promise { + const content = requiredForCommand(fileText, 'file_text', 'create') + const sandboxPolicy = policy.resolve(exec) + const target = await resolveTarget(ctx, path, exec.signal) + if (await ctx.fs.stat(target, exec.signal) !== undefined) { + throw new Error(`File already exists at: ${target.displayPath}. Cannot overwrite files using command \`create\`.`) + } + const intent = await ctx.waterfall( + 'fs/write-intent', + target, + exec, + () => ({ kind: 'createIfAbsent' } as const), + ) + let outcome + try { + outcome = await ctx.fs.writeText( + target, + content, + intent, + exec.signal, + sandboxPolicy, + ) + } catch (error: unknown) { + throw policy.mapError(error, sandboxPolicy) + } + ctx.emit('fs/observed', target, { kind: 'present', version: outcome.version }, exec) + return `New file created successfully at: ${target.displayPath}` +} + +async function replaceInFile( + ctx: Context, + policy: MutationPolicy, + path: string, + oldStr: string | undefined, + newStr: string | null | undefined, + exec: ToolRunContext, +): Promise { + if (newStr === null) { + throw new Error('Parameter `new_str` must be omitted or contain a string for command: str_replace') + } + const sandboxPolicy = policy.resolve(exec) + const target = await resolveTarget(ctx, path, exec.signal) + const intent = await ctx.waterfall('fs/edit-intent', target, exec, () => undefined) + const oldValue = requiredForCommand(oldStr, 'old_str', 'str_replace', false) + const newValue = newStr ?? '' + const info = await statExisting(ctx, target, 'str_replace', exec) + if (info.type !== 'file') { + throw new FsError(`cannot edit "${target.displayPath}": not a regular file`, 'FS_NOT_REGULAR_FILE') + } + const before = await ctx.fs.readText(target, exec.signal) + const offsets = matchOffsets(before, oldValue) + const offset = offsets[0] + if (offset === undefined) { + throw new FsError( + `No replacement was performed, old_str \`${oldValue}\` did not appear verbatim in ${target.displayPath}.`, + 'FS_EDIT_NOT_FOUND', + ) + } + if (offsets.length > 1) { + const lines = lineNumbersAt(before, offsets) + throw new FsError( + `No replacement was performed. Multiple occurrences of old_str \`${oldValue}\` in lines [${lines.join(', ')}]. Please ensure it is unique`, + 'FS_AMBIGUOUS_EDIT', + ) + } + let outcome + try { + outcome = await ctx.fs.writeText( + target, + before.slice(0, offset) + newValue + before.slice(offset + oldValue.length), + intent === undefined + ? { kind: 'replaceIfVersion', version: info.version } + : { kind: 'replaceIfVersion', version: intent.version }, + exec.signal, + sandboxPolicy, + ) + } catch (error: unknown) { + throw policy.mapError(error, sandboxPolicy) + } + ctx.emit('fs/observed', target, { kind: 'present', version: outcome.version }, exec) + return `The file ${target.displayPath} has been edited successfully.` +} + +async function insertInFile( + ctx: Context, + policy: MutationPolicy, + path: string, + insertLine: number | undefined, + newStr: string | undefined, + exec: ToolRunContext, +): Promise { + if (insertLine === undefined) throw new Error('Parameter `insert_line` is required for command: insert') + const value = requiredForCommand(newStr, 'new_str', 'insert') + const sandboxPolicy = policy.resolve(exec) + const target = await resolveTarget(ctx, path, exec.signal) + const intent = await ctx.waterfall('fs/edit-intent', target, exec, () => undefined) + const info = await statExisting(ctx, target, 'insert', exec) + if (info.type !== 'file') { + throw new FsError(`cannot insert into "${target.displayPath}": not a regular file`, 'FS_NOT_REGULAR_FILE') + } + const before = await ctx.fs.readText(target, exec.signal) + const lines = before.split('\n') + if (!Number.isInteger(insertLine) || insertLine < 0 || insertLine > lines.length) { + throw new Error( + `Invalid \`insert_line\` parameter: ${insertLine}. It should be within the range of lines of the file: [0, ${lines.length}]`, + ) + } + const after = [ + ...lines.slice(0, insertLine), + ...value.split('\n'), + ...lines.slice(insertLine), + ].join('\n') + const expected: FsWriteIntent = intent === undefined + ? { kind: 'replaceIfVersion', version: info.version } + : { kind: 'replaceIfVersion', version: intent.version } + let outcome + try { + outcome = await ctx.fs.writeText(target, after, expected, exec.signal, sandboxPolicy) + } catch (error: unknown) { + throw policy.mapError(error, sandboxPolicy) + } + ctx.emit('fs/observed', target, { kind: 'present', version: outcome.version }, exec) + return `The file ${target.displayPath} has been edited successfully.` +} + +interface ResolvedConfig { + maxOutputChars: number + description: string +} + +function presentEditorCall(args: { + command: 'view' | 'create' | 'str_replace' | 'insert' + path: string + file_text?: string | null + insert_line?: number | null + new_str?: string | null + old_str?: string | null +}): ToolCallView { + switch (args.command) { + case 'view': + return { + card: 'generic', + title: `view ${args.path}`, + kind: 'read', + locations: [{ path: args.path }], + } + case 'create': + return { + card: 'diff', + title: `create ${args.path}`, + diffs: [{ path: args.path, oldText: null, newText: args.file_text ?? '' }], + locations: [{ path: args.path }], + } + case 'str_replace': + return { + card: 'diff', + title: `str_replace ${args.path}`, + diffs: [{ + path: args.path, + oldText: args.old_str ?? null, + newText: args.new_str ?? '', + }], + locations: [{ path: args.path }], + } + case 'insert': + return { + card: 'generic', + title: `insert ${args.path}`, + kind: 'edit', + locations: [{ + path: args.path, + ...args.insert_line === undefined || args.insert_line === null + ? {} + : { line: Math.max(1, args.insert_line + 1) }, + }], + } + } +} + +/** Register the model-facing `str_replace_editor` tool. */ +function registerStrReplaceEditor(ctx: Context, config: ResolvedConfig): void { + const policy = new MutationPolicy(ctx) + ctx.tools.register(defineTool({ + name: 'str_replace_editor', + description: config.description, + parameters: { + command: { + type: 'string', + required: true, + enum: ['view', 'create', 'str_replace', 'insert'], + description: 'The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.', + }, + path: { + type: 'string', + required: true, + description: 'Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`.', + }, + file_text: { + oneOf: [{ type: 'string' }, { type: 'null' }], + description: 'Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter.', + }, + insert_line: { + oneOf: [{ type: 'integer' }, { type: 'null' }], + description: 'Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter.', + }, + new_str: { + oneOf: [{ type: 'string' }, { type: 'null' }], + description: 'Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter.', + }, + old_str: { + oneOf: [{ type: 'string' }, { type: 'null' }], + description: 'Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter.', + }, + view_range: { + oneOf: [ + { type: 'array', items: { type: 'integer' } }, + { type: 'null' }, + ], + description: 'Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file.', + }, + }, + output: { + schema: { type: 'string' }, + render: (_args, value) => [{ type: 'text', text: value }], + }, + async execute(args, exec) { + switch (args.command) { + case 'view': + return viewPath(ctx, args.path, args.view_range ?? undefined, config.maxOutputChars, exec) + case 'create': + return createFile(ctx, policy, args.path, args.file_text ?? undefined, exec) + case 'str_replace': + return replaceInFile( + ctx, + policy, + args.path, + args.old_str ?? undefined, + args.new_str, + exec, + ) + case 'insert': + return insertInFile( + ctx, + policy, + args.path, + args.insert_line ?? undefined, + args.new_str ?? undefined, + exec, + ) + } + }, + presentCall: presentEditorCall, + })) +} + +export const name = 'tool-str-replace-editor' +export const inject = ['tools', 'fs'] + +/** Configuration for the string-replacement editor tool. */ +export interface Config { + /** Maximum returned view characters before clipping (default 16000). */ + maxOutputChars?: number + /** Model-facing tool description. */ + description?: string +} + +/** Runtime configuration schema for the string-replacement editor tool. */ +export const Config: z = z.object({ + maxOutputChars: z.number().default(16_000), + description: z.string().default(DEFAULT_DESCRIPTION), +}) + +/** Register one `str_replace_editor` tool over `ctx.fs`. */ +export function apply(ctx: Context, config: Config): void { + const resolved: ResolvedConfig = { + maxOutputChars: config.maxOutputChars ?? 16_000, + description: config.description ?? DEFAULT_DESCRIPTION, + } + if (!Number.isSafeInteger(resolved.maxOutputChars) || resolved.maxOutputChars <= 0) { + throw new Error('tool-str-replace-editor: maxOutputChars must be a positive safe integer') + } + if (resolved.description.trim().length === 0) { + throw new Error('tool-str-replace-editor: description must be non-empty') + } + registerStrReplaceEditor(ctx, resolved) +} diff --git a/packages/fs/tool-str-replace-editor/tests/tools.spec.ts b/packages/fs/tool-str-replace-editor/tests/tools.spec.ts new file mode 100644 index 0000000000..0965db21d2 --- /dev/null +++ b/packages/fs/tool-str-replace-editor/tests/tools.spec.ts @@ -0,0 +1,643 @@ +import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { Context } from '@deepseek-ai/cordis' +import { FsVersion } from '@deepseek-ai/dsh-fs' +import { ToolCallId } from '@deepseek-ai/dsh-llm' +import { Session, SessionId } from '@deepseek-ai/dsh-session' +import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent' +import type { Agent } from '@deepseek-ai/dsh-agent' +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' + +const contexts: Context[] = [] +const roots: string[] = [] +let callNumber = 0 + +afterEach(async () => { + for (const ctx of contexts.splice(0)) await ctx.fiber.dispose() + for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true }) +}) + +function agent(ctx: Context, cwd: string): Agent { + const id = SessionId(`str-replace-editor-owner-${callNumber}`) + const scope = ctx.plugin(() => {}) + const session = Session.create(id, [], { version: 0, id, createdAt: 0, cwd, isSeeded: false }) + const value: Agent = { + id, + options: {}, + session, + inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }), + status: 'idle', + ctx: scope.ctx, + send: () => {}, + followup: () => {}, + steer: () => ({ outcome: Promise.resolve({ status: 'rejected' as const }) }), + inject: () => {}, + cancel() {}, + runMaintenance: task => task(new AbortController().signal), + whenIdle: () => Promise.resolve(), + } + ctx.agents.register(value) + return value +} + +function text(result: { content: { type: string; text?: string }[] }): string { + return result.content.filter(block => block.type === 'text').map(block => block.text).join('') +} + +function call(ctx: Context, owner: Agent | undefined, args: unknown) { + return ctx.tools.execute({ + signal: new AbortController().signal, + callId: ToolCallId(`str-replace-editor-${++callNumber}`), + name: 'str_replace_editor', + arguments: args, + ...owner === undefined ? {} : { agent: owner }, + }) +} + +async function setup( + config: ToolStrReplaceEditor.Config = {}, + options: { fsPolicy?: boolean; sandboxMode?: 'read-only' | 'workspace-write' | 'danger-full-access' } = {}, +) { + const root = await mkdtemp(join(tmpdir(), 'dsh-tool-str-replace-editor-')) + roots.push(root) + const ctx = new Context() + contexts.push(ctx) + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRuntime) + await ctx.plugin(AgentRegistry) + 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 }) + } + if (options.fsPolicy === true) await ctx.plugin(FsPolicy) + const fiber = await ctx.plugin(ToolStrReplaceEditor, config) + return { ctx, root, fiber, owner: agent(ctx, root) } +} + +describe('tool-str-replace-editor', () => { + it('registers the standalone schema and configurable description', async () => { + const { ctx, fiber } = await setup({ description: 'custom editor description' }) + const schema = ctx.tools.schemas()[0] + expect(ctx.tools.schemas().map(item => item.name)).toEqual(['str_replace_editor']) + expect(schema?.description).toBe('custom editor description') + const properties = (schema?.parameters as { + properties: Record + }).properties + expect(properties).not.toHaveProperty('replace_all') + expect(properties.file_text?.oneOf?.map(option => option.type)).toEqual(['string', 'null']) + expect(properties.insert_line?.oneOf?.map(option => option.type)).toEqual(['integer', 'null']) + expect(properties.new_str?.oneOf?.map(option => option.type)).toEqual(['string', 'null']) + expect(properties.old_str?.oneOf?.map(option => option.type)).toEqual(['string', 'null']) + expect(properties.view_range?.oneOf?.map(option => option.type)).toEqual(['array', 'null']) + expect(properties.view_range?.oneOf?.[0]?.items?.type).toBe('integer') + expect(ctx.tools.get('str_replace_editor')?.presentCall?.({ + command: 'view', + path: '/workspace/a.txt', + file_text: null, + insert_line: null, + new_str: null, + old_str: null, + view_range: null, + })).toMatchObject({ + card: 'generic', + kind: 'read', + locations: [{ path: '/workspace/a.txt' }], + }) + expect(ctx.tools.get('str_replace_editor')?.presentCall?.({ + command: 'create', + path: '/workspace/a.txt', + file_text: 'hello', + insert_line: null, + new_str: null, + old_str: null, + view_range: null, + })).toMatchObject({ + card: 'diff', + diffs: [{ path: '/workspace/a.txt', oldText: null, newText: 'hello' }], + }) + expect(ctx.tools.get('str_replace_editor')?.presentCall?.({ + command: 'str_replace', + path: '/workspace/a.txt', + old_str: 'old', + new_str: 'new', + file_text: null, + insert_line: null, + view_range: null, + })).toMatchObject({ + card: 'diff', + diffs: [{ path: '/workspace/a.txt', oldText: 'old', newText: 'new' }], + }) + expect(ctx.tools.get('str_replace_editor')?.presentCall?.({ + command: 'insert', + path: '/workspace/a.txt', + insert_line: null, + new_str: 'x', + })).toMatchObject({ + card: 'generic', + kind: 'edit', + locations: [{ path: '/workspace/a.txt' }], + }) + expect(ctx.tools.get('str_replace_editor')?.presentCall?.({ + command: 'insert', + path: '/workspace/a.txt', + insert_line: 0, + new_str: 'x', + file_text: null, + old_str: null, + view_range: null, + })).toMatchObject({ + card: 'generic', + kind: 'edit', + locations: [{ path: '/workspace/a.txt', line: 1 }], + }) + expect(ctx.tools.get('str_replace_editor')?.presentCall?.({ + command: 'create', + path: '/workspace/empty.txt', + })).toMatchObject({ + diffs: [{ path: '/workspace/empty.txt', oldText: null, newText: '' }], + }) + expect(ctx.tools.get('str_replace_editor')?.presentCall?.({ + command: 'str_replace', + path: '/workspace/a.txt', + })).toMatchObject({ + diffs: [{ path: '/workspace/a.txt', oldText: null, newText: '' }], + }) + expect(ctx.tools.get('str_replace_editor')?.presentCall?.({ + command: 'insert', + path: '/workspace/a.txt', + })).toMatchObject({ + locations: [{ path: '/workspace/a.txt' }], + }) + + await fiber.dispose() + expect(ctx.tools.schemas()).toEqual([]) + expect(ctx.tools.get('str_replace_editor')).toBeUndefined() + }) + + it('creates, views, replaces, and inserts with the canonical model-facing output', async () => { + const { ctx, root, owner } = await setup() + const sample = join(root, 'sample.txt') + expect(text(await call(ctx, owner, { + command: 'create', + path: sample, + file_text: 'one\ntwo\nthree\n', + insert_line: null, + new_str: null, + old_str: null, + view_range: null, + }))).toBe(`New file created successfully at: ${sample}`) + + expect(text(await call(ctx, owner, { + command: 'view', + path: sample, + file_text: null, + insert_line: null, + new_str: null, + old_str: null, + view_range: null, + }))).toContain(' 2 two') + + expect(text(await call(ctx, owner, { + command: 'view', + path: sample, + view_range: [2, -1], + }))).toBe([ + `Here's the content of ${sample} with line numbers (which has a total of 4 lines) with view_range=[2, -1]:`, + ' 2 two', + ' 3 three', + ' 4 ', + '', + ].join('\n')) + + expect(text(await call(ctx, owner, { + command: 'str_replace', + path: sample, + old_str: 'two', + new_str: 'TWO', + file_text: null, + insert_line: null, + view_range: null, + }))).toBe(`The file ${sample} has been edited successfully.`) + expect(text(await call(ctx, owner, { + command: 'str_replace', + path: sample, + old_str: 'TWO', + }))).toBe(`The file ${sample} has been edited successfully.`) + expect(text(await call(ctx, owner, { + command: 'insert', + path: sample, + insert_line: 1, + new_str: 'between', + file_text: null, + old_str: null, + view_range: null, + }))).toBe(`The file ${sample} has been edited successfully.`) + expect(await readFile(sample, 'utf8')).toBe('one\nbetween\n\nthree\n') + }) + + it('a failed view records absence so create can recover after external deletion', async () => { + const { ctx, root, owner } = await setup({}, { fsPolicy: true }) + const sample = join(root, 'deleted.txt') + await writeFile(sample, 'original') + expect((await call(ctx, owner, { command: 'view', path: sample })).isError).toBe(false) + await rm(sample) + + const missing = await call(ctx, owner, { command: 'view', path: sample }) + expect(missing.isError).toBe(true) + expect(missing.error).toMatchObject({ info: { code: 'FS_NOT_FOUND' } }) + + const edit = await call(ctx, owner, { + command: 'str_replace', + path: sample, + old_str: 'original', + new_str: 'edited', + }) + expect(edit.isError).toBe(true) + expect(edit.error).toMatchObject({ info: { code: 'FS_NOT_FOUND' } }) + + const created = await call(ctx, owner, { + command: 'create', + path: sample, + file_text: 'fresh', + }) + expect(created.isError).toBe(false) + expect(await readFile(sample, 'utf8')).toBe('fresh') + }) + + it('writes replacement text literally', async () => { + const { ctx, root, owner } = await setup() + const sample = join(root, 'literal.txt') + const replacement = "$&|$`|$'|$$" + await writeFile(sample, 'before OLD after') + + expect((await call(ctx, owner, { + command: 'str_replace', + path: sample, + old_str: 'OLD', + new_str: replacement, + })).isError).toBe(false) + expect(await readFile(sample, 'utf8')).toBe(`before ${replacement} after`) + }) + + it('lists visible entries to depth two and clips at the configured view limit', async () => { + const { ctx, root, owner } = await setup({ maxOutputChars: 10_000 }) + await mkdir(join(root, 'dir', 'nested', 'third'), { recursive: true }) + await mkdir(join(root, 'dir', 'node_modules', 'pkg'), { recursive: true }) + await mkdir(join(root, 'dir', 'node_modules_old'), { recursive: true }) + await mkdir(join(root, 'dir', '__pycache__'), { recursive: true }) + await mkdir(join(root, 'dir', '__pycache__backup'), { recursive: true }) + await writeFile(join(root, 'dir', 'visible.txt'), 'ok') + await writeFile(join(root, 'dir', '.hidden'), 'hidden') + await writeFile(join(root, 'dir', 'nested', 'child.txt'), 'child') + await writeFile(join(root, 'dir', 'nested', 'third', 'too-deep.txt'), 'deep') + await writeFile(join(root, 'dir', 'node_modules', 'pkg', 'index.js'), 'hidden dependency') + await writeFile(join(root, 'dir', 'node_modules_old', 'kept.js'), 'visible source') + await writeFile(join(root, 'dir', '__pycache__', 'module.pyc'), 'cache') + await writeFile(join(root, 'dir', '__pycache__backup', 'kept.py'), 'visible source') + const listDir = ctx.fs.listDir.bind(ctx.fs) + const otherTarget = await ctx.fs.resolve(join(root, 'dir', 'other')) + ctx.fs.listDir = async (target, signal) => { + const entries = await listDir(target, signal) + return target.displayPath === join(root, 'dir') + ? [ + { name: 'same-target', type: 'other', target: otherTarget }, + { name: 'other', type: 'other', target: otherTarget }, + ...entries.toReversed(), + ] + : entries + } + + const listing = text(await call(ctx, owner, { command: 'view', path: join(root, 'dir') })) + expect(listing).not.toContain('.hidden') + expect(listing).not.toContain('too-deep.txt') + expect(listing).not.toContain('index.js') + expect(listing).not.toContain('module.pyc') + // The listing carries absolute display paths; the POSIX-style substrings + // only match on Linux, so assert with platform separators. + expect(listing).toContain(join('node_modules_old', 'kept.js')) + expect(listing).toContain(join('__pycache__backup', 'kept.py')) + + const clipped = await setup({ maxOutputChars: 10 }) + await writeFile(join(clipped.root, 'large.txt'), 'x'.repeat(100)) + expect(text(await call(clipped.ctx, clipped.owner, { + command: 'view', + path: join(clipped.root, 'large.txt'), + }))) + .toContain('') + }) + + it('matches canonical empty-line, range, and end-insert behavior', async () => { + const { ctx, root, owner } = await setup() + const empty = join(root, 'empty.txt') + const newline = join(root, 'newline.txt') + const plain = join(root, 'plain.txt') + await writeFile(empty, '') + await writeFile(newline, '\n') + await writeFile(plain, 'one\ntwo') + + expect(text(await call(ctx, owner, { command: 'view', path: empty }))) + .toContain('(which has a total of 1 lines):\n 1 \n') + expect(text(await call(ctx, owner, { command: 'view', path: newline }))) + .toContain('(which has a total of 2 lines):\n 1 \n 2 \n') + expect(text(await call(ctx, owner, { + command: 'view', + path: plain, + view_range: [1, 2], + }))).toContain(' 2 two') + expect(text(await call(ctx, undefined, { + command: 'view', + path: plain, + }))).toContain(' 1 one') + expect((await call(ctx, undefined, { + command: 'create', + path: join(root, 'ownerless.txt'), + file_text: 'ownerless', + })).isError).toBe(false) + + await call(ctx, owner, { + command: 'insert', + path: plain, + insert_line: 2, + new_str: 'three', + }) + expect(await readFile(plain, 'utf8')).toBe('one\ntwo\nthree') + + await writeFile(newline, 'one\n') + await call(ctx, owner, { + command: 'insert', + path: newline, + insert_line: 2, + new_str: 'three', + }) + expect(await readFile(newline, 'utf8')).toBe('one\n\nthree') + }) + + it('uses old_str-only replacement failures and rejects relative paths', async () => { + const { ctx, root, owner } = await setup() + const ambiguous = join(root, 'ambiguous.txt') + await writeFile(ambiguous, 'same\nother\nsame') + + const missing = await call(ctx, owner, { + command: 'str_replace', + path: ambiguous, + old_str: 'absent', + new_str: 'x', + }) + expect(missing.isError).toBe(true) + expect(text(missing)).toContain(`old_str \`absent\` did not appear verbatim in ${ambiguous}`) + expect(text(missing)).not.toContain('old_string') + + const repeated = await call(ctx, owner, { + command: 'str_replace', + path: ambiguous, + old_str: 'same', + new_str: 'x', + }) + expect(repeated.isError).toBe(true) + expect(text(repeated)).toContain('Multiple occurrences of old_str `same` in lines [1, 3]') + expect(text(repeated)).not.toContain('replace_all') + + await writeFile(ambiguous, 'alpha\nbeta\nmiddle\nalpha\nbeta') + const repeatedMultiline = await call(ctx, owner, { + command: 'str_replace', + path: ambiguous, + old_str: 'alpha\nbeta', + new_str: 'x', + }) + expect(text(repeatedMultiline)) + .toContain('Multiple occurrences of old_str `alpha\nbeta` in lines [1, 4]') + + const mixedEol = join(root, 'mixed-eol.txt') + await writeFile(mixedEol, 'alpha\r\nbeta\nmiddle\nalpha\nbeta') + expect((await call(ctx, owner, { + command: 'str_replace', + path: mixedEol, + old_str: 'alpha\r\nbeta', + new_str: 'replaced', + })).isError).toBe(false) + expect(await readFile(mixedEol, 'utf8')).toBe('replaced\nmiddle\nalpha\nbeta') + + const relative = await call(ctx, owner, { command: 'view', path: 'ambiguous.txt' }) + expect(relative.isError).toBe(true) + expect(text(relative)).toContain('is not an absolute path') + expect(await readFile(ambiguous, 'utf8')).toBe('alpha\nbeta\nmiddle\nalpha\nbeta') + }) + + it('reports invalid commands or arguments without mutating files', async () => { + const { ctx, root, owner } = await setup() + const ambiguous = join(root, 'ambiguous.txt') + const empty = join(root, 'empty.txt') + const trailingNewline = join(root, 'trailing-newline.txt') + const threeLines = join(root, 'three-lines.txt') + const directory = join(root, 'directory') + await writeFile(ambiguous, 'same same') + await writeFile(empty, '') + await writeFile(trailingNewline, 'one\n') + await writeFile(threeLines, 'one\ntwo\nthree') + await mkdir(directory) + + const cases = [ + { command: null, path: ambiguous }, + { command: 'view', path: null }, + { command: 'view', path: '' }, + { command: 'view', path: join(root, 'missing.txt') }, + { command: 'view', path: ambiguous, view_range: [1] }, + { command: 'view', path: ambiguous, view_range: [0, 1] }, + { command: 'view', path: ambiguous, view_range: [1.5, 2] }, + { command: 'view', path: threeLines, view_range: [1, 99] }, + { command: 'view', path: threeLines, view_range: [2, 1] }, + { command: 'view', path: directory, view_range: [1, 1] }, + { command: 'create', path: join(root, 'new.txt') }, + { command: 'create', path: join(root, 'new.txt'), file_text: null }, + { command: 'create', path: ambiguous, file_text: 'overwrite' }, + { command: 'str_replace', path: ambiguous, new_str: 'x' }, + { command: 'str_replace', path: ambiguous, old_str: null, new_str: 'x' }, + { command: 'str_replace', path: ambiguous, old_str: 'same same', new_str: null }, + { command: 'str_replace', path: ambiguous, old_str: '', new_str: 'x' }, + { command: 'insert', path: ambiguous, new_str: 'x' }, + { command: 'insert', path: ambiguous, insert_line: null, new_str: 'x' }, + { command: 'insert', path: ambiguous, insert_line: 0, new_str: null }, + { command: 'insert', path: ambiguous, insert_line: -1, new_str: 'x' }, + { command: 'insert', path: ambiguous, insert_line: 1.5, new_str: 'x' }, + { command: 'insert', path: ambiguous, insert_line: 99, new_str: 'x' }, + { command: 'insert', path: empty, insert_line: 2, new_str: 'x' }, + { command: 'insert', path: directory, insert_line: 0, new_str: 'x' }, + ] + for (const args of cases) { + expect((await call(ctx, owner, args)).isError).toBe(true) + } + expect(await readFile(ambiguous, 'utf8')).toBe('same same') + + ctx.fs.stat = async () => ({ version: FsVersion('special'), type: 'other' }) + const special = await call(ctx, owner, { command: 'view', path: join(root, 'special') }) + expect(special.isError).toBe(true) + expect(special.error).toMatchObject({ info: { code: 'FS_NOT_REGULAR_FILE' } }) + expect((await call(ctx, owner, { + command: 'str_replace', + path: join(root, 'special'), + old_str: 'x', + new_str: 'y', + })).error).toMatchObject({ info: { code: 'FS_NOT_REGULAR_FILE' } }) + expect((await call(ctx, owner, { + command: 'insert', + path: join(root, 'special'), + insert_line: 0, + new_str: 'x', + })).error).toMatchObject({ info: { code: 'FS_NOT_REGULAR_FILE' } }) + }) + + it('delegates read-before-edit decisions to fs-observation-policy', async () => { + const { ctx, root, owner } = await setup({}, { fsPolicy: true }) + const existing = join(root, 'existing.txt') + const created = join(root, 'created.txt') + await writeFile(existing, 'before') + + const blindEdit = await call(ctx, owner, { + command: 'str_replace', + path: existing, + old_str: 'before', + new_str: 'after', + }) + expect(blindEdit.error).toMatchObject({ info: { code: 'FS_NOT_OBSERVED' } }) + expect(await readFile(existing, 'utf8')).toBe('before') + + await call(ctx, owner, { command: 'view', path: existing }) + expect((await call(ctx, owner, { + command: 'str_replace', + path: existing, + old_str: 'before', + new_str: 'after', + })).isError).toBe(false) + expect(await readFile(existing, 'utf8')).toBe('after') + + expect((await call(ctx, owner, { + command: 'insert', + path: existing, + insert_line: 1, + new_str: 'tail', + })).isError).toBe(false) + expect(await readFile(existing, 'utf8')).toBe('after\ntail') + + expect((await call(ctx, owner, { + command: 'create', + path: created, + file_text: 'new', + })).isError).toBe(false) + expect(await readFile(created, 'utf8')).toBe('new') + }) + + it('passes the session sandbox policy to every mutation', async () => { + const { ctx, root, owner } = await setup({}, { sandboxMode: 'read-only' }) + const path = join(root, 'blocked.txt') + const result = await call(ctx, owner, { + command: 'create', + path, + file_text: 'blocked', + }) + expect(result.error).toMatchObject({ info: { code: 'FS_SANDBOX_DENIED' } }) + expect(text(result)).toContain('[sandbox: file access denied under read-only mode]') + + const ownerless = await call(ctx, undefined, { + command: 'create', + path: join(root, 'ownerless-blocked.txt'), + file_text: 'blocked', + }) + expect(ownerless.error).toMatchObject({ info: { code: 'FS_SANDBOX_DENIED' } }) + }) + + it('preserves tabs outside the edited region', async () => { + const { ctx, root, owner } = await setup() + const path = join(root, 'Makefile') + await writeFile(path, 'target:\n\told\nremove\n') + expect(text(await call(ctx, owner, { command: 'view', path }))) + .toContain(' 2 \told') + await call(ctx, owner, { + command: 'str_replace', + path, + old_str: '\told', + new_str: '\tnew', + }) + await call(ctx, owner, { + command: 'str_replace', + path, + old_str: 'remove\n', + }) + await call(ctx, owner, { + command: 'insert', + path, + insert_line: 1, + new_str: '\tkept', + }) + expect(await readFile(path, 'utf8')).toBe('target:\n\tkept\n\tnew\n') + }) + + it('reports missing sandbox-policy composition during plugin startup', async () => { + const root = await mkdtemp(join(tmpdir(), 'dsh-tool-str-replace-editor-missing-policy-')) + roots.push(root) + const ctx = new Context() + contexts.push(ctx) + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRuntime) + await ctx.plugin(AgentRegistry) + await ctx.plugin(LocalFileSystem, { cwd: root }) + Object.defineProperty(ctx.fs, 'sandboxMode', { value: 'read-only' }) + + await expect(ctx.plugin(ToolStrReplaceEditor)) + .rejects.toThrow('the mounted filesystem confines but ctx.sandboxPolicy is missing') + }) + + it('maps unexpected backend write failures for replace and insert', async () => { + const { ctx, root, owner } = await setup() + const path = join(root, 'backend-error.txt') + await writeFile(path, 'old\n') + const failWrite = async (): Promise => { + throw new Error('backend write failed') + } + ctx.fs.writeText = failWrite + + const replace = await call(ctx, owner, { + command: 'str_replace', + path, + old_str: 'old', + new_str: 'new', + }) + expect(replace.isError).toBe(true) + expect(text(replace)).toContain('backend write failed') + + const insert = await call(ctx, owner, { + command: 'insert', + path, + insert_line: 1, + new_str: 'new', + }) + expect(insert.isError).toBe(true) + expect(text(insert)).toContain('backend write failed') + }) + + it('rejects invalid plugin config', () => { + expect(() => { + ToolStrReplaceEditor.apply(new Context(), { maxOutputChars: 0 }) + }).toThrow('maxOutputChars must be a positive safe integer') + expect(() => { + ToolStrReplaceEditor.apply(new Context(), { description: ' ' }) + }).toThrow('description must be non-empty') + }) +}) diff --git a/packages/fs/tool-str-replace-editor/tsconfig.json b/packages/fs/tool-str-replace-editor/tsconfig.json new file mode 100644 index 0000000000..b9dba81d5d --- /dev/null +++ b/packages/fs/tool-str-replace-editor/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": ["src"], + "references": [ + { "path": "../../../vendor/cordis" }, + { "path": "../../../vendor/schemastery" }, + { "path": "../../core/tools" }, + { "path": "../fs" }, + { "path": "../../sandbox/sandbox" }, + { "path": "../../sandbox/sandbox-policy" } + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08cdf72672..4388263d21 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -298,6 +298,9 @@ importers: '@deepseek-ai/dsh-tool-skill': specifier: workspace:^ version: link:../../packages/skill/tool-skill + '@deepseek-ai/dsh-tool-str-replace-editor': + specifier: workspace:^ + version: link:../../packages/fs/tool-str-replace-editor '@deepseek-ai/dsh-tool-subagent': specifier: workspace:^ version: link:../../packages/subagent/tool-subagent @@ -1239,6 +1242,9 @@ importers: '@deepseek-ai/dsh-tool-skill': specifier: workspace:^ version: link:../../skill/tool-skill + '@deepseek-ai/dsh-tool-str-replace-editor': + specifier: workspace:^ + version: link:../../fs/tool-str-replace-editor '@deepseek-ai/dsh-tool-subagent': specifier: workspace:^ version: link:../../subagent/tool-subagent @@ -5318,6 +5324,52 @@ importers: specifier: workspace:^ version: link:../../core/tools + packages/fs/tool-str-replace-editor: + dependencies: + '@deepseek-ai/schemastery': + specifier: link:../../../vendor/schemastery + version: link:../../../vendor/schemastery + devDependencies: + '@deepseek-ai/cordis': + specifier: workspace:^ + version: link:../../../vendor/cordis + '@deepseek-ai/dsh-agent': + specifier: workspace:^ + version: link:../../core/agent + '@deepseek-ai/dsh-fs': + specifier: workspace:^ + version: link:../fs + '@deepseek-ai/dsh-fs-local': + specifier: workspace:^ + version: link:../fs-local + '@deepseek-ai/dsh-fs-observation-policy': + specifier: workspace:^ + version: link:../fs-observation-policy + '@deepseek-ai/dsh-fs-sandbox': + specifier: workspace:^ + version: link:../fs-sandbox + '@deepseek-ai/dsh-llm': + specifier: workspace:^ + version: link:../../llm/llm + '@deepseek-ai/dsh-sandbox': + specifier: workspace:^ + version: link:../../sandbox/sandbox + '@deepseek-ai/dsh-sandbox-policy': + specifier: workspace:^ + version: link:../../sandbox/sandbox-policy + '@deepseek-ai/dsh-session': + specifier: workspace:^ + version: link:../../core/session + '@deepseek-ai/dsh-session-projection': + specifier: workspace:^ + version: link:../../session/session-projection + '@deepseek-ai/dsh-system-prompt': + specifier: workspace:^ + version: link:../../core/system-prompt + '@deepseek-ai/dsh-tools': + specifier: workspace:^ + version: link:../../core/tools + packages/goal/command-goal: devDependencies: '@deepseek-ai/cordis': @@ -9985,6 +10037,9 @@ importers: '@deepseek-ai/dsh-tool-skill': specifier: workspace:^ version: link:../../packages/skill/tool-skill + '@deepseek-ai/dsh-tool-str-replace-editor': + specifier: workspace:^ + version: link:../../packages/fs/tool-str-replace-editor '@deepseek-ai/dsh-tool-subagent': specifier: workspace:^ version: link:../../packages/subagent/tool-subagent diff --git a/python/sdk-runtime/package.json b/python/sdk-runtime/package.json index 762619c5d1..97c1a6bc68 100644 --- a/python/sdk-runtime/package.json +++ b/python/sdk-runtime/package.json @@ -110,6 +110,7 @@ "@deepseek-ai/dsh-tool-pwsh-persistent": "workspace:^", "@deepseek-ai/dsh-tool-ralph": "workspace:^", "@deepseek-ai/dsh-tool-skill": "workspace:^", + "@deepseek-ai/dsh-tool-str-replace-editor": "workspace:^", "@deepseek-ai/dsh-tool-subagent": "workspace:^", "@deepseek-ai/dsh-tool-subagent-control": "workspace:^", "@deepseek-ai/dsh-tool-todo": "workspace:^", diff --git a/scripts/gen-tool-catalog.ts b/scripts/gen-tool-catalog.ts index 4535e3fef6..972eadc1b8 100644 --- a/scripts/gen-tool-catalog.ts +++ b/scripts/gen-tool-catalog.ts @@ -48,6 +48,7 @@ import CordisHostRunner from '@deepseek-ai/dsh-cordis-host-runner' import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis' import * as ToolFs from '@deepseek-ai/dsh-tool-fs' import * as ToolFsSearch from '@deepseek-ai/dsh-tool-fs-search' +import * as ToolStrReplaceEditor from '@deepseek-ai/dsh-tool-str-replace-editor' import TerminalSessionService from '@deepseek-ai/dsh-terminal' import * as ToolPty from '@deepseek-ai/dsh-tool-terminal' import * as ToolGoal from '@deepseek-ai/dsh-tool-goal' @@ -297,6 +298,19 @@ const TOOL_PACKAGES: ToolPackage[] = [ note: 'One owner-isolated persistent pwsh tool, the Windows counterpart of the persistent bash tool; deployment composition supplies a pwsh-dialect PTY backend and may override the model-facing environment description.', }, + { + pkg: '@deepseek-ai/dsh-tool-str-replace-editor', + dir: 'tool-str-replace-editor', + source: 'packages/fs/tool-str-replace-editor/src/index.ts', + requires: ['ctx.tools', 'ctx.fs'], + writes: ['tool/call', 'fs/observed after view presence/absence, edit absence, or successful mutation', 'tool/result'], + async mount(ctx) { + await ctx.plugin(LocalFileSystem) + await ctx.plugin(ToolStrReplaceEditor) + }, + note: + 'Standalone view/create/unique literal replace/line insert tool over the filesystem seam; it composes with any shell or terminal API.', + }, { pkg: '@deepseek-ai/dsh-tool-fs', dir: 'tool-fs', diff --git a/scripts/smoke-python-runtime.py b/scripts/smoke-python-runtime.py index 66e9221cae..b1969ab5b9 100644 --- a/scripts/smoke-python-runtime.py +++ b/scripts/smoke-python-runtime.py @@ -79,6 +79,7 @@ LEGACY_CUSTOM_DISABLED_ROWS = ( "tool-goal", "tool-ralph", "tool-skill", + "tool-str-replace-editor", "tool-subagent-control", "tool-subagent-list-agents", "tool-subagent-fork", diff --git a/snapshots/sdk/bash-tool/tool-schemas.expected.json b/snapshots/sdk/bash-tool/tool-schemas.expected.json index dd7a2da87e..716b08cd1d 100644 --- a/snapshots/sdk/bash-tool/tool-schemas.expected.json +++ b/snapshots/sdk/bash-tool/tool-schemas.expected.json @@ -357,6 +357,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This call waits for the subagent and returns its result.", diff --git a/snapshots/sdk/persistent-tools/cordis.snapshot.yml b/snapshots/sdk/persistent-tools/cordis.snapshot.yml new file mode 100644 index 0000000000..0579e3881d --- /dev/null +++ b/snapshots/sdk/persistent-tools/cordis.snapshot.yml @@ -0,0 +1,15 @@ +# Replay layer for the TypeScript SDK minimal customization patch. + +- id: llm-deepseek + name: '@deepseek-ai/dsh-llm-deepseek' + disabled: true + +- insert: + - id: llm-replay + name: '@deepseek-ai/dsh-llm-replay' + config: + providers: + - id: deepseek-official + name: DeepSeek + models: + - id: deepseek-v4-flash diff --git a/snapshots/sdk/persistent-tools/cordis.yml b/snapshots/sdk/persistent-tools/cordis.yml new file mode 100644 index 0000000000..c78756cb69 --- /dev/null +++ b/snapshots/sdk/persistent-tools/cordis.yml @@ -0,0 +1,158 @@ +# Per-launch SDK customization: retain only persistent Bash and the string +# editor as model-facing tools, with process-local providers and an explicit +# danger-full-access / never-ask policy. + +- id: llm-deepseek + name: '@deepseek-ai/dsh-llm-deepseek' + config: + apiKeyEnv: DEEPSEEK_API_KEY + streamIdleTimeoutMs: 172800000 + models: + - id: !!js process.env.DSH_MODEL ?? 'deepseek-v4-flash' + contextWindow: !!js Number(process.env.DSH_CONTEXT_WINDOW ?? 1000000) + +- id: sandbox-policy + name: '@deepseek-ai/dsh-sandbox-policy' + config: + mode: danger-full-access + workspaceRoot: !!js process.cwd() + +- id: approval + name: '@deepseek-ai/dsh-user-approval' + config: + policy: never + +- id: permission + name: '@deepseek-ai/dsh-permission-presets' + disabled: true + +- id: bash-sandbox + name: '@deepseek-ai/dsh-bash-sandbox' + disabled: true + +- id: pwsh-sandbox + name: '@deepseek-ai/dsh-pwsh-sandbox' + disabled: true + +- id: fs-sandbox + name: '@deepseek-ai/dsh-fs-sandbox' + disabled: true + +- id: tool-bash + name: '@deepseek-ai/dsh-tool-bash' + disabled: true + +- id: tool-pwsh + name: '@deepseek-ai/dsh-tool-pwsh' + disabled: true + +- id: agent-instructions + name: '@deepseek-ai/dsh-agent-instructions' + disabled: true + +- id: skill + name: '@deepseek-ai/dsh-skill' + disabled: true + +- id: skill-filesystem + name: '@deepseek-ai/dsh-skill-filesystem' + disabled: true + +- id: tool-skill + name: '@deepseek-ai/dsh-tool-skill' + disabled: true + +- id: tool-jobs + name: '@deepseek-ai/dsh-tool-jobs' + disabled: true + +- id: tool-fs + name: '@deepseek-ai/dsh-tool-fs' + disabled: true + +- id: tool-fs-search + name: '@deepseek-ai/dsh-tool-fs-search' + disabled: true + +- id: tool-subagent-control + name: '@deepseek-ai/dsh-tool-subagent-control' + disabled: true + +- id: tool-subagent-list-agents + name: '@deepseek-ai/dsh-tool-subagent-control/list-agents' + disabled: true + +- id: tool-subagent + name: '@deepseek-ai/dsh-tool-subagent' + disabled: true + +- id: tool-subagent-fork + name: '@deepseek-ai/dsh-tool-subagent' + disabled: true + +- id: tool-workflow + name: '@deepseek-ai/dsh-tool-workflow' + disabled: true + +- id: tool-todo + name: '@deepseek-ai/dsh-tool-todo' + disabled: true + +- id: tool-goal + name: '@deepseek-ai/dsh-tool-goal' + disabled: true + +- id: tool-ralph + name: '@deepseek-ai/dsh-tool-ralph' + disabled: true + +- id: tool-web + name: '@deepseek-ai/dsh-tool-web' + disabled: true + +- id: plan-mode + name: '@deepseek-ai/dsh-plan-mode' + disabled: true + +- id: system-prompt + name: '@deepseek-ai/dsh-system-prompt' + config: + includeHarnessIdentity: false + persona: !!js process.env.DSH_SYSTEM_PROMPT ?? 'You are a helpful software engineer assistant.' + +- id: session-persistence-jsonl + name: '@deepseek-ai/dsh-session-persistence-jsonl' + config: + root: !!js dshHomePath('sessions') + compression: none + +- insert: + - id: bash-local + name: '@deepseek-ai/dsh-bash-local' + + - id: fs-local + name: '@deepseek-ai/dsh-fs-local' + config: + cwd: !!js process.cwd() + + - id: terminal + name: '@deepseek-ai/dsh-terminal' + + - id: terminal-bash + name: '@deepseek-ai/dsh-terminal-bash' + config: + timeoutMs: 300000 + + - id: persistent-bash + name: '@deepseek-ai/dsh-tool-bash-persistent' + config: + timeoutMs: 300000 + description: |- + Run commands in a bash shell + * When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped. + * You don't have access to the internet via this tool. + * You do have access to a mirror of common linux and python packages via apt and pip. + * State is persistent across command calls and discussions with the user. + * To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'. + * Please avoid commands that may produce a very large amount of output. + * Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background. diff --git a/snapshots/sdk/persistent-tools/notifications.expected.jsonl b/snapshots/sdk/persistent-tools/notifications.expected.jsonl new file mode 100644 index 0000000000..fe4872b440 --- /dev/null +++ b/snapshots/sdk/persistent-tools/notifications.expected.jsonl @@ -0,0 +1,89 @@ +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":0,"time":0,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Prove that bash state persists. After the persistence checks, run exactly `{ sleep 0.1; echo delayed; } | cat` as its own bash call and observe `delayed`. Then create {{cwd}}/note.txt with a tab-indented line, view it, replace that literal tab-indented line, and make the persistent shell exit with code 9."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}}}} +{"method":"session.status","params":{"sessionId":"{{sessionId}}","status":"running"}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/start","seq":1,"time":0,"data":{"turn":1}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":2,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":4,"time":0,"data":{"content":[{"type":"text","text":"Prove that bash state persists. After the persistence checks, run exactly `{ sleep 0.1; echo delayed; } | cat` as its own bash call and observe `delayed`. Then create {{cwd}}/note.txt with a tab-indented line, view it, replace that literal tab-indented line, and make the persistent shell exit with code 9."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":5,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"session/title","seq":6,"time":0,"data":{"title":"Prove that bash state persists.","messageSeqs":[4],"source":{"kind":"fallback"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":7,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/context","seq":8,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":9,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":10,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-1","name":"bash","argumentsDelta":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":12,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":13,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":14,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":15,"time":0,"data":{"turn":1,"step":1,"callId":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":16,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"bash-1"},"content":[{"type":"tool-result","toolCallId":"bash-1","content":[{"type":"text","text":"COUNT=1 CWD=/tmp"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":17,"time":0,"data":{"turn":1,"step":1}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":18,"time":0,"data":{"turn":1,"step":2}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":19,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":20,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-2","name":"bash","argumentsDelta":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":21,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":22,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":23,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":24,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":25,"time":0,"data":{"turn":1,"step":2,"callId":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":26,"time":0,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"bash-2"},"content":[{"type":"tool-result","toolCallId":"bash-2","content":[{"type":"text","text":"COUNT=2 CWD=/tmp"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[25],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":27,"time":0,"data":{"turn":1,"step":2}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":28,"time":0,"data":{"turn":1,"step":3}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":29,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":30,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-delayed-pipeline","name":"bash","argumentsDelta":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":31,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":32,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":33,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":34,"time":0,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[29,30,31,32,33],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":35,"time":0,"data":{"turn":1,"step":3,"callId":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":36,"time":0,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"bash-delayed-pipeline"},"content":[{"type":"tool-result","toolCallId":"bash-delayed-pipeline","content":[{"type":"text","text":"delayed"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[35],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":37,"time":0,"data":{"turn":1,"step":3}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":38,"time":0,"data":{"turn":1,"step":4}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":39,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":40,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-create","name":"str_replace_editor","argumentsDelta":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":41,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":42,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":43,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":44,"time":0,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[39,40,41,42,43],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":45,"time":0,"data":{"turn":1,"step":4,"callId":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":46,"time":0,"data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"editor-create"},"content":[{"type":"tool-result","toolCallId":"editor-create","content":[{"type":"text","text":"New file created successfully at: {{cwd}}/note.txt"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[45],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":47,"time":0,"data":{"turn":1,"step":4}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":48,"time":0,"data":{"turn":1,"step":5}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":49,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":50,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-view","name":"str_replace_editor","argumentsDelta":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":null,\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":51,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":null,\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":52,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":53,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":54,"time":0,"data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":null,\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[49,50,51,52,53],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":55,"time":0,"data":{"turn":1,"step":5,"callId":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":null,\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":56,"time":0,"data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"editor-view"},"content":[{"type":"tool-result","toolCallId":"editor-view","content":[{"type":"text","text":"Here's the content of {{cwd}}/note.txt with line numbers (which has a total of 3 lines):\n 1 target:\n 2 \told\n 3 \n"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[55],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":57,"time":0,"data":{"turn":1,"step":5}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":58,"time":0,"data":{"turn":1,"step":6}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":59,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":60,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-replace","name":"str_replace_editor","argumentsDelta":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\",\"file_text\":null,\"insert_line\":null,\"view_range\":null}"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":61,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\",\"file_text\":null,\"insert_line\":null,\"view_range\":null}"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":62,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":63,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":64,"time":0,"data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\",\"file_text\":null,\"insert_line\":null,\"view_range\":null}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[59,60,61,62,63],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":65,"time":0,"data":{"turn":1,"step":6,"callId":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\",\"file_text\":null,\"insert_line\":null,\"view_range\":null}"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":66,"time":0,"data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"editor-replace"},"content":[{"type":"tool-result","toolCallId":"editor-replace","content":[{"type":"text","text":"The file {{cwd}}/note.txt has been edited successfully."}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[65],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":67,"time":0,"data":{"turn":1,"step":6}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":68,"time":0,"data":{"turn":1,"step":7}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":69,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":70,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-exit","name":"bash","argumentsDelta":"{\"command\":\"exit 9\"}"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":71,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":72,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":73,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":74,"time":0,"data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[69,70,71,72,73],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":75,"time":0,"data":{"turn":1,"step":7,"callId":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":76,"time":0,"data":{"turn":1,"step":7,"message":{"source":{"kind":"tool","callId":"bash-exit"},"content":[{"type":"tool-result","toolCallId":"bash-exit","content":[{"type":"text","text":"exit\n[shell exited: code 9]\nThe persistent bash shell was reset; the next bash call starts from the workspace with a fresh current directory and environment."}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[75],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":77,"time":0,"data":{"turn":1,"step":7}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":78,"time":0,"data":{"turn":1,"step":8}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":79,"time":0,"data":{"turn":1,"step":8,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":80,"time":0,"data":{"turn":1,"step":8,"chunk":{"type":"text-delta","index":0,"text":"PERSISTENT_TOOLS_OK"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":81,"time":0,"data":{"turn":1,"step":8,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PERSISTENT_TOOLS_OK"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":82,"time":0,"data":{"turn":1,"step":8,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":83,"time":0,"data":{"turn":1,"step":8,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":84,"time":0,"data":{"turn":1,"step":8,"message":{"role":"assistant","content":[{"type":"text","text":"PERSISTENT_TOOLS_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[79,80,81,82,83],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":85,"time":0,"data":{"turn":1,"step":8}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":86,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}}} +{"method":"session.status","params":{"sessionId":"{{sessionId}}","status":"idle"}} diff --git a/snapshots/sdk/persistent-tools/result.expected.json b/snapshots/sdk/persistent-tools/result.expected.json new file mode 100644 index 0000000000..53a1342f7d --- /dev/null +++ b/snapshots/sdk/persistent-tools/result.expected.json @@ -0,0 +1 @@ +{"sessionId":"{{sessionId}}","finalResponse":"PERSISTENT_TOOLS_OK"} diff --git a/snapshots/sdk/persistent-tools/session.jsonl b/snapshots/sdk/persistent-tools/session.jsonl new file mode 100644 index 0000000000..b382309331 --- /dev/null +++ b/snapshots/sdk/persistent-tools/session.jsonl @@ -0,0 +1,88 @@ +{"type":"session","version":0,"id":"{{session:1}}","createdAt":1785331618309,"cwd":"{{cwd}}","delegationDepth":0} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Prove that bash state persists. After the persistence checks, run exactly `{ sleep 0.1; echo delayed; } | cat` as its own bash call and observe `delayed`. Then create {{cwd}}/note.txt with a tab-indented line, view it, replace that literal tab-indented line, and make the persistent shell exit with code 9."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}} +{"type":"turn/start","data":{"turn":1}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} +{"type":"step/start","data":{"turn":1,"step":1}} +{"type":"user/message","data":{"content":[{"type":"text","text":"Prove that bash state persists. After the persistence checks, run exactly `{ sleep 0.1; echo delayed; } | cat` as its own bash call and observe `delayed`. Then create {{cwd}}/note.txt with a tab-indented line, view it, replace that literal tab-indented line, and make the persistent shell exit with code 9."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:2}}"},"surfaceOp":"append"} +{"type":"session/title","data":{"title":"Prove that bash state persists.","messageSeqs":[4],"source":{"kind":"fallback"}}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} +{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-1","name":"bash","argumentsDelta":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}} +{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}} +{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"} +{"type":"tool/call","data":{"turn":1,"step":1,"callId":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"bash-1"},"content":[{"type":"tool-result","toolCallId":"bash-1","content":[{"type":"text","text":"COUNT=1 CWD=/tmp"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":1}} +{"type":"step/start","data":{"turn":1,"step":2}} +{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-2","name":"bash","argumentsDelta":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}} +{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}} +{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"} +{"type":"tool/call","data":{"turn":1,"step":2,"callId":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}} +{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"bash-2"},"content":[{"type":"tool-result","toolCallId":"bash-2","content":[{"type":"text","text":"COUNT=2 CWD=/tmp"}],"isError":false}],"role":"user","id":"{{message:6}}"}},"sourceEventSeqs":[25],"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":2}} +{"type":"step/start","data":{"turn":1,"step":3}} +{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-delayed-pipeline","name":"bash","argumentsDelta":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}}} +{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}}}} +{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[29,30,31,32,33],"surfaceOp":"append"} +{"type":"tool/call","data":{"turn":1,"step":3,"callId":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}} +{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"bash-delayed-pipeline"},"content":[{"type":"tool-result","toolCallId":"bash-delayed-pipeline","content":[{"type":"text","text":"delayed"}],"isError":false}],"role":"user","id":"{{message:8}}"}},"sourceEventSeqs":[35],"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":3}} +{"type":"step/start","data":{"turn":1,"step":4}} +{"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-create","name":"str_replace_editor","argumentsDelta":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}} +{"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}}} +{"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[39,40,41,42,43],"surfaceOp":"append"} +{"type":"tool/call","data":{"turn":1,"step":4,"callId":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}} +{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"editor-create"},"content":[{"type":"tool-result","toolCallId":"editor-create","content":[{"type":"text","text":"New file created successfully at: {{cwd}}/note.txt"}],"isError":false}],"role":"user","id":"{{message:10}}"}},"sourceEventSeqs":[45],"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":4}} +{"type":"step/start","data":{"turn":1,"step":5}} +{"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-view","name":"str_replace_editor","argumentsDelta":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":null,\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}} +{"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":null,\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}}} +{"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":null,\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[49,50,51,52,53],"surfaceOp":"append"} +{"type":"tool/call","data":{"turn":1,"step":5,"callId":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":null,\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}} +{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"editor-view"},"content":[{"type":"tool-result","toolCallId":"editor-view","content":[{"type":"text","text":"Here's the content of {{cwd}}/note.txt with line numbers (which has a total of 3 lines):\n 1 target:\n 2 \told\n 3 \n"}],"isError":false}],"role":"user","id":"{{message:12}}"}},"sourceEventSeqs":[55],"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":5}} +{"type":"step/start","data":{"turn":1,"step":6}} +{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-replace","name":"str_replace_editor","argumentsDelta":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\",\"file_text\":null,\"insert_line\":null,\"view_range\":null}"}}} +{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\",\"file_text\":null,\"insert_line\":null,\"view_range\":null}"}}}} +{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\",\"file_text\":null,\"insert_line\":null,\"view_range\":null}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:13}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[59,60,61,62,63],"surfaceOp":"append"} +{"type":"tool/call","data":{"turn":1,"step":6,"callId":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\",\"file_text\":null,\"insert_line\":null,\"view_range\":null}"}} +{"type":"tool/result","data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"editor-replace"},"content":[{"type":"tool-result","toolCallId":"editor-replace","content":[{"type":"text","text":"The file {{cwd}}/note.txt has been edited successfully."}],"isError":false}],"role":"user","id":"{{message:14}}"}},"sourceEventSeqs":[65],"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":6}} +{"type":"step/start","data":{"turn":1,"step":7}} +{"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-exit","name":"bash","argumentsDelta":"{\"command\":\"exit 9\"}"}}} +{"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}}}} +{"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:15}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[69,70,71,72,73],"surfaceOp":"append"} +{"type":"tool/call","data":{"turn":1,"step":7,"callId":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}} +{"type":"tool/result","data":{"turn":1,"step":7,"message":{"source":{"kind":"tool","callId":"bash-exit"},"content":[{"type":"tool-result","toolCallId":"bash-exit","content":[{"type":"text","text":"exit\n[shell exited: code 9]\nThe persistent bash shell was reset; the next bash call starts from the workspace with a fresh current directory and environment."}],"isError":false}],"role":"user","id":"{{message:16}}"}},"sourceEventSeqs":[75],"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":7}} +{"type":"step/start","data":{"turn":1,"step":8}} +{"type":"assistant/chunk","data":{"turn":1,"step":8,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} +{"type":"assistant/chunk","data":{"turn":1,"step":8,"chunk":{"type":"text-delta","index":0,"text":"PERSISTENT_TOOLS_OK"}}} +{"type":"assistant/chunk","data":{"turn":1,"step":8,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PERSISTENT_TOOLS_OK"}}}} +{"type":"assistant/chunk","data":{"turn":1,"step":8,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","data":{"turn":1,"step":8,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","data":{"turn":1,"step":8,"message":{"role":"assistant","content":[{"type":"text","text":"PERSISTENT_TOOLS_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:17}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[79,80,81,82,83],"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":8}} +{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/sdk/persistent-tools/snapshot.yml b/snapshots/sdk/persistent-tools/snapshot.yml new file mode 100644 index 0000000000..b4876f0b17 --- /dev/null +++ b/snapshots/sdk/persistent-tools/snapshot.yml @@ -0,0 +1,10 @@ +version: 1 +scenario: persistent-tools +profile: sdk +composition: sdk-minimal +recording: live +header: + class: sdk-minimal + pin: true +workspace: + final: true diff --git a/snapshots/sdk/persistent-tools/system-prompt.expected.md b/snapshots/sdk/persistent-tools/system-prompt.expected.md new file mode 100644 index 0000000000..3066e65687 --- /dev/null +++ b/snapshots/sdk/persistent-tools/system-prompt.expected.md @@ -0,0 +1 @@ +You are the environment-selected minimal software engineer. diff --git a/snapshots/sdk/persistent-tools/tool-schemas.expected.json b/snapshots/sdk/persistent-tools/tool-schemas.expected.json new file mode 100644 index 0000000000..73234c5e49 --- /dev/null +++ b/snapshots/sdk/persistent-tools/tool-schemas.expected.json @@ -0,0 +1,106 @@ +{ + "initial": [ + { + "name": "bash", + "description": "Run commands in a bash shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* You don't have access to the internet via this tool.\n* You do have access to a mirror of common linux and python packages via apt and pip.\n* State is persistent across command calls and discussions with the user.\n* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The bash command to run. Relative path is preferred in the command." + } + }, + "required": [ + "command" + ] + } + }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + } + ], + "changes": [] +} diff --git a/snapshots/sdk/persistent-tools/workspace.expected/note.txt b/snapshots/sdk/persistent-tools/workspace.expected/note.txt new file mode 100644 index 0000000000..86b943efe7 --- /dev/null +++ b/snapshots/sdk/persistent-tools/workspace.expected/note.txt @@ -0,0 +1,2 @@ +target: + new diff --git a/snapshots/sdk/sdk.snapshot.ts b/snapshots/sdk/sdk.snapshot.ts index 3095c36575..50ececf76a 100644 --- a/snapshots/sdk/sdk.snapshot.ts +++ b/snapshots/sdk/sdk.snapshot.ts @@ -54,6 +54,16 @@ import { const corpusRoot = fileURLToPath(new URL('../', import.meta.url)) +const MINIMAL_SYSTEM_PROMPT = 'You are the environment-selected minimal software engineer.' +const MINIMAL_BASH_DESCRIPTION = `Run commands in a bash shell +* When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped. +* You don't have access to the internet via this tool. +* You do have access to a mirror of common linux and python packages via apt and pip. +* State is persistent across command calls and discussions with the user. +* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'. +* Please avoid commands that may produce a very large amount of output. +* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.` + const mode = process.env.DSH_SNAPSHOT ?? 'replay' const recording = mode === 'record' const refreshing = mode === 'refresh' @@ -102,6 +112,16 @@ const SDK_ASSERTIONS: Readonly> = { 'subagent-dsh-sdk-diagnostic': { environment: { DSH_TEST_CHILD_PATCH: dshSdkDiagnosticChildPatch }, }, + 'persistent-tools': { + environment: { DSH_SYSTEM_PROMPT: MINIMAL_SYSTEM_PROMPT }, + expectedTools: { bash: ['command'], str_replace_editor: ['command', 'path'] }, + expectedSystem: MINIMAL_SYSTEM_PROMPT, + expectedToolDescriptions: { bash: MINIMAL_BASH_DESCRIPTION }, + runtimeContext: { + includes: ['Current DSH file policy: danger-full-access', 'Approval prompts are disabled in this session'], + excludes: ['workspace-write'], + }, + }, 'subagent-dsh-sdk-dynamic-route': { environment: { DSH_TEST_PARENT_PROVIDER: 'deepseek-official' }, dshSdkChild: { diff --git a/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json b/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json index 1531215c7f..87cbb941a1 100644 --- a/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json @@ -357,6 +357,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json b/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json index 1531215c7f..87cbb941a1 100644 --- a/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json @@ -357,6 +357,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json b/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json index ba63548517..ee4ad0e6b6 100644 --- a/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json +++ b/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json @@ -357,6 +357,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json index e7899283b4..c21ad8a0f5 100644 --- a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json @@ -357,6 +357,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json index 73c4ae509a..9cc82c6fb5 100644 --- a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json +++ b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json @@ -374,6 +374,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This call waits for the subagent and returns its result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and this provider's route defaults. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", diff --git a/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json b/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json index 1531215c7f..87cbb941a1 100644 --- a/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json @@ -357,6 +357,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json b/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json index 1531215c7f..87cbb941a1 100644 --- a/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json @@ -357,6 +357,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/sdk/text-turn/tool-schemas.expected.json b/snapshots/sdk/text-turn/tool-schemas.expected.json index dd7a2da87e..716b08cd1d 100644 --- a/snapshots/sdk/text-turn/tool-schemas.expected.json +++ b/snapshots/sdk/text-turn/tool-schemas.expected.json @@ -357,6 +357,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This call waits for the subagent and returns its result.", diff --git a/snapshots/session/agent-instructions/tool-schemas.expected.json b/snapshots/session/agent-instructions/tool-schemas.expected.json index 40661b4472..a3e1ee5238 100644 --- a/snapshots/session/agent-instructions/tool-schemas.expected.json +++ b/snapshots/session/agent-instructions/tool-schemas.expected.json @@ -357,6 +357,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", @@ -989,6 +1074,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/both-mode-turn/system-prompt.expected.md b/snapshots/session/both-mode-turn/system-prompt.expected.md index 5c3c43a2e7..22f4a66ba2 100644 --- a/snapshots/session/both-mode-turn/system-prompt.expected.md +++ b/snapshots/session/both-mode-turn/system-prompt.expected.md @@ -173,6 +173,23 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; + /** Custom editing tool for viewing, creating and editing files * State is persistent across command calls and discussions with the user * If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep * The `create` command cannot be used if the specified `path` already exists as a file * If a `command` generates a long output, it will be truncated and marked with `` * A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match Notes for using the `str_replace` command: * The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces! * If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique * The `new_str` parameter should contain the edited lines that should replace the `old_str` */ + str_replace_editor: { + /** The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`. */ + command: "view" | "create" | "str_replace" | "insert"; + /** Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`. */ + path: string; + /** Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter. */ + file_text?: string | null; + /** Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter. */ + insert_line?: number | null; + /** Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter. */ + new_str?: string | null; + /** Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter. */ + old_str?: string | null; + /** Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file. */ + view_range?: number[] | null; + } & Record; /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result. */ subagent: { /** A short (3-5 word) description of the delegated task, for display. */ @@ -442,6 +459,7 @@ interface ToolOutputMap { }; content: string; }; + str_replace_editor: string; subagent: { kind: "background"; jobId: string; diff --git a/snapshots/session/both-mode-turn/tool-schemas.expected.json b/snapshots/session/both-mode-turn/tool-schemas.expected.json index fcaa91628e..b1a1179d18 100644 --- a/snapshots/session/both-mode-turn/tool-schemas.expected.json +++ b/snapshots/session/both-mode-turn/tool-schemas.expected.json @@ -378,6 +378,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/compaction-recovery/tool-schemas.expected.json b/snapshots/session/compaction-recovery/tool-schemas.expected.json index 40661b4472..a3e1ee5238 100644 --- a/snapshots/session/compaction-recovery/tool-schemas.expected.json +++ b/snapshots/session/compaction-recovery/tool-schemas.expected.json @@ -357,6 +357,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", @@ -989,6 +1074,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/cordis-inspect-jsdoc/system-prompt.expected.md b/snapshots/session/cordis-inspect-jsdoc/system-prompt.expected.md index 6bd67f58ab..8f0f468911 100644 --- a/snapshots/session/cordis-inspect-jsdoc/system-prompt.expected.md +++ b/snapshots/session/cordis-inspect-jsdoc/system-prompt.expected.md @@ -340,6 +340,23 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; + /** Custom editing tool for viewing, creating and editing files * State is persistent across command calls and discussions with the user * If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep * The `create` command cannot be used if the specified `path` already exists as a file * If a `command` generates a long output, it will be truncated and marked with `` * A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match Notes for using the `str_replace` command: * The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces! * If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique * The `new_str` parameter should contain the edited lines that should replace the `old_str` */ + str_replace_editor: { + /** The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`. */ + command: "view" | "create" | "str_replace" | "insert"; + /** Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`. */ + path: string; + /** Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter. */ + file_text?: string | null; + /** Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter. */ + insert_line?: number | null; + /** Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter. */ + new_str?: string | null; + /** Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter. */ + old_str?: string | null; + /** Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file. */ + view_range?: number[] | null; + } & Record; /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result. */ subagent: { /** A short (3-5 word) description of the delegated task, for display. */ @@ -628,6 +645,7 @@ interface ToolOutputMap { }; content: string; }; + str_replace_editor: string; subagent: { kind: "background"; jobId: string; diff --git a/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json b/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json index fe1f6ed30a..fbb01942ab 100644 --- a/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json +++ b/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json @@ -575,6 +575,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/fs-glob-sampling/tool-schemas.expected.json b/snapshots/session/fs-glob-sampling/tool-schemas.expected.json index 5d3cd77ac0..0dd86f7f77 100644 --- a/snapshots/session/fs-glob-sampling/tool-schemas.expected.json +++ b/snapshots/session/fs-glob-sampling/tool-schemas.expected.json @@ -261,6 +261,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/lsp-definition/tool-schemas.expected.json b/snapshots/session/lsp-definition/tool-schemas.expected.json index 393fe158eb..def66aefbd 100644 --- a/snapshots/session/lsp-definition/tool-schemas.expected.json +++ b/snapshots/session/lsp-definition/tool-schemas.expected.json @@ -394,6 +394,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/product-subagent-both/tool-schemas.expected.json b/snapshots/session/product-subagent-both/tool-schemas.expected.json index a20d2a132f..232c2778f2 100644 --- a/snapshots/session/product-subagent-both/tool-schemas.expected.json +++ b/snapshots/session/product-subagent-both/tool-schemas.expected.json @@ -357,6 +357,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/product-subagent-codex/tool-schemas.expected.json b/snapshots/session/product-subagent-codex/tool-schemas.expected.json index f28699d048..e8877690ec 100644 --- a/snapshots/session/product-subagent-codex/tool-schemas.expected.json +++ b/snapshots/session/product-subagent-codex/tool-schemas.expected.json @@ -357,6 +357,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json b/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json index 9ba36a6336..8303bc5845 100644 --- a/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json +++ b/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json @@ -357,6 +357,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/ptc-python-turn/system-prompt.expected.md b/snapshots/session/ptc-python-turn/system-prompt.expected.md index f9eaff62cb..375b87b4e0 100644 --- a/snapshots/session/ptc-python-turn/system-prompt.expected.md +++ b/snapshots/session/ptc-python-turn/system-prompt.expected.md @@ -366,6 +366,23 @@ class SkillOutput(TypedDict): resourceBase: NotRequired[SkillOutputResourceBase1 | SkillOutputResourceBase2 | SkillOutputResourceBase3] content: str +class StrReplaceEditorArgs(TypedDict): + # The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`. + command: Literal["view", "create", "str_replace", "insert"] + # Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`. + path: str + # Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter. + file_text: NotRequired[str | None] + # Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter. + insert_line: NotRequired[int | None] + # Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter. + new_str: NotRequired[str | None] + # Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter. + old_str: NotRequired[str | None] + # Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file. + view_range: NotRequired[list[int] | None] + # Additional keys beyond those declared are allowed. + class SubagentArgs(TypedDict): # A short (3-5 word) description of the delegated task, for display. description: str @@ -590,6 +607,8 @@ class Tools(Protocol): """Send a message to a direct continuable child by its agent id. If you are a resident continuable child, you may also target your direct parent. If the target is still working, the message steers its nearest step; if it is idle, the message starts a turn. This call returns no answer from the agent — only confirmation that the message was delivered. A failure means the message was NOT delivered.""" async def skill(self, args: SkillArgs) -> SkillOutput: """Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.""" + async def str_replace_editor(self, args: StrReplaceEditorArgs) -> str: + """Custom editing tool for viewing, creating and editing files * State is persistent across command calls and discussions with the user * If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep * The `create` command cannot be used if the specified `path` already exists as a file * If a `command` generates a long output, it will be truncated and marked with `` * A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match Notes for using the `str_replace` command: * The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces! * If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique * The `new_str` parameter should contain the edited lines that should replace the `old_str`""" async def subagent(self, args: SubagentArgs) -> SubagentOutput1 | SubagentOutput2 | SubagentOutput3: """Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.""" async def subagent_fork(self, args: SubagentForkArgs) -> SubagentForkOutput1 | SubagentForkOutput2 | SubagentForkOutput3: diff --git a/snapshots/session/ptc-read-image/system-prompt.expected.md b/snapshots/session/ptc-read-image/system-prompt.expected.md index 672242d92e..004a681296 100644 --- a/snapshots/session/ptc-read-image/system-prompt.expected.md +++ b/snapshots/session/ptc-read-image/system-prompt.expected.md @@ -175,6 +175,23 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; + /** Custom editing tool for viewing, creating and editing files * State is persistent across command calls and discussions with the user * If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep * The `create` command cannot be used if the specified `path` already exists as a file * If a `command` generates a long output, it will be truncated and marked with `` * A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match Notes for using the `str_replace` command: * The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces! * If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique * The `new_str` parameter should contain the edited lines that should replace the `old_str` */ + str_replace_editor: { + /** The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`. */ + command: "view" | "create" | "str_replace" | "insert"; + /** Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`. */ + path: string; + /** Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter. */ + file_text?: string | null; + /** Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter. */ + insert_line?: number | null; + /** Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter. */ + new_str?: string | null; + /** Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter. */ + old_str?: string | null; + /** Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file. */ + view_range?: number[] | null; + } & Record; /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result. */ subagent: { /** A short (3-5 word) description of the delegated task, for display. */ @@ -444,6 +461,7 @@ interface ToolOutputMap { }; content: string; }; + str_replace_editor: string; subagent: { kind: "background"; jobId: string; diff --git a/snapshots/session/ptc-turn/system-prompt.expected.md b/snapshots/session/ptc-turn/system-prompt.expected.md index dd648445e1..5c43f1c4db 100644 --- a/snapshots/session/ptc-turn/system-prompt.expected.md +++ b/snapshots/session/ptc-turn/system-prompt.expected.md @@ -175,6 +175,23 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; + /** Custom editing tool for viewing, creating and editing files * State is persistent across command calls and discussions with the user * If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep * The `create` command cannot be used if the specified `path` already exists as a file * If a `command` generates a long output, it will be truncated and marked with `` * A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match Notes for using the `str_replace` command: * The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces! * If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique * The `new_str` parameter should contain the edited lines that should replace the `old_str` */ + str_replace_editor: { + /** The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`. */ + command: "view" | "create" | "str_replace" | "insert"; + /** Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`. */ + path: string; + /** Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter. */ + file_text?: string | null; + /** Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter. */ + insert_line?: number | null; + /** Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter. */ + new_str?: string | null; + /** Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter. */ + old_str?: string | null; + /** Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file. */ + view_range?: number[] | null; + } & Record; /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result. */ subagent: { /** A short (3-5 word) description of the delegated task, for display. */ @@ -444,6 +461,7 @@ interface ToolOutputMap { }; content: string; }; + str_replace_editor: string; subagent: { kind: "background"; jobId: string; diff --git a/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json b/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json index 3c9ef95371..fd18c5805a 100644 --- a/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json +++ b/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json @@ -357,6 +357,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/ralph-loop/tool-schemas.1.expected.json b/snapshots/session/ralph-loop/tool-schemas.1.expected.json index 1feee5af52..08dce4ee40 100644 --- a/snapshots/session/ralph-loop/tool-schemas.1.expected.json +++ b/snapshots/session/ralph-loop/tool-schemas.1.expected.json @@ -357,6 +357,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "structured_output", "description": "Report your final structured result. Call this exactly once, when your answer is complete; the arguments must match this tool's parameter schema exactly.", diff --git a/snapshots/session/ralph-loop/tool-schemas.2.expected.json b/snapshots/session/ralph-loop/tool-schemas.2.expected.json index 1feee5af52..08dce4ee40 100644 --- a/snapshots/session/ralph-loop/tool-schemas.2.expected.json +++ b/snapshots/session/ralph-loop/tool-schemas.2.expected.json @@ -357,6 +357,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "structured_output", "description": "Report your final structured result. Call this exactly once, when your answer is complete; the arguments must match this tool's parameter schema exactly.", diff --git a/snapshots/session/session-query-spill/tool-schemas.expected.json b/snapshots/session/session-query-spill/tool-schemas.expected.json index e59600ed46..5d8b78fc63 100644 --- a/snapshots/session/session-query-spill/tool-schemas.expected.json +++ b/snapshots/session/session-query-spill/tool-schemas.expected.json @@ -561,6 +561,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json b/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json index cb0c104048..cec2b16ef8 100644 --- a/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json +++ b/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json @@ -357,6 +357,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json b/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json index 0cbbda81fe..ed2d4fa46f 100644 --- a/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json +++ b/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json @@ -420,6 +420,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/text-turn/tool-schemas.expected.json b/snapshots/session/text-turn/tool-schemas.expected.json index 1531215c7f..87cbb941a1 100644 --- a/snapshots/session/text-turn/tool-schemas.expected.json +++ b/snapshots/session/text-turn/tool-schemas.expected.json @@ -357,6 +357,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/snapshots/session/web-fetch/tool-schemas.expected.json b/snapshots/session/web-fetch/tool-schemas.expected.json index ed38c64b2b..aa3f6cabd5 100644 --- a/snapshots/session/web-fetch/tool-schemas.expected.json +++ b/snapshots/session/web-fetch/tool-schemas.expected.json @@ -357,6 +357,91 @@ ] } }, + { + "name": "str_replace_editor", + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.", + "enum": [ + "view", + "create", + "str_replace", + "insert" + ] + }, + "path": { + "type": "string", + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." + }, + "file_text": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "insert_line": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "new_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter." + }, + "old_str": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter." + }, + "view_range": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file." + } + }, + "required": [ + "command", + "path" + ] + } + }, { "name": "subagent", "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.", diff --git a/tsconfig.base.json b/tsconfig.base.json index ea2868ce93..68aab92144 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -394,6 +394,7 @@ "@deepseek-ai/dsh-tool-ralph": ["./packages/workflow/tool-ralph/src"], "@deepseek-ai/dsh-tool-session-query": ["./packages/session-query/tool-session-query/src"], "@deepseek-ai/dsh-tool-skill": ["./packages/skill/tool-skill/src"], + "@deepseek-ai/dsh-tool-str-replace-editor": ["./packages/fs/tool-str-replace-editor/src"], "@deepseek-ai/dsh-tool-subagent": ["./packages/subagent/tool-subagent/src"], "@deepseek-ai/dsh-tool-subagent/invariant": ["./packages/subagent/tool-subagent/src/invariant.ts"], "@deepseek-ai/dsh-tool-subagent-control": ["./packages/subagent/tool-subagent-control/src"], diff --git a/tsconfig.host.json b/tsconfig.host.json index 7f79fe8c98..d2a033266d 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -248,6 +248,7 @@ { "path": "./packages/fs/fs-sandbox" }, { "path": "./packages/fs/tool-fs" }, { "path": "./packages/fs/tool-fs-search" }, + { "path": "./packages/fs/tool-str-replace-editor" }, { "path": "./packages/compaction/compaction" }, { "path": "./packages/compaction/compaction-basic" }, { "path": "./packages/compaction/command-compact" }, From d3281d0218c3176028eb265a3cef2481e1b5cb28 Mon Sep 17 00:00:00 2001 From: fz Date: Mon, 7 Sep 2026 19:33:48 +0800 Subject: [PATCH 04/58] docs(minimal): explain editor opt-in configuration --- ...mal-profiles-persistent-shell-only.i18n.yaml | 4 ++-- ...03-minimal-profiles-persistent-shell-only.md | 4 ++-- ...minimal-profiles-persistent-shell-only.zh.md | 4 ++-- docs/user/guide/python-sdk.i18n.yaml | 4 ++-- docs/user/guide/python-sdk.md | 17 +++++++++++++++++ docs/user/guide/python-sdk.zh.md | 17 +++++++++++++++++ 6 files changed, 42 insertions(+), 8 deletions(-) diff --git a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml index 121293f1f0..f57f4f88b9 100644 --- a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml +++ b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md -2026-09-03-minimal-profiles-persistent-shell-only.md: f3a95cc8b80d039ae13f7fc16570a861e0c5cd09 -2026-09-03-minimal-profiles-persistent-shell-only.zh.md: 38f9140e9d49361ec699bbb2885aea57f5e92925 +2026-09-03-minimal-profiles-persistent-shell-only.md: 602106e305c51dd01223b33f0e3880d90e4d183b +2026-09-03-minimal-profiles-persistent-shell-only.zh.md: db7bc7562adb3fc67500a99883531ffd314dd47b diff --git a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md index f3a95cc8b8..602106e305 100644 --- a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md +++ b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md @@ -8,13 +8,13 @@ English | [中文](2026-09-03-minimal-profiles-persistent-shell-only.zh.md) The shipped Web `minimal` preset and standalone `sdk-minimal` profile exposed `str_replace_editor` beside their persistent shell. The editor added a second file-mutation interface and its complete schema to every minimal model request, although the shell already provides file inspection and mutation. It also required a dedicated `fs-local` service that no other row in either minimal composition consumed. -The next-generation model's minimal runtime requires one model-facing tool. Leaving the editor mounted but hidden through a presentation filter would preserve an inactive capability that could reappear when presentation configuration changes. +Using one persistent shell gives the model a consistent file-operation interface and keeps the harness composition aligned with that interface. Leaving the editor mounted but hidden through a presentation filter would preserve an inactive capability that could reappear when presentation configuration changes. ## Decision The shipped minimal compositions expose exactly one platform-selected persistent shell: `bash` on Linux and macOS, or `pwsh` on Windows. Neither composition mounts `@deepseek-ai/dsh-tool-str-replace-editor`, a filesystem tool, or the `fs-local` service that supported the editor. The fixed complete persona, absence of runtime context and compaction, shell timeout, and launch-specific host services remain unchanged. -The standalone editor package and its consumers outside the two minimal compositions remain supported. A trusted user-authored preset or higher profile patch can mount it explicitly; the shipped `minimal` and `sdk-minimal` defaults never insert it. +The standalone editor package remains available in the short term for explicit custom compositions. A trusted user-authored preset or higher profile patch must insert the editor into the Cordis tree with a filesystem provider in the same service scope; the shipped `minimal` and `sdk-minimal` defaults never insert it. The [Python SDK guide](../../../../docs/user/guide/python-sdk.md#opt-in-to-str_replace_editor) provides an executable patch example. Exact composition tests assert the single tool and the absence of a preset-local filesystem service. The `sdk-minimal` bundle test and built config dump assert that its row and dependency allowlists contain neither `fs-local` nor `dsh-tool-str-replace-editor`. Web and packaged-Python model-visible snapshots pin the one-tool schema roster. diff --git a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md index 38f9140e9d..db7bc7562a 100644 --- a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md +++ b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md @@ -8,13 +8,13 @@ Status: implemented 随附 Web `minimal` preset 与独立 `sdk-minimal` profile 在持久 shell 之外还提供 `str_replace_editor`。Shell 已经可以检查和修改文件,editor 仍会为每个极简模型请求增加第二种文件修改接口及其完整 schema。它还要求挂载一个专用 `fs-local` 服务,而两份极简组合中的其他配置项都不使用该服务。 -下一代模型的极简运行时要求只提供一个面向模型的工具。如果保留 editor 的挂载,只通过呈现层过滤隐藏它,那么呈现配置变化时,该能力仍可能重新出现。 +只使用持久 shell 可以为模型提供一致的文件操作接口,并让 harness 组合与这一接口保持一致。如果保留 editor 的挂载,只通过呈现层过滤隐藏它,那么呈现配置变化时,该能力仍可能重新出现。 ## 决策 随附的极简组合只提供一个按平台选择的持久 shell:Linux 与 macOS 使用 `bash`,Windows 使用 `pwsh`。两份组合都不挂载 `@deepseek-ai/dsh-tool-str-replace-editor`、文件系统工具或支撑 editor 的 `fs-local` 服务。固定的 complete persona、运行时上下文与 compaction 的缺失、shell 超时和各启动路径的宿主服务保持不变。 -独立 editor 包及其在两份极简组合之外的消费方继续受支持。受信任的用户自定义 preset 或更高优先级的 profile patch 可以显式挂载它;随附的 `minimal` 与 `sdk-minimal` 默认组合不会插入它。 +独立 editor 包在短期内仍可用于显式自定义组合。受信任的用户自定义 preset 或更高优先级的 profile patch 必须将 editor 插入 Cordis tree,并在同一服务作用域内提供文件系统后端;随附的 `minimal` 与 `sdk-minimal` 默认组合不会插入它。[Python SDK 指南](../../../../docs/user/guide/python-sdk.zh.md#opt-in-to-str_replace_editor)提供可执行的 patch 示例。 精确组合测试会断言单工具清单以及 preset 内不存在文件系统服务。`sdk-minimal` bundle 测试与构建后配置转储会断言配置项和依赖 allowlist 都不含 `fs-local` 或 `dsh-tool-str-replace-editor`。Web 与打包 Python 的模型可见快照会固定单工具 schema 清单。 diff --git a/docs/user/guide/python-sdk.i18n.yaml b/docs/user/guide/python-sdk.i18n.yaml index 1634954baf..476fb26576 100644 --- a/docs/user/guide/python-sdk.i18n.yaml +++ b/docs/user/guide/python-sdk.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/guide/python-sdk.md -python-sdk.md: ee187ac2ef0156c56b6cdea8a2e0fddc32ac8ec2 -python-sdk.zh.md: 961ff2f9f0170cf924fa10e6f0825b5c47e427ef +python-sdk.md: 6fde258d9f4f3e8ba1fd41537129ba5b8f02fd58 +python-sdk.zh.md: f50fd6d2bd37e3dc203b519e07d0df4098cab0c7 diff --git a/docs/user/guide/python-sdk.md b/docs/user/guide/python-sdk.md index ee187ac2ef..6fde258d9f 100644 --- a/docs/user/guide/python-sdk.md +++ b/docs/user/guide/python-sdk.md @@ -129,6 +129,23 @@ The first command initializes the shipped standalone profile. The second forward Another `profile` is valid when it includes `@deepseek-ai/dsh-sdk-app` or another JSON-RPC server row. Missing server rows, unresolved plugins, and invalid patches fail during startup instead of falling back to another composition. + +### Opt in to `str_replace_editor` + +The bundled runtime includes `str_replace_editor`, but `sdk-minimal` omits it from the default Cordis tree. To use it, save this configuration as `editor.patch.yml`; `insert` adds both the editor and the filesystem provider that the minimal profile lacks: + +```yaml +- insert: + - id: fs-local + name: '@deepseek-ai/dsh-fs-local' + config: + cwd: !!js process.cwd() + - id: tool-str-replace-editor + name: '@deepseek-ai/dsh-tool-str-replace-editor' +``` + +Pass `patches=("/absolute/path/to/editor.patch.yml",)` when constructing `DeepSeekHarness(profile="sdk-minimal", ...)`, or put the patch in `$DSH_HOME/profiles/sdk-minimal/cordis.patch.yml` for persistent configuration. On the next runtime launch, model requests include `str_replace_editor` beside the persistent shell. The local filesystem provider uses the runtime working directory for relative paths; like the minimal shell, it does not confine access to that directory. For the standard `sdk` profile, insert only the editor row so it uses the existing filesystem provider and policies. + ## Understand the minimal profile | Property | Value | diff --git a/docs/user/guide/python-sdk.zh.md b/docs/user/guide/python-sdk.zh.md index 961ff2f9f0..f50fd6d2bd 100644 --- a/docs/user/guide/python-sdk.zh.md +++ b/docs/user/guide/python-sdk.zh.md @@ -129,6 +129,23 @@ dsh plugin --profile sdk-minimal add file:C:/work/my-plugin-bundle 另一个 `profile` 只有包含 `@deepseek-ai/dsh-sdk-app` 或另一个 JSON-RPC server 配置项时才有效。缺失 server 配置项、无法解析的插件和非法 patch 会在启动时失败,不会回退到其他组合。 + +### 显式启用 `str_replace_editor` + +随附运行时包含 `str_replace_editor`,但 `sdk-minimal` 的默认 Cordis tree 不挂载它。要使用该工具,请将以下配置保存为 `editor.patch.yml`;`insert` 会添加 editor,以及极简 profile 缺少的文件系统后端: + +```yaml +- insert: + - id: fs-local + name: '@deepseek-ai/dsh-fs-local' + config: + cwd: !!js process.cwd() + - id: tool-str-replace-editor + name: '@deepseek-ai/dsh-tool-str-replace-editor' +``` + +构造 `DeepSeekHarness(profile="sdk-minimal", ...)` 时传入 `patches=("/absolute/path/to/editor.patch.yml",)`,或将 patch 写入 `$DSH_HOME/profiles/sdk-minimal/cordis.patch.yml` 以持久保存配置。下次运行时启动后,模型请求会在持久 shell 之外包含 `str_replace_editor`。本地文件系统后端以运行时工作目录解析相对路径;与极简 shell 一样,它不会将访问限制在该目录内。对于标准 `sdk` profile,只插入 editor 配置项,让它使用已有的文件系统后端与策略。 + ## 理解极简 profile | 属性 | 值 | From 5c98a81a50a27e3e492d73471ce421ac5b4d0d6f Mon Sep 17 00:00:00 2001 From: fz Date: Mon, 7 Sep 2026 19:43:49 +0800 Subject: [PATCH 05/58] test(benchmarks): explicitly compose the SDK file editor --- .../2026-09-06-backend-continuation-performance.i18n.yaml | 4 ++-- .../testing/2026-09-06-backend-continuation-performance.md | 2 ++ .../2026-09-06-backend-continuation-performance.zh.md | 2 ++ benchmarks/agent-continuation/README.i18n.yaml | 4 ++-- benchmarks/agent-continuation/README.md | 2 +- benchmarks/agent-continuation/README.zh.md | 2 +- .../agent-continuation/profile-continuation.worker.ts | 7 +++++-- 7 files changed, 15 insertions(+), 8 deletions(-) diff --git a/.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.i18n.yaml b/.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.i18n.yaml index dfa20e45f0..ab338ea560 100644 --- a/.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.i18n.yaml +++ b/.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.md -2026-09-06-backend-continuation-performance.md: f4316e790cf62f5027a0f7bfb2d3148cc79e7536 -2026-09-06-backend-continuation-performance.zh.md: 0fb36ba5375c61e907791b31d96f09062b7f62a3 +2026-09-06-backend-continuation-performance.md: 62d8af10015cc2da7b399aae3c9d197f75931753 +2026-09-06-backend-continuation-performance.zh.md: 7c8904fa019b27362e2cdb0e50697921913e5d09 diff --git a/.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.md b/.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.md index f4316e790c..62d8af1001 100644 --- a/.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.md +++ b/.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.md @@ -23,6 +23,8 @@ The shared history has 800 completed two-step turns, four tool calls per turn, a The tool execution pipeline, request preparation, Session projections required by those services, persistence, and catalog observations remain production code. Only the model adapter and bounded tool body are synthetic. The adapter retains a request counter, not request objects, so the fixture cannot manufacture a growing retention cost. Sequential input means each idle interval belongs to the one request delivered by this worker; it does not generalize idle to a per-message completion API under concurrent input. +The SDK fixture explicitly inserts `fs-local` and `str_replace_editor` through its profile patch. This preserves the calibrated file-view workload independently of the [minimal profile's shell-only defaults](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.md). File reads, timing endpoints, and budgets remain the same. + Five samples report raw wall time, CPU user/system time, peak RSS, endpoint counts, and the minimum, median, and maximum total wall time. Budgets enforce the unrounded median. Continuation additionally measures retained heap against an initialized Host: two explicit GCs separated by an event-loop yield precede and follow the timed operation, while the idle Agent remains reachable. The measured delta therefore includes the resident historical Session and live additions, not just newly appended turns. GC and teardown are outside timing; flush is inside. Request-history retention starts after resume and is diagnostic only. Catalog peak RSS is diagnostic; no retained-heap budget claims to measure already-released child observations. The parent bounds every child to 60 seconds, checks timeout, signal, exit, and report independently, awaits process close, and removes private roots after failures. Context and Agent teardown run in finally blocks. Seed processes cannot warm the measured process's caches. Filesystem caches are not forcibly evicted: cold means a fresh process, not cold physical storage. diff --git a/.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.zh.md b/.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.zh.md index 0fb36ba537..7c8904fa01 100644 --- a/.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.zh.md +++ b/.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.zh.md @@ -23,6 +23,8 @@ Status: implemented 工具执行管线、请求准备、这些服务所需的 Session 投影、持久化和目录观察均保留生产代码。只有模型适配器和有界工具体是合成的。适配器只保留请求计数,不保留请求对象,因此 fixture(测试前置数据)不会制造不断增长的保留成本。顺序输入使每个空闲区间对应此 worker 提交的唯一请求;这不代表并发输入时可以把空闲状态推广为逐消息完成 API。 +SDK fixture 通过 profile patch 显式插入 `fs-local` 和 `str_replace_editor`。这使经校准的文件查看负载不依赖[极简 profile 只提供 shell 的默认组合](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md)。文件读取、计时终点和预算保持不变。 + 五个样本报告原始壁钟时间、CPU 用户态/内核态时间、峰值 RSS、终点计数及总壁钟时间的最小值、中位数和最大值。预算约束未经舍入的中位数。续聊还相对已初始化 Host 测量保留堆内存:计时操作前后各执行两次显式 GC,中间让出一次事件循环,空闲 Agent 始终可达。因此该增量包含常驻历史 Session 和实时追加,而不只是新轮次。GC 与资源释放不计时;flush 计时。请求历史的内存基线从恢复后开始,只作诊断。目录峰值 RSS 仅作诊断;没有保留堆预算声称衡量已经释放的子会话观察。 父进程为每个子进程设置 60 秒上限,独立检查超时、信号、退出状态和报告,等待进程关闭,并在失败后删除私有根目录。Context 和 Agent 在 finally 中释放。播种进程无法预热被测进程的缓存。不强制清除文件系统缓存:冷指新进程,不指冷物理存储。 diff --git a/benchmarks/agent-continuation/README.i18n.yaml b/benchmarks/agent-continuation/README.i18n.yaml index 1db841dc14..565f8ef6a7 100644 --- a/benchmarks/agent-continuation/README.i18n.yaml +++ b/benchmarks/agent-continuation/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 benchmarks/agent-continuation/README.md -README.md: 5f76b67805a906ea13321ab22eb671d41bfc3190 -README.zh.md: 54d9e1a0e950d14ce5a01568bc9e1cdb61de597a +README.md: 6c682f506c5ea62004a885731dd56e3a679a1d86 +README.zh.md: 123fe54f1175e166d83f38ad27df4a46abb6721b diff --git a/benchmarks/agent-continuation/README.md b/benchmarks/agent-continuation/README.md index 5f76b67805..6c682f506c 100644 --- a/benchmarks/agent-continuation/README.md +++ b/benchmarks/agent-continuation/README.md @@ -4,7 +4,7 @@ English | [中文](README.zh.md) ## Summary -Measure long-history request processing, cold tool-heavy continuation, and repeated discovery of inactive fork children without network services or recorded user data. The SDK variant drives 100 turns and 800 real file reads through the shipped sdk-minimal profile; other cases isolate backend service costs. No case renders a browser. +Measure long-history request processing, cold tool-heavy continuation, and repeated discovery of inactive fork children without network services or recorded user data. The SDK variant drives 100 turns and 800 real file reads through the shipped sdk-minimal profile with an explicit editor patch; other cases isolate backend service costs. No case renders a browser. ## Table of Contents diff --git a/benchmarks/agent-continuation/README.zh.md b/benchmarks/agent-continuation/README.zh.md index 54d9e1a0e9..123fe54f11 100644 --- a/benchmarks/agent-continuation/README.zh.md +++ b/benchmarks/agent-continuation/README.zh.md @@ -4,7 +4,7 @@ ## Summary -在不使用网络服务或录制用户数据的情况下,测量长历史请求处理、冷工具密集续聊和重复发现非活动 fork 子会话。SDK 变体通过已发布 sdk-minimal profile 执行 100 个轮次和 800 次真实文件读取;其他用例隔离后端服务成本。所有用例均不渲染浏览器。 +在不使用网络服务或录制用户数据的情况下,测量长历史请求处理、冷工具密集续聊和重复发现非活动 fork 子会话。SDK 变体通过已发布 sdk-minimal profile 和显式 editor patch 执行 100 个轮次和 800 次真实文件读取;其他用例隔离后端服务成本。所有用例均不渲染浏览器。 ## Table of Contents diff --git a/benchmarks/agent-continuation/profile-continuation.worker.ts b/benchmarks/agent-continuation/profile-continuation.worker.ts index 4e0fade29b..531fc2f50d 100644 --- a/benchmarks/agent-continuation/profile-continuation.worker.ts +++ b/benchmarks/agent-continuation/profile-continuation.worker.ts @@ -1,4 +1,4 @@ -/** End-to-end SDK continuation through the built dsh sdk-minimal profile and real file tools. */ +/** End-to-end SDK continuation through built dsh sdk-minimal with an explicitly mounted file editor. */ import { mkdir, writeFile } from 'node:fs/promises' import { join } from 'node:path' @@ -27,7 +27,10 @@ async function run(root: string): Promise { await writeFile(patch, [ '- id: llm-deepseek', ' disabled: true', '- id: sessions', ' config:', ' root: ' + JSON.stringify(join(root, 'profile-sessions')), ' compression: zstd', - '- insert:', ' - id: benchmark-model', ' name: ' + JSON.stringify(join(import.meta.dirname, 'profile-adapter.js')), + '- insert:', + ' - id: fs-local', " name: '@deepseek-ai/dsh-fs-local'", + ' - id: str-replace-editor', " name: '@deepseek-ai/dsh-tool-str-replace-editor'", + ' - id: benchmark-model', ' name: ' + JSON.stringify(join(import.meta.dirname, 'profile-adapter.js')), '', ].join('\n')) const env: NodeJS.ProcessEnv = { From b92cc94122b98c98c9c5b331f517b1f2a297d603 Mon Sep 17 00:00:00 2001 From: fz Date: Mon, 7 Sep 2026 22:56:48 +0800 Subject: [PATCH 06/58] test(minimal): cover documented editor opt-in --- ...l-profiles-persistent-shell-only.i18n.yaml | 4 +- ...-minimal-profiles-persistent-shell-only.md | 2 +- ...nimal-profiles-persistent-shell-only.zh.md | 2 +- .../tests/profiles/sdk/keyless-smoke.e2e.ts | 62 ++++++++++++++++--- apps/cli/tests/web-agent-presets.e2e.ts | 7 ++- .../sdk-minimal/tests/sdk-minimal.spec.ts | 2 - .../client/ui-user-questions/src/index.ts | 11 ++-- .../tests/node-plugin.client.spec.ts | 6 +- 8 files changed, 68 insertions(+), 28 deletions(-) diff --git a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml index f57f4f88b9..c5c44eded0 100644 --- a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml +++ b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md -2026-09-03-minimal-profiles-persistent-shell-only.md: 602106e305c51dd01223b33f0e3880d90e4d183b -2026-09-03-minimal-profiles-persistent-shell-only.zh.md: db7bc7562adb3fc67500a99883531ffd314dd47b +2026-09-03-minimal-profiles-persistent-shell-only.md: 34229d17913e9fe944bbb458ec439eaf93e57fe8 +2026-09-03-minimal-profiles-persistent-shell-only.zh.md: 4183696f7c1feece4599ec6ce0e000c0d949aceb diff --git a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md index 602106e305..34229d1791 100644 --- a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md +++ b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md @@ -16,7 +16,7 @@ The shipped minimal compositions expose exactly one platform-selected persistent The standalone editor package remains available in the short term for explicit custom compositions. A trusted user-authored preset or higher profile patch must insert the editor into the Cordis tree with a filesystem provider in the same service scope; the shipped `minimal` and `sdk-minimal` defaults never insert it. The [Python SDK guide](../../../../docs/user/guide/python-sdk.md#opt-in-to-str_replace_editor) provides an executable patch example. -Exact composition tests assert the single tool and the absence of a preset-local filesystem service. The `sdk-minimal` bundle test and built config dump assert that its row and dependency allowlists contain neither `fs-local` nor `dsh-tool-str-replace-editor`. Web and packaged-Python model-visible snapshots pin the one-tool schema roster. +Exact composition tests assert the single tool and the absence of a preset-local filesystem service. The `sdk-minimal` bundle test and built config dump assert that its row and dependency allowlists contain neither `fs-local` nor `dsh-tool-str-replace-editor`. Web and packaged-Python model-visible snapshots pin the one-tool schema roster. SDK profile smoke tests execute the guide's editor patch and verify file creation and viewing. This decision partially supersedes the tool selection in [the bare minimal runtime](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md) and the minimal exception in [the base editor decision](2026-09-05-base-default-file-editor.md). Those notes retain authority for prompt ownership, no-compaction behavior, and base-backed file editing. [The application architecture](../../../../docs/architecture.md) owns profile launch and bundle layering. diff --git a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md index db7bc7562a..4183696f7c 100644 --- a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md +++ b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md @@ -16,7 +16,7 @@ Status: implemented 独立 editor 包在短期内仍可用于显式自定义组合。受信任的用户自定义 preset 或更高优先级的 profile patch 必须将 editor 插入 Cordis tree,并在同一服务作用域内提供文件系统后端;随附的 `minimal` 与 `sdk-minimal` 默认组合不会插入它。[Python SDK 指南](../../../../docs/user/guide/python-sdk.zh.md#opt-in-to-str_replace_editor)提供可执行的 patch 示例。 -精确组合测试会断言单工具清单以及 preset 内不存在文件系统服务。`sdk-minimal` bundle 测试与构建后配置转储会断言配置项和依赖 allowlist 都不含 `fs-local` 或 `dsh-tool-str-replace-editor`。Web 与打包 Python 的模型可见快照会固定单工具 schema 清单。 +精确组合测试会断言单工具清单以及 preset 内不存在文件系统服务。`sdk-minimal` bundle 测试与构建后配置转储会断言配置项和依赖 allowlist 都不含 `fs-local` 或 `dsh-tool-str-replace-editor`。Web 与打包 Python 的模型可见快照会固定单工具 schema 清单。SDK profile 冒烟测试会执行指南中的 editor patch,并验证文件创建和查看。 本决策部分取代[极简裸运行时](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md)中的工具选择,以及[base 编辑器决策](2026-09-05-base-default-file-editor.zh.md)中的极简例外。这些 Agent Note 继续负责提示词所有权、无 compaction 行为和基于 base 的文件编辑。[应用架构](../../../../docs/architecture.zh.md)负责 profile 启动与 bundle 分层。 diff --git a/apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts b/apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts index cc5f78940a..c879e5b8b9 100644 --- a/apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts +++ b/apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts @@ -189,8 +189,25 @@ describe('Python SDK dsh profile keyless smoke', () => { } }, 40_000) - it('boots the standalone minimal profile through its generated manifest', async () => { + it.each([ + { label: 'boots the standalone minimal profile through its generated manifest', editorEnabled: false }, + { label: 'executes the documented editor opt-in patch with sdk-minimal', editorEnabled: true }, + ])('$label', async ({ editorEnabled }) => { const root = await mkdtemp(join(tmpdir(), 'dsh-python-sdk-minimal-')) + const editorPatch = join(root, 'editor.patch.yml') + if (editorEnabled) { + const guide = await readFile(join(repoRoot, 'docs/user/guide/python-sdk.md'), 'utf8') + const yaml = guide.split('')[1] + ?.match(/```yaml\n([\s\S]*?)```/)?.[1] + expect(yaml).toBeDefined() + await writeFile(editorPatch, yaml!) + } + const editorFile = join(root, 'editor.txt') + const editorContent = 'sdk-minimal editor opt-in\n' + const editorCalls = editorEnabled ? [ + { command: 'create', path: editorFile, file_text: editorContent }, + { command: 'view', path: editorFile }, + ] : [] const modelRequests: Record[] = [] const modelServer = createServer((request, response) => { let body = '' @@ -200,8 +217,21 @@ describe('Python SDK dsh profile keyless smoke', () => { modelRequests.push(JSON.parse(body) as Record) response.writeHead(200, { 'content-type': 'text/event-stream' }) response.write('data: {"choices":[{"delta":{"role":"assistant","content":null}}]}\n\n') - response.write('data: {"choices":[{"delta":{"content":"done"}}]}\n\n') - response.write('data: {"choices":[{"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":1}}\n\n') + const toolCall = editorCalls[modelRequests.length - 1] + if (toolCall) { + response.write(`data: ${JSON.stringify({ choices: [{ delta: { tool_calls: [{ + index: 0, + id: `editor-${toolCall.command}`, + type: 'function', + function: { name: 'str_replace_editor', arguments: JSON.stringify(toolCall) }, + }] } }] })}\n\n`) + } else { + response.write('data: {"choices":[{"delta":{"content":"done"}}]}\n\n') + } + response.write(`data: ${JSON.stringify({ + choices: [{ delta: {}, finish_reason: toolCall ? 'tool_calls' : 'stop' }], + usage: { prompt_tokens: 3, completion_tokens: 1 }, + })}\n\n`) response.end('data: [DONE]\n\n') }) }) @@ -214,6 +244,7 @@ describe('Python SDK dsh profile keyless smoke', () => { binScript, '--profile', 'sdk-minimal', + ...(editorEnabled ? ['--patch', editorPatch] : []), ], { cwd: repoRoot, env: { @@ -251,11 +282,14 @@ describe('Python SDK dsh profile keyless smoke', () => { method: 'session/prompt', params: { sessionId: 'minimal', contentBlocks: [{ type: 'text', text: 'inspect tools' }] }, })}\n`) - await waitForLine(lines, (value) => { + const turnEnd = await waitForLine(lines, (value) => { const params = value.params as Record | undefined const event = params?.event as Record | undefined return params?.sessionId === 'minimal' && event?.type === 'turn/end' }, () => stderr) + expect(turnEnd).toMatchObject({ + params: { event: { data: { reason: { kind: 'completed' } } } }, + }) const profile = JSON.parse( await readFile(join(root, '.dsh', 'profiles', 'sdk-minimal', 'package.json'), 'utf8'), @@ -266,13 +300,27 @@ describe('Python SDK dsh profile keyless smoke', () => { }) expect(modelRequests[0]?.tools).toEqual(expect.any(Array)) const tools = modelRequests[0]?.tools as { function?: { name?: string } }[] - expect(tools.map(tool => tool.function?.name).sort()).toEqual( - [process.platform === 'win32' ? 'pwsh' : 'bash'], - ) + expect(tools.map(tool => tool.function?.name)).toEqual([ + process.platform === 'win32' ? 'pwsh' : 'bash', + ...(editorEnabled ? ['str_replace_editor'] : []), + ]) + expect(modelRequests).toHaveLength(editorEnabled ? 3 : 1) + if (editorEnabled) { + expect(await readFile(editorFile, 'utf8')).toBe(editorContent) + expect(modelRequests[2]?.messages).toEqual(expect.arrayContaining([ + expect.objectContaining({ + role: 'tool', + tool_call_id: 'editor-view', + content: expect.stringContaining(editorContent.trim()) as unknown, + }), + ])) + } child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'shutdown' })}\n`) await waitForLine(lines, value => value.id === 3, () => stderr) const exit = await child + expect(exit.timedOut, stderr).toBe(false) + expect(exit.signal, stderr).toBeUndefined() expect(exit.exitCode, `signal=${String(exit.signal)}; stderr=${stderr}`).toBe(0) } finally { child.kill('SIGKILL') diff --git a/apps/cli/tests/web-agent-presets.e2e.ts b/apps/cli/tests/web-agent-presets.e2e.ts index 942ab7b895..75d5947be3 100644 --- a/apps/cli/tests/web-agent-presets.e2e.ts +++ b/apps/cli/tests/web-agent-presets.e2e.ts @@ -189,9 +189,7 @@ describe('the shipped Web composition', () => { it('leaves the global tool layer empty', () => { // Every model-facing tool belongs to a preset, `ask_user_question` // included: a tool in the global layer reaches EVERY agent regardless of - // which preset composed it, so a single-tool benchmark surface would really - // present two. A regression here means an agent-plane row came back to - // the host composition. + // which preset composed it, expanding that preset's tool list. expect(toolNames(ctx)).toEqual([]) }) @@ -300,7 +298,10 @@ describe('the shipped Web composition', () => { expect(assembly.tools.map(tool => tool.name)).toEqual(['bash']) expect(assembly.tools.find(tool => tool.name === 'bash')?.description).toBe(MINIMAL_BASH_DESCRIPTION) expect(ctx.commands.find(handle.agent, 'goal')).toBeUndefined() + // serviceFor reports preset-owned providers; unisolated consumers inherit the host fs. expect(ctx.agentPresets.serviceFor(handle.agent, 'fs')).toBeUndefined() + expect(ctx.get('fs')?.sandboxMode).toBeDefined() + expect(handle.agent.ctx.get('fs')?.sandboxMode).toBe(ctx.get('fs')?.sandboxMode) expect(ctx.agentPresets.serviceFor(handle.agent, 'compaction')).toBeUndefined() expect(handle.agent.ctx.get('compaction')).toBeUndefined() } finally { diff --git a/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts b/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts index 7e4f41dd89..39867f9d21 100644 --- a/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts +++ b/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts @@ -81,8 +81,6 @@ describe('dsh-sdk-minimal bundle', () => { disabled: { __jsExpr: "process.platform !== 'win32'" }, config: { shellDialect: 'pwsh', timeoutMs: 300000 }, }) - expect(manifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-fs-local') - expect(manifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-tool-str-replace-editor') expect(Object.keys(manifest.dependencies ?? {}).sort()).toEqual( [...new Set(rows.map(row => row.name).filter((name): name is string => name !== undefined).map(packageName))].sort(), ) diff --git a/packages/client/ui-user-questions/src/index.ts b/packages/client/ui-user-questions/src/index.ts index 4ceb9e0bd1..9357c3bba6 100644 --- a/packages/client/ui-user-questions/src/index.ts +++ b/packages/client/ui-user-questions/src/index.ts @@ -1,13 +1,10 @@ /** * Web question plugin, node half. * - * Deliberately empty. Mounting `ask_user_question` here put it in the tools - * registry's GLOBAL layer, so every agent saw it no matter which preset - * composed it — a two-tool benchmark preset actually presented three, and a - * locally authored `bash-only` preset presented two. Rendering a question is - * a host UI capability; having the tool is an agent capability, and only a - * preset decides that. The `tool-ask-user` row belongs in the presets that - * want it (and in the TUI composition, which has no presets). + * Mounting `ask_user_question` in the tools registry's global layer expands + * every agent's tool list regardless of its preset. Rendering a question is + * a host UI capability; the model-facing tool belongs to the presets that + * include it and to the TUI composition, which has no presets. */ /** Host plugin body — the model-facing tool is composed per preset, not here. */ diff --git a/packages/client/ui-user-questions/tests/node-plugin.client.spec.ts b/packages/client/ui-user-questions/tests/node-plugin.client.spec.ts index 8cd942a80a..1ffb0a9caf 100644 --- a/packages/client/ui-user-questions/tests/node-plugin.client.spec.ts +++ b/packages/client/ui-user-questions/tests/node-plugin.client.spec.ts @@ -21,11 +21,7 @@ describe('ui-user-questions node plugin', () => { await ctx.plugin({ apply }).await() - // Selecting the Web question FEATURE must not hand every agent the tool. - // `ctx.tools.register` on an unscoped host context files into the global - // layer, which merges into every agent's view regardless of the preset - // that composed it — so a two-tool benchmark preset would really present - // three. The `tool-ask-user` row belongs to the presets that want it. + // Host UI registration must leave each preset's model-facing tool list intact. expect(ctx.tools.get('ask_user_question')).toBeUndefined() }) }) From fe03b0686df1d9513144ee2d74276529a18b0d7a Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Tue, 8 Sep 2026 10:28:45 +0800 Subject: [PATCH 07/58] fix(llm-pi-ai): keep catalog-invalid settings repairable --- ...-14-provider-routed-llm-adapters.i18n.yaml | 4 +- ...2026-07-14-provider-routed-llm-adapters.md | 2 +- ...6-07-14-provider-routed-llm-adapters.zh.md | 2 +- ...-pi-ai-settings-catalog-recovery.i18n.yaml | 6 + ...6-09-07-pi-ai-settings-catalog-recovery.md | 39 +++++++ ...9-07-pi-ai-settings-catalog-recovery.zh.md | 39 +++++++ .../stored-error.expected.md | 37 ++++++ .../web/tests/models-settings-recovery.e2e.ts | 89 ++++++++++++++ apps/web/tsconfig.json | 1 + docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 2 +- docs/config-catalog.zh.md | 2 +- docs/subsystems/llm-streaming.i18n.yaml | 4 +- docs/subsystems/llm-streaming.md | 2 + docs/subsystems/llm-streaming.zh.md | 2 + docs/subsystems/settings.i18n.yaml | 4 +- docs/subsystems/settings.md | 8 ++ docs/subsystems/settings.zh.md | 8 ++ .../ui-settings-models/README.i18n.yaml | 4 +- packages/client/ui-settings-models/README.md | 2 + .../client/ui-settings-models/README.zh.md | 2 + .../src/client/ModelsSection.tsx | 74 +++++++----- .../ui-settings-models/src/client/store.ts | 23 ++-- .../tests/components.client.spec.tsx | 35 ++++++ .../tests/store.client.spec.ts | 10 +- .../extensions/tool-cordis/src/api-catalog.ts | 6 +- packages/llm/llm-pi-ai/README.i18n.yaml | 4 +- packages/llm/llm-pi-ai/README.md | 2 + packages/llm/llm-pi-ai/README.zh.md | 2 + packages/llm/llm-pi-ai/src/adapter.ts | 16 ++- packages/llm/llm-pi-ai/src/catalog.ts | 54 ++++++--- packages/llm/llm-pi-ai/src/config.ts | 109 +++++++++++------- packages/llm/llm-pi-ai/src/index.ts | 21 ++-- packages/llm/llm-pi-ai/src/provider.ts | 4 +- packages/llm/llm-pi-ai/tests/catalog.spec.ts | 38 +++--- .../llm-pi-ai/tests/compat-upgrade.spec.ts | 2 +- packages/llm/llm-pi-ai/tests/config.spec.ts | 11 +- .../llm-pi-ai/tests/dynamic-config.spec.ts | 64 +++++++++- packages/llm/llm/README.i18n.yaml | 4 +- packages/llm/llm/README.md | 2 +- packages/llm/llm/README.zh.md | 2 +- packages/llm/llm/src/index.ts | 5 +- packages/llm/llm/src/types.ts | 2 + packages/llm/llm/tests/service.spec.ts | 1 + packages/settings/settings/README.i18n.yaml | 4 +- packages/settings/settings/README.md | 2 + packages/settings/settings/README.zh.md | 2 + packages/settings/settings/src/index.ts | 20 ++++ .../settings/settings/tests/settings.spec.ts | 29 +++++ tsconfig.host.json | 1 + 50 files changed, 655 insertions(+), 157 deletions(-) create mode 100644 .agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.i18n.yaml create mode 100644 .agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.md create mode 100644 .agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.zh.md create mode 100644 apps/web/tests/expected/models-settings-recovery/stored-error.expected.md create mode 100644 apps/web/tests/models-settings-recovery.e2e.ts diff --git a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml index d757a5efd0..06c136e20b 100644 --- a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md -2026-07-14-provider-routed-llm-adapters.md: 4cc3cc3cdceeefdfea864ccf3465e528e32159c7 -2026-07-14-provider-routed-llm-adapters.zh.md: 098a6af7c951c6dee8a1b4f948bc69725e8d189c +2026-07-14-provider-routed-llm-adapters.md: 24d6e7e439dc74a158801b647ddac96169730af1 +2026-07-14-provider-routed-llm-adapters.zh.md: f740468ed67830dabf5769eca206402d91c90aac diff --git a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md index 4cc3cc3cdc..24d6e7e439 100644 --- a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md +++ b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md @@ -30,7 +30,7 @@ A provider has exactly one adapter owner in a Cordis context. `dsh-llm-deepseek` `dsh-llm-pi-ai` takes one non-empty list of provider profiles. Provider names must be unique within the list and present in pi-ai's `getProviders()` result. Each profile contains the provider name plus optional `apiKey`, `baseURL`, headers, reasoning level and budgets, cache retention, transport, SDK timeouts, a Harness stream-idle timeout, and a provider-owned `retryPolicy`. The adapter forces pi-ai's `maxRetries` to zero so one `stream()` call makes one visible provider attempt, while `dsh-llm-retry` executes the resolved policy at the agent failed-step extension point. Credentials are never global: an explicit key applies only to its profile, while an absent key lets pi-ai resolve its standard environment variable, OAuth token, AWS credential chain, Google ADC, or other provider-native ambient authentication. An explicitly empty key is invalid configuration rather than an environment fallback. -The plugin registers all configured provider names against one `PiAiAdapter` in one all-or-nothing call. A request uses its provider to select the matching profile and finds its model in `getModels(provider)` to obtain the catalog descriptor. An unknown provider fails at plugin load; an unknown model fails before network I/O with `UNKNOWN_MODEL`. The catalog object is never mutated. When a profile supplies `baseURL`, the adapter clones the selected descriptor and overrides only `baseUrl`, so a private endpoint can retain pi-ai's API, capabilities, compatibility flags, context limits, and reasoning map. The private endpoint must implement the selected provider's protocol, and the model id must still exist in the installed pi-ai catalog. +The plugin registers configured provider names against one `PiAiAdapter` in one atomic call. Each immutable request snapshot combines the effective profiles and their serviceable model descriptors. Catalog-external models require an explicit or inferable protocol and endpoint. Stored catalog errors remain visible and repairable under the [settings catalog recovery decision](../bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.md), while writes validate changed providers and requests reject the selected failed model before network I/O. The adapter calls pi-ai's `streamSimple()` so each catalog model chooses its registered API implementation, including OpenAI Responses instead of Chat Completions where the descriptor says `openai-responses`. Harness temperature, maximum tokens, signal, session id, and the profile's common stream options flow through directly. Profile headers merge with the mandatory Harness attribution headers, with Harness attribution winning its reserved names. The adapter no longer maintains DeepSeek-specific payload rewrites or a provider-protocol matrix. diff --git a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md index 098a6af7c9..f740468ed6 100644 --- a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md @@ -30,7 +30,7 @@ Status: implemented `dsh-llm-pi-ai` 接受一个非空的提供方配置列表。列表内的提供方名称必须唯一,并且存在于 pi-ai 的 `getProviders()` 结果中。每项配置包含提供方名称,以及可选的 `apiKey`、`baseURL`、headers、推理级别和预算、缓存保留设置、传输方式、SDK 超时、Harness 流空闲超时,以及由提供方拥有的 `retryPolicy`。适配器强制将 pi-ai 的 `maxRetries` 设为零,使一次 `stream()` 调用只发起一次可见的提供方请求;`dsh-llm-retry` 则在 agent 失败步骤扩展点上执行解析后的策略。凭据不设全局值:显式密钥仅对所属配置生效;未提供密钥时,pi-ai 使用标准环境变量、OAuth token、AWS 凭据链、Google ADC 或其他提供方原生环境认证。显式空密钥属于无效配置,不会回退到环境认证。 -插件通过一次全有或全无调用,将所有已配置的提供方名称注册到同一个 `PiAiAdapter`。请求按 provider 选择对应配置,并在 `getModels(provider)` 中查找模型以取得目录描述符。未知提供方会在插件加载时失败;未知模型会在网络 I/O 前以 `UNKNOWN_MODEL` 失败。适配器不会修改目录对象。当配置提供 `baseURL` 时,适配器复制选中的描述符,仅覆盖 `baseUrl`,使私有端点保留 pi-ai 的 API、能力、兼容标志、上下文限制与推理映射。私有端点必须实现所选提供方的协议,模型 ID 也仍须存在于已安装的 pi-ai 目录中。 +插件通过一次原子调用,将已配置的提供方名称注册到同一个 `PiAiAdapter`。每个不可变请求快照组合有效 profile 与可服务模型的描述符。目录外模型需要显式指定或可推断的协议与端点。根据[设置目录恢复决策](../bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.zh.md),已存储的目录错误保持可见、可修复,写入仍会校验已修改提供方,请求则在网络 I/O 前拒绝所选错误模型。 适配器调用 pi-ai 的 `streamSimple()`,因此每个目录模型会选择其注册的 API 实现;描述符为 `openai-responses` 时使用 OpenAI Responses,而非 Chat Completions。Harness 的 temperature、最大 token 数、signal、session ID,以及提供方配置中的通用流选项均直接传递。配置 headers 与 Harness 强制归因 headers 合并;发生保留名称冲突时,以 Harness 归因为准。适配器不再维护 DeepSeek 专用 payload 重写或提供方协议矩阵。 diff --git a/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.i18n.yaml new file mode 100644 index 0000000000..1db4095228 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.md +2026-09-07-pi-ai-settings-catalog-recovery.md: 1af5a3cb5bd0a406103490cd1b87951f1956e6cd +2026-09-07-pi-ai-settings-catalog-recovery.zh.md: 3cab1f939b7d1589b17ddf82283dc1beae6509fc diff --git a/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.md b/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.md new file mode 100644 index 0000000000..1af5a3cb5b --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.md @@ -0,0 +1,39 @@ +# Agent Note: Repairable pi-ai settings after catalog changes + +Status: implemented + +English | [中文](2026-09-07-pi-ai-settings-catalog-recovery.zh.md) + +## Problem + +An installed pi-ai catalog can change the validity of unchanged user settings. OpenRouter models outside the catalog can inherit a protocol while all shipped models agree; adding a second protocol removes that inference. Removing a catalog model also invalidates an override keyed by its former id. Rejecting the entire settings namespace at registration makes unrelated providers disappear and removes the controls needed to repair the configuration. + +## Decision + +Settings separates shared `validate` checks from `validateWrite(next, previous)`. The latter runs inside the namespace's write queue, after resolution and before persistence, for update, replacement, and path mutation. Both values include the composition base. Registration and external reload do not invoke write-only checks; existing consumers retain their shared-validation behavior. + +The pi-ai consumer retains catalog diagnostics when reading stored profiles, while schema and self-contained profile constraints still reject loading. Writes strictly resolve each new or changed provider, comparing effective provider values against the committed snapshot. Unchanged failed providers do not block another provider's edit, and deletion remains possible. Editing a provider-wide setting validates all models it affects. + +Profile resolution keeps valid models beside per-model errors. A missing override retains its diagnostic without disabling the remaining catalog. A route-level catalog failure retains its provider and editable settings but supplies no callable models. The adapter checks the selected model's recorded failure before credentials or network I/O and reports `INVALID_CONFIG`. No protocol is guessed and no user configuration is rewritten during loading. Immutable snapshots still keep an in-flight request on its captured configuration. + +`LlmProviderInfo.configurationError` carries the first diagnostic for the provider row. Registration facts include that diagnostic so configuration repair refreshes the browser. Failed model ids remain in settings, while the model selector receives serviceable entries. Models settings displays the diagnostic and retains edit/delete controls. Both add actions require their owning settings namespace; the ordinary add menu filters out unavailable namespaces. + +This extends the [provider-routed adapter decision](../architecture/2026-07-14-provider-routed-llm-adapters.md): provider ownership and request snapshots remain unchanged, while catalog validity does not determine whether settings can be managed. That note remains active for routing, ownership, and replay rationale. + +## Alternatives considered + +**Reject catalog errors at registration.** This prevents users from repairing an otherwise parseable configuration and lets an unused stale model disable unrelated providers. + +**Relax save validation too.** A newly entered model with no inferable protocol can be rejected immediately with the offending provider and model named. Accepting it creates an avoidable request-time failure. + +**Strictly revalidate the entire namespace on every save.** An unrelated provider's old error would block adding a healthy provider or repairing providers independently. + +**Assign OpenRouter a fixed route protocol.** A route override replaces every model's protocol and can change working catalog entries that intentionally use another API. + +## Consequences + +Upgrade-dependent errors remain visible and repairable without weakening validation of new provider edits. Configuration errors remain distinct from remote model existence: a catalog-external id with an explicit protocol is accepted, and its endpoint decides whether that id exists. Scalar or document errors still fail early. The settings write hook adds no storage format or session event; provider metadata gains one optional diagnostic field. + +## Testing + +Settings tests cover write-only validation, persistence refusal, resolved previous values, and external reload. Adapter tests cover mixed valid/invalid models, deleted override referents, independent provider edits, route deletion, pre-network failure, and repair. The assembled Web expectation boots with stale OpenRouter settings, preserves zai and both add controls, rejects an invalid save without changing the file, and repairs the route by removing the stale model. Existing snapshot tests continue to own request freezing and replay behavior. diff --git a/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.zh.md b/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.zh.md new file mode 100644 index 0000000000..3cab1f939b --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.zh.md @@ -0,0 +1,39 @@ +# Agent Note: pi-ai 目录变化后可修复的设置 + +Status: implemented + +English | [中文](2026-09-07-pi-ai-settings-catalog-recovery.md) + +## Problem + +已安装的 pi-ai 目录可能改变未修改用户设置的有效性。OpenRouter 的目录外模型可以在所有内置模型协议一致时继承协议;加入第二种协议会使这种推断失效。删除目录模型也会使按其旧 ID 声明的覆盖失效。注册时拒绝整个 settings 命名空间,会让无关提供方消失,也移除了修复配置所需的控件。 + +## Decision + +Settings 将共用的 `validate` 校验与 `validateWrite(next, previous)` 分开。后者在命名空间写入队列内部、解析之后、持久化之前执行,覆盖更新、整体替换和路径修改。两个值均包含组合基础层。注册和外部重载不执行仅写入校验;现有消费者保留其共用校验行为。 + +pi-ai 消费者读取已存储 profile 时保留目录诊断,而 schema 与 profile 自身的约束仍会拒绝加载。写入会将有效提供方值与已提交快照比较,严格解析每个新增或修改的提供方。未修改的错误提供方不会阻止其他提供方编辑,删除仍然可用。修改提供方级设置会校验其影响的所有模型。 + +Profile 解析在有效模型旁保留逐模型错误。失去引用目标的覆盖会保留诊断,而不会禁用其余目录。路由级目录失败会保留提供方与可编辑设置,但不提供可调用模型。适配器在解析凭据和网络 I/O 前检查所选模型已记录的错误,并报告 `INVALID_CONFIG`。加载过程不会猜测协议或改写用户配置。不可变快照仍保证进行中的请求使用其捕获的配置。 + +`LlmProviderInfo.configurationError` 为提供方行携带首个诊断。注册事实包含该诊断,因此修复配置会刷新浏览器。错误模型 ID 保留在设置中,模型选择器只接收可服务条目。模型设置页显示诊断并保留编辑、删除控件。两个添加操作都要求其所属 settings 命名空间存在;普通添加菜单会过滤不可用的命名空间。 + +本决策扩展了[按提供方路由的适配器决策](../architecture/2026-07-14-provider-routed-llm-adapters.zh.md):提供方所有权与请求快照不变,目录有效性不决定设置是否可管理。旧记录仍保留为路由、所有权与回放设计的依据。 + +## Alternatives considered + +**在注册时拒绝目录错误。** 这会阻止用户修复结构可解析的配置,并让未使用的过期模型禁用无关提供方。 + +**同时放宽保存校验。** 对无法推断协议的新模型,可以立即拒绝并点名提供方与模型。接受它只会制造可避免的请求时错误。 + +**每次保存都严格重校验整个命名空间。** 无关提供方的旧错误会阻止添加正常提供方,或逐个修复提供方。 + +**为 OpenRouter 指定固定路由协议。** 路由覆盖会替换所有模型的协议,可能改变有意使用其他 API 的正常目录条目。 + +## Consequences + +依赖升级产生的错误仍可见、可修复,而新增提供方编辑的校验不会放宽。配置错误与远端模型是否存在仍然不同:显式指定协议的目录外 ID 可以被接受,由端点决定该 ID 是否存在。标量或文档错误仍尽早失败。Settings 写入钩子不增加存储格式或 session 事件;提供方元数据新增一个可选诊断字段。 + +## Testing + +Settings 测试覆盖仅写入校验、持久化拒绝、已解析前值与外部重载。适配器测试覆盖有效与错误模型混合、覆盖目标被删除、独立提供方编辑、路由删除、联网前失败及修复。完整 Web 期望测试从过期 OpenRouter 设置启动,保留 zai 与两个添加控件,在不改变文件的前提下拒绝无效保存,并通过删除过期模型修复路由。已有快照测试继续负责请求冻结与回放行为。 diff --git a/apps/web/tests/expected/models-settings-recovery/stored-error.expected.md b/apps/web/tests/expected/models-settings-recovery/stored-error.expected.md new file mode 100644 index 0000000000..31fca1116a --- /dev/null +++ b/apps/web/tests/expected/models-settings-recovery/stored-error.expected.md @@ -0,0 +1,37 @@ +- dialog "设置": + - navigation: + - text: 设置 + - button "通用设置": + - img + - text: 通用设置 + - button "模型": + - img + - text: 模型 + - button "插件": + - img + - text: 插件 + - button "Agent 预设": + - img + - text: Agent 预设 + - button "打开配置文件" + - button "关闭": + - img + - text: 关闭 + - heading "模型" [level=2] + - paragraph: 填入各提供方的 API 密钥即可使用其模型。 + - list: + - listitem: + - text: openrouter + - button "编辑 openrouter": 编辑 + - button "删除 openrouter": 删除 + - alert: "llm-pi-ai: provider \"openrouter\" model \"111\" needs an api; the installed catalog does not describe it, so set the route's api to the wire protocol its endpoint speaks" + - listitem: + - text: zai + - button "编辑 zai": 编辑 + - button "删除 zai": 删除 + - button "添加提供方": + - img + - text: 添加提供方 + - button "添加自定义提供方": + - img + - text: 添加自定义提供方 diff --git a/apps/web/tests/models-settings-recovery.e2e.ts b/apps/web/tests/models-settings-recovery.e2e.ts new file mode 100644 index 0000000000..c8e06a5aae --- /dev/null +++ b/apps/web/tests/models-settings-recovery.e2e.ts @@ -0,0 +1,89 @@ +/** Stored catalog drift remains repairable through the assembled Models settings page. */ +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { chromium, type Browser, type Page } from 'playwright' +import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' +import { + captureStableAria, compareOrRefreshGolden, launchWebScaffold, + watchConsole, webSnapshotMode, type WebScaffold, +} from './scaffold.ts' +import { saveFailureShot, ZH_BROWSER_LOCALE } from './support.ts' + +const EXPECTED = fileURLToPath(new URL('./expected/models-settings-recovery/stored-error.expected.md', import.meta.url)) +const FAILURE = 'llm-pi-ai: provider "openrouter" model "111" needs an api; ' + + 'the installed catalog does not describe it, so set the route\'s api to the wire protocol its endpoint speaks' + +describe('web e2e: repairs a stored provider after catalog drift', () => { + let home: string + let scaffold: WebScaffold + let browser: Browser + let page: Page + let tripwire: ReturnType + + beforeAll(async () => { + home = await mkdtemp(join(tmpdir(), 'dsh-models-recovery-')) + await writeFile(join(home, 'settings.yaml'), [ + 'llm-pi-ai:', ' providers:', ' openrouter:', ' models:', + ' - id: "111"', ' zai: {}', '', + ].join('\n')) + scaffold = await launchWebScaffold({ harnessHome: home }) + browser = await chromium.launch() + page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE }) + tripwire = watchConsole(page) + await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) + await page.getByRole('button', { name: '设置', exact: true }).click() + const dialog = page.getByRole('dialog', { name: '设置' }) + await dialog.getByRole('button', { name: '模型', exact: true }).click() + await dialog.getByText(FAILURE, { exact: true }).waitFor() + }, 120_000) + + afterAll(async () => { + try { + await browser?.close() + } finally { + try { + await scaffold?.close() + } finally { + if (home !== undefined) await rm(home, { recursive: true, force: true }) + } + } + }) + + it('shows the failed provider beside healthy providers and keeps both add actions usable', async () => { + onTestFailed(() => saveFailureShot(page, 'models-settings-recovery')) + const dialog = page.getByRole('dialog', { name: '设置' }) + expect(await dialog.getByRole('button', { name: '编辑 openrouter', exact: true }).count()).toBe(1) + expect(await dialog.getByRole('button', { name: '编辑 zai', exact: true }).count()).toBe(1) + expect(await dialog.getByRole('button', { name: '添加提供方', exact: true }).isEnabled()).toBe(true) + expect(await dialog.getByRole('button', { name: '添加自定义提供方', exact: true }).isEnabled()).toBe(true) + await compareOrRefreshGolden(EXPECTED, await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd), webSnapshotMode()) + + await dialog.getByRole('button', { name: '添加提供方', exact: true }).click() + await dialog.getByLabel('提供方', { exact: true }).selectOption('minimax-cn') + await dialog.getByRole('button', { name: '保存', exact: true }).click() + await dialog.getByText('已保存 minimax-cn。', { exact: true }).waitFor() + expect(await readFile(join(home, 'settings.yaml'), 'utf8')).toContain('minimax-cn: {}') + expect(await dialog.getByText(FAILURE, { exact: true }).count()).toBe(1) + }) + + it('rejects an invalid edit without persisting and accepts removal of the obsolete model', async () => { + const dialog = page.getByRole('dialog', { name: '设置' }) + await dialog.getByRole('button', { name: '编辑 openrouter', exact: true }).click() + await dialog.getByText('自定义设置', { exact: true }).click() + await dialog.getByLabel('API 地址', { exact: true }).fill('https://gateway.example/v1') + const before = await readFile(join(home, 'settings.yaml'), 'utf8') + await dialog.getByRole('button', { name: '保存', exact: true }).click() + await expect.poll(() => dialog.getByText(FAILURE, { exact: true }).count()).toBe(2) + expect(await readFile(join(home, 'settings.yaml'), 'utf8')).toBe(before) + await dialog.getByRole('button', { name: '删除模型 1', exact: true }).click() + await dialog.getByRole('button', { name: '保存', exact: true }).click() + await dialog.getByText('已保存 openrouter。', { exact: true }).waitFor() + expect(await dialog.getByText(FAILURE, { exact: true }).count()).toBe(0) + const repaired = await readFile(join(home, 'settings.yaml'), 'utf8') + expect(repaired).not.toContain('111') + expect(repaired).toContain('baseURL: https://gateway.example/v1') + expect(tripwire.pageErrors).toEqual([]) + }) +}) diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 17c8670403..f25c4d6d1d 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -42,6 +42,7 @@ "tests/plugin-config.e2e.ts", "tests/settings-chrome.e2e.ts", "tests/models-settings.e2e.ts", + "tests/models-settings-recovery.e2e.ts", "tests/default-model.e2e.ts", "tests/github-ready-review.e2e.ts", "tests/streaming-fence-highlight.e2e.ts", diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 7aafa5de3d..20a8b59b82 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-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/config-catalog.md -config-catalog.md: 90ec60c30c5ef2ef5fcf75e18ab3d2b51f9cac60 -config-catalog.zh.md: e5c3134561393afe3267808ee04a4cf46399a0f9 +config-catalog.md: 267e9866b12af7b4af71bda87a7d4603009aa3b4 +config-catalog.zh.md: d4676ae6adde5a735e9323dc0282f9c9441821fd diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 90ec60c30c..267e9866b1 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -1335,7 +1335,7 @@ export type PiAiThinkingTokenBudgetField = NonNullable diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index e5c3134561..d4676ae6ad 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -1337,7 +1337,7 @@ export type PiAiThinkingTokenBudgetField = NonNullable diff --git a/docs/subsystems/llm-streaming.i18n.yaml b/docs/subsystems/llm-streaming.i18n.yaml index 9b4bcd724f..007bd857a6 100644 --- a/docs/subsystems/llm-streaming.i18n.yaml +++ b/docs/subsystems/llm-streaming.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/llm-streaming.md -llm-streaming.md: 97062fb326a2718daf33b19b2f7f00175a2ec1fa -llm-streaming.zh.md: 9f4dc7d32bee62f55e971afb44905141cabe4e80 +llm-streaming.md: 8544517517cb7c1da4ee4afaaa881eebab8063c4 +llm-streaming.zh.md: 0036939d0a8703d38bca77918cba62861e589bee diff --git a/docs/subsystems/llm-streaming.md b/docs/subsystems/llm-streaming.md index 97062fb326..8544517517 100644 --- a/docs/subsystems/llm-streaming.md +++ b/docs/subsystems/llm-streaming.md @@ -459,6 +459,8 @@ interface LlmProviderInfo { id: string /** Human-readable provider name for selectors and diagnostics. */ name: string + /** Configuration diagnostic; the route stays registered for inspection and repair. */ + configurationError?: string } ``` diff --git a/docs/subsystems/llm-streaming.zh.md b/docs/subsystems/llm-streaming.zh.md index 9f4dc7d32b..0036939d0a 100644 --- a/docs/subsystems/llm-streaming.zh.md +++ b/docs/subsystems/llm-streaming.zh.md @@ -465,6 +465,8 @@ interface LlmProviderInfo { id: string /** Human-readable provider name for selectors and diagnostics. */ name: string + /** Configuration diagnostic; the route stays registered for inspection and repair. */ + configurationError?: string } ``` diff --git a/docs/subsystems/settings.i18n.yaml b/docs/subsystems/settings.i18n.yaml index 94a8cc437f..464c961996 100644 --- a/docs/subsystems/settings.i18n.yaml +++ b/docs/subsystems/settings.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/settings.md -settings.md: d8e3cbc46eb697315d4938b696e921a0c2e11828 -settings.zh.md: 772fd7832ff80cab9444c16c9a1dc7ae1875d51e +settings.md: 7d34bfa34471bc01475e147c5b3bbde74d0284ec +settings.zh.md: a3562187545151bed4a4b582a94f6a92d0bc95e1 diff --git a/docs/subsystems/settings.md b/docs/subsystems/settings.md index d8e3cbc46e..7d34bfa344 100644 --- a/docs/subsystems/settings.md +++ b/docs/subsystems/settings.md @@ -46,6 +46,14 @@ interface SettingsRegisterOptions { * @param value - the resolved section, schema-valid by construction. */ validate?: (value: T) => void + /** + * Check an in-process write after resolution and before persistence. Stored + * values do not run this check on registration or external reload, allowing + * the owner to expose repairable configuration after dependencies change. + * @param value - next resolved section. + * @param previous - current resolved section, including the composition base. + */ + validateWrite?: (value: T, previous: T) => void } ``` diff --git a/docs/subsystems/settings.zh.md b/docs/subsystems/settings.zh.md index 772fd7832f..a356218754 100644 --- a/docs/subsystems/settings.zh.md +++ b/docs/subsystems/settings.zh.md @@ -46,6 +46,14 @@ interface SettingsRegisterOptions { * @param value - the resolved section, schema-valid by construction. */ validate?: (value: T) => void + /** + * Check an in-process write after resolution and before persistence. Stored + * values do not run this check on registration or external reload, allowing + * the owner to expose repairable configuration after dependencies change. + * @param value - next resolved section. + * @param previous - current resolved section, including the composition base. + */ + validateWrite?: (value: T, previous: T) => void } ``` diff --git a/packages/client/ui-settings-models/README.i18n.yaml b/packages/client/ui-settings-models/README.i18n.yaml index b60f05babf..102eeb21d6 100644 --- a/packages/client/ui-settings-models/README.i18n.yaml +++ b/packages/client/ui-settings-models/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-settings-models/README.md -README.md: 64a295e9ebacdd8cbb2b5a0a10b14c1c1dfb24ee -README.zh.md: a75b6f2300eb59e2961b5ca85799b28b32d0856a +README.md: 8c852023f6c7c90a4aad67beef8c4a3081e5968e +README.zh.md: 6b5fd016dbc17bced816a16192c80640f56b9ab2 diff --git a/packages/client/ui-settings-models/README.md b/packages/client/ui-settings-models/README.md index 64a295e9eb..8c852023f6 100644 --- a/packages/client/ui-settings-models/README.md +++ b/packages/client/ui-settings-models/README.md @@ -27,6 +27,8 @@ English | [中文](README.zh.md) Open the Models page from the Settings navigation to see every configured provider as a row. A whole-section provider whose key is not configured anywhere renders as its open setup card instead, but only in the first-run posture and only until the user closes that card. Each card kind owns its own open state, so closing one never discards a draft in another. +A provider with a stored catalog error remains visible with its diagnostic and edit/delete actions. Add actions are offered only for registered settings namespaces, so an unavailable namespace cannot leave a button that opens no editor. A rejected save leaves the editor open and displays the Host diagnostic. + ### API keys The primary field on an editor card is a single **API key** input — the page never asks for an environment-variable name. A typed key stores write-only through `credentials.set` under the profile's reference, deriving `_API_KEY` when the profile has none, and the pi-ai profile records that derivation as `apiKeyEnv`, so `settings.yaml` never carries a key value. Leaving a new pi-ai provider's key blank saves a reference-free profile and preserves provider-native authentication (for example the Bedrock credential chain or Vertex ADC). A row labels API-key state with a green solid dot only when a referenced credential is confirmed configured, and with a red solid dot only when a named reference is confirmed missing. A successful Apply emits a local accessible status message without echoing secret material. diff --git a/packages/client/ui-settings-models/README.zh.md b/packages/client/ui-settings-models/README.zh.md index a75b6f2300..6b5fd016db 100644 --- a/packages/client/ui-settings-models/README.zh.md +++ b/packages/client/ui-settings-models/README.zh.md @@ -27,6 +27,8 @@ kind: "package-reference" 从设置导航打开 Models 页面,即可看到每个已配置的提供方都有一行。其配置键未在任何位置配置的整分节提供方会渲染为其展开的设置卡片而非一行,但仅限首次运行姿态,且仅持续到用户关闭该卡片为止。每一类卡片各自持有自己的展开状态,因此关掉其中一张绝不会丢弃另一张里的草稿。 +存在已存储目录错误的提供方仍显示诊断以及编辑、删除入口。添加操作只面向已注册的 settings 命名空间,因此不可用的命名空间不会留下无法打开编辑器的按钮。保存被拒绝时,编辑器保持打开并展示 Host 诊断。 + ### API 密钥 编辑卡片上的主字段是单独一个 **API 密钥**输入框——页面从不询问环境变量名。键入的密钥经 `credentials.set` 以**只写**方式存入 profile 的引用之下,profile 没有引用时便派生 `_API_KEY`,pi-ai profile 会把这次派生记录为 `apiKeyEnv`,因此 `settings.yaml` 从不携带密钥值。为新的 pi-ai 提供方留空密钥会保存一个不带引用的 profile,从而保留提供方原生认证(例如 Bedrock 凭据链或 Vertex ADC)。只有确认引用的凭据已配置时,行才会以绿色实心点标示 API 密钥状态;只有确认具名引用缺失时,才会以红色实心点标示。「应用」成功后会发出本地无障碍状态消息,且绝不回显任何机密内容。 diff --git a/packages/client/ui-settings-models/src/client/ModelsSection.tsx b/packages/client/ui-settings-models/src/client/ModelsSection.tsx index b99767a364..9f7e25d5ca 100644 --- a/packages/client/ui-settings-models/src/client/ModelsSection.tsx +++ b/packages/client/ui-settings-models/src/client/ModelsSection.tsx @@ -290,7 +290,8 @@ function Loaded({ injected, renderSlot }: { injected: ModelsSectionFace; renderS // step: whether the user already has a provider to talk to. const anyUsable = state.rows.some(providerUsable) const configured = state.rows.filter(row => row.configured) - const addable = state.rows.filter(row => !row.configured && row.entry.settingsNs !== '') + const configurable = state.rows.filter(row => state.namespaces.has(row.entry.settingsNs)) + const addable = configurable.filter(row => !row.configured) const addTarget = adding ? editing : undefined const addNamespace = addTarget === undefined ? undefined : state.namespaces.get(addTarget.settingsNs) // The draft's directory row, for the card extension seat. A refresh can drop @@ -322,11 +323,15 @@ function Loaded({ injected, renderSlot }: { injected: ModelsSectionFace; renderS const namespace = state.namespaces.get(target.settingsNs) /* v8 ignore next -- the join marks a row configured only when its namespace resolved */ if (namespace === undefined) return null + const configurationError = row.entry.configurationError === undefined + ? null + :

{row.entry.configurationError}

if (needsSetup(row, anyUsable) && !dismissedSetup.has(row.entry.provider)) { // First-run posture: the provider exists but has no key — the // setup card IS its presence on the page, until the user closes it. return (
  • + {configurationError} {renderProviderEditor({ target, namespace, @@ -416,6 +421,7 @@ function Loaded({ injected, renderSlot }: { injected: ModelsSectionFace; renderS : null} + {configurationError} {renderSlot( 'settings.models.provider-card', { provider: row.entry, configured: row.configured, keyConfigured: keyConfiguredOf(row) }, @@ -504,37 +510,41 @@ function Loaded({ injected, renderSlot }: { injected: ModelsSectionFace; renderS // and equal-width so they read as siblings and line up with the // rows above, rather than two pills of different lengths.
    - - + {configurable.length > 0 && ( + + )} + {state.namespaces.has('llm-pi-ai') && ( + + )}
    )} diff --git a/packages/client/ui-settings-models/src/client/store.ts b/packages/client/ui-settings-models/src/client/store.ts index fd8eeaf9f9..305c583e8d 100644 --- a/packages/client/ui-settings-models/src/client/store.ts +++ b/packages/client/ui-settings-models/src/client/store.ts @@ -30,6 +30,7 @@ export interface ProviderDirectoryEntry { readonly settingsPath: readonly string[] readonly active: boolean readonly declared?: boolean + readonly configurationError?: string } /** @@ -43,15 +44,20 @@ export function joinProviderDirectory( directory: readonly LlmConfigurableProvider[], ): ProviderDirectoryEntry[] { const active = new Set(registered.map(provider => provider.id)) + const errors = new Map(registered.map(provider => [provider.id, provider.configurationError])) const declared = new Set(directory.map(entry => entry.provider)) - const rows: ProviderDirectoryEntry[] = directory.map(entry => ({ - provider: entry.provider, - displayName: entry.displayName, - settingsNs: entry.settingsNs, - settingsPath: [...entry.settingsPath], - active: active.has(entry.provider), - ...entry.declared === undefined ? {} : { declared: entry.declared }, - })) + const rows: ProviderDirectoryEntry[] = directory.map((entry) => { + const configurationError = errors.get(entry.provider) + return { + provider: entry.provider, + displayName: entry.displayName, + settingsNs: entry.settingsNs, + settingsPath: [...entry.settingsPath], + active: active.has(entry.provider), + ...entry.declared === undefined ? {} : { declared: entry.declared }, + ...configurationError === undefined ? {} : { configurationError }, + } + }) for (const provider of registered) { if (declared.has(provider.id)) continue rows.push({ @@ -60,6 +66,7 @@ export function joinProviderDirectory( settingsNs: '', settingsPath: [], active: true, + ...provider.configurationError === undefined ? {} : { configurationError: provider.configurationError }, }) } return rows diff --git a/packages/client/ui-settings-models/tests/components.client.spec.tsx b/packages/client/ui-settings-models/tests/components.client.spec.tsx index 6d100d800e..37cc0d832e 100644 --- a/packages/client/ui-settings-models/tests/components.client.spec.tsx +++ b/packages/client/ui-settings-models/tests/components.client.spec.tsx @@ -307,6 +307,41 @@ async function mountDeepSeekCard(overrides: Parameters[0] = } describe('ModelsSection', () => { + it('hides both add actions when their settings namespaces are absent', async () => { + const scripted = scriptedFace() + scripted.face.settings.describe.mockResolvedValue(remoteOk({ writable: true, hasDocument: false, namespaces: [] })) + await mountFace(scripted) + expect(screen.queryByRole('button', { name: en.add })).toBeNull() + expect(screen.queryByRole('button', { name: en.customAdd })).toBeNull() + }) + + it('offers only providers whose settings namespace can open an editor', async () => { + const scripted = scriptedFace() + scripted.face.settings.describe.mockResolvedValue(remoteOk({ + writable: true, hasDocument: false, + namespaces: wireNamespaces().filter(view => view.ns !== 'llm-pi-ai'), + })) + await mountFace(scripted) + expect(screen.queryByRole('button', { name: en.customAdd })).toBeNull() + fireEvent.click(screen.getByRole('button', { name: en.add })) + expect(screen.queryByRole('option', { name: 'anthropic' })).toBeNull() + expect(screen.getByRole('option', { name: 'plain' })).toBeTruthy() + }) + + it('shows a catalog diagnostic while keeping the provider editable', async () => { + const scripted = scriptedFace() + const failure = 'llm-pi-ai: provider "openai" model "111" needs an api' + scripted.face.llm.listProviders.mockResolvedValue(remoteOk([ + { id: 'openai', name: 'openai', configurationError: failure }, + ])) + await mountFace(scripted) + expect(screen.getByRole('alert').textContent).toBe(failure) + fireEvent.click(screen.getByRole('button', { name: openaiCopy(en.editProvider) })) + expect(await screen.findByLabelText(en.keyInput)).toBeTruthy() + expect(screen.getByRole('button', { name: en.add })).toBeTruthy() + expect(screen.getByRole('button', { name: en.customAdd })).toBeTruthy() + }) + it('renders nothing before the slot injects its dependencies', () => { const uninjected = {} as ModelsSectionProps render() diff --git a/packages/client/ui-settings-models/tests/store.client.spec.ts b/packages/client/ui-settings-models/tests/store.client.spec.ts index 7757cf3792..c963017311 100644 --- a/packages/client/ui-settings-models/tests/store.client.spec.ts +++ b/packages/client/ui-settings-models/tests/store.client.spec.ts @@ -4,7 +4,15 @@ import type { RpcResponse } from '@deepseek-ai/dsh-api-remotes/client' import { RemoteError } from '@deepseek-ai/dsh-client-test-runtime' import { SettingsDescribeMirror } from '@deepseek-ai/dsh-client-ui-settings/src/client/settings-mirror.ts' import { settingsSchema } from './settings-schema.client.ts' -import { ModelsSettingsStore } from '../src/client/store.ts' +import { joinProviderDirectory, ModelsSettingsStore } from '../src/client/store.ts' + +it('retains diagnostics for a live provider without a configurable directory entry', () => { + expect(joinProviderDirectory([{ id: 'orphan', name: 'Orphan', configurationError: 'catalog unavailable' }], [])) + .toEqual([{ + provider: 'orphan', displayName: 'Orphan', settingsNs: '', settingsPath: [], active: true, + configurationError: 'catalog unavailable', + }]) +}) let nextRpc = 0 function ok(value: T): RpcResponse { diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index a1c333b265..e054143149 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -4452,7 +4452,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'LlmProviderInfo', - declaration: 'export interface LlmProviderInfo {\n id: string;\n name: string;\n}', + declaration: 'export interface LlmProviderInfo {\n id: string;\n name: string;\n configurationError?: string;\n}', }, { name: 'LlmReasoningEffortInfo', @@ -5452,7 +5452,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SettingsRegisterOptions', - declaration: 'export interface SettingsRegisterOptions {\n base?: Partial;\n applies?: SettingsApplies;\n validate?: (value: T) => void;\n}', + declaration: 'export interface SettingsRegisterOptions {\n base?: Partial;\n applies?: SettingsApplies;\n validate?: (value: T) => void;\n validateWrite?: (value: T, previous: T) => void;\n}', }, { name: 'SettingsSecretView', @@ -5460,7 +5460,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SettingsSectionHooks', - declaration: 'export interface SettingsSectionHooks {\n setSource(current: () => T): void;\n onChange(): void;\n validate?: (value: T) => void;\n}', + declaration: 'export interface SettingsSectionHooks {\n setSource(current: () => T): void;\n onChange(): void;\n validate?: (value: T) => void;\n validateWrite?: (value: T, previous: T) => void;\n}', }, { name: 'SettingsUpdateSource', diff --git a/packages/llm/llm-pi-ai/README.i18n.yaml b/packages/llm/llm-pi-ai/README.i18n.yaml index 02ed82def7..6c971a7aa5 100644 --- a/packages/llm/llm-pi-ai/README.i18n.yaml +++ b/packages/llm/llm-pi-ai/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/llm/llm-pi-ai/README.md -README.md: e9a59a830e694341174969670f59787a6fd34854 -README.zh.md: 67c14746d8bb7fa36cd3b45b45d79b0e81d9050e +README.md: e9013b634e8a8b392f88e5e8986ace4a8fefac9b +README.zh.md: 9c91e937ab31793085fa3627b88460817917e58a diff --git a/packages/llm/llm-pi-ai/README.md b/packages/llm/llm-pi-ai/README.md index e9a59a830e..e9013b634e 100644 --- a/packages/llm/llm-pi-ai/README.md +++ b/packages/llm/llm-pi-ai/README.md @@ -114,6 +114,8 @@ The plugin answers "which models can this provider serve?" for a route a configu A route pi-ai does not ship needs `api`, `baseURL`, and a non-empty `models` list; an unserviceable profile is refused where it is written, naming the route and model. Failures carry stable codes: a credential that cannot be used fails with `INVALID_CREDENTIAL` naming the route and reference, a route whose `apiKeyEnv` reference resolves to nothing fails with `MISSING_CREDENTIAL`, an unconfigured model fails with `UNKNOWN_MODEL`, and terminal provider failures distinguish `QUOTA` from transient `RATE_LIMIT`. `GenerateOptions.stop` is rejected with `UNSUPPORTED_OPTION` because pi-ai's common streaming UI cannot guarantee it across providers. +Settings writes strictly validate each new or changed provider after merging its composition and user layers. Stored catalog failures retain the namespace and provider rows, with the first diagnostic in `LlmProviderInfo.configurationError`; unchanged failed providers do not block edits elsewhere. Serviceable models remain selectable, while unresolved models remain in the editable configuration and fail with `INVALID_CONFIG` before network I/O if requested directly. Repairing or deleting the offending configuration clears its diagnostic. Schema and self-contained profile errors still reject loading. + ----- diff --git a/packages/llm/llm-pi-ai/README.zh.md b/packages/llm/llm-pi-ai/README.zh.md index 67c14746d8..9c91e937ab 100644 --- a/packages/llm/llm-pi-ai/README.zh.md +++ b/packages/llm/llm-pi-ai/README.zh.md @@ -114,6 +114,8 @@ profile 通过可选 settings seam 每次操作重新读取:base 与用户的 pi-ai 不提供的路由需要 `api`、`baseURL` 与非空 `models` 列表;无法服务的 profile 会在写入处被拒绝,并点名路由与模型。失败携带稳定 code:无法使用的凭据以 `INVALID_CREDENTIAL` 失败并点名路由与引用,`apiKeyEnv` 引用解析为空的路由以 `MISSING_CREDENTIAL` 失败,未配置模型以 `UNKNOWN_MODEL` 失败,终止性提供方失败则区分 `QUOTA` 与暂时性 `RATE_LIMIT`。`GenerateOptions.stop` 以 `UNSUPPORTED_OPTION` 被拒绝,因为 pi-ai 的通用流式 UI 无法跨提供方保证它。 +Settings 写入会在合并组合层与用户层后严格校验每个新增或修改的提供方。已存储配置的目录解析错误会保留命名空间与提供方行,并通过 `LlmProviderInfo.configurationError` 返回首个诊断;未修改的错误提供方不会阻止其他编辑。可解析的模型仍可选择,无法解析的模型保留在可编辑配置中,直接请求时会在网络 I/O 前以 `INVALID_CONFIG` 失败。修复或删除错误配置会清除诊断。Schema 与 profile 自身的约束错误仍会拒绝加载。 + ----- diff --git a/packages/llm/llm-pi-ai/src/adapter.ts b/packages/llm/llm-pi-ai/src/adapter.ts index 41a2171752..337761d960 100644 --- a/packages/llm/llm-pi-ai/src/adapter.ts +++ b/packages/llm/llm-pi-ai/src/adapter.ts @@ -233,7 +233,9 @@ export class PiAiAdapter extends LlmAdapter { const profiles = this.config.profiles() if (this.snapshot?.profiles === profiles) return this.snapshot const models: MutableModels = createModels(this.config.auth) - for (const profile of profiles.values()) models.setProvider(profile.piProvider) + for (const profile of profiles.values()) { + if (profile.piProvider !== undefined) models.setProvider(profile.piProvider) + } this.snapshot = { profiles, models } return this.snapshot } @@ -249,7 +251,10 @@ export class PiAiAdapter extends LlmAdapter { /** The configured descriptor for one exact route/model pair within one snapshot. */ private modelOf(snapshot: PiAiSnapshot, provider: string, model: string): Model { - this.profileOf(snapshot, provider) + const profile = this.profileOf(snapshot, provider) + const failure = profile.modelErrors.get(model) + ?? (profile.piProvider === undefined ? profile.catalogError : undefined) + if (failure !== undefined) throw new LlmError(failure, 'INVALID_CONFIG') const resolved = snapshot.models.getModel(provider, model) if (resolved === undefined) { throw new LlmError(`pi-ai provider "${provider}" has no configured model "${model}"`, 'UNKNOWN_MODEL') @@ -261,7 +266,12 @@ export class PiAiAdapter extends LlmAdapter { // The configured name, not the route key: `displayName` exists so a // deployment can label a route, and a label only the configuration surface // reads would leave every selector showing the raw key. - return { id: provider, name: this.current().profiles.get(provider)?.displayName ?? provider } + const profile = this.current().profiles.get(provider) + return { + id: provider, + name: profile?.displayName ?? provider, + ...profile?.catalogError === undefined ? {} : { configurationError: profile.catalogError }, + } } override providerRetryPolicy(provider: string): ResolvedRetryPolicy | undefined { diff --git a/packages/llm/llm-pi-ai/src/catalog.ts b/packages/llm/llm-pi-ai/src/catalog.ts index af52b8a49c..d626c1ac71 100644 --- a/packages/llm/llm-pi-ai/src/catalog.ts +++ b/packages/llm/llm-pi-ai/src/catalog.ts @@ -5,9 +5,9 @@ * stays configuration-free while a route pi-ai has never heard of is fully * describable from `settings.yaml`. * - * Every pi-ai `Model` field the harness cannot default is required here rather - * than at request time: an unserviceable route fails while its configuration is - * being resolved, which is the earliest point that can name the offending key. + * Strict resolution rejects unserviceable models before settings writes. + * Deferred resolution retains their diagnostics so stored catalog drift does + * not prevent inspection, repair, or requests to independently valid models. * * @module dsh-llm-pi-ai/catalog */ @@ -640,9 +640,12 @@ export interface RouteCatalogRequest { defaultInput: Model['input'] } +/** An expected configuration failure that stored-catalog reads may retain for repair. */ +export class PiAiCatalogError extends Error {} + /** Report a route the deployment cannot serve, naming the settings key at fault. */ function invalid(provider: string, detail: string): never { - throw new Error(`llm-pi-ai: provider "${provider}" ${detail}`) + throw new PiAiCatalogError(`llm-pi-ai: provider "${provider}" ${detail}`) } /** @@ -798,6 +801,8 @@ function resolveModelCompat( export interface RouteCatalog { /** The materialized models in configuration order. */ models: readonly Model[] + /** Models that cannot be resolved, retained as diagnostics during stored-config reads. */ + modelErrors: ReadonlyMap /** * Per-request output caps this profile explicitly configured, by model id. * @@ -817,9 +822,13 @@ export interface RouteCatalog { * installed catalog unchanged, which is what keeps an existing * `providers: { deepseek: { apiKeyEnv: … } }` profile working untouched. * @param request - the route-level catalog facts. + * @param validation - strict writes reject every error; deferred reads retain model diagnostics. * @returns the materialized models and the explicitly configured request caps. */ -export function resolveRouteModels(request: RouteCatalogRequest): RouteCatalog { +export function resolveRouteModels( + request: RouteCatalogRequest, + validation: 'strict' | 'deferred' = 'strict', +): RouteCatalog { const { provider } = request const defaults = catalogModels(provider) const providerBaseUrl = catalogProvider(provider)?.baseUrl @@ -828,8 +837,9 @@ export function resolveRouteModels(request: RouteCatalogRequest): RouteCatalog { // serve no request anyway, so both mean "serve the installed catalog". const configured = request.models ?? [] const overrides = request.modelOverrides ?? {} - // Every miss is refused, never skipped: an override that lands nowhere is a - // typo someone would otherwise hunt for in a silently unchanged model. + const modelErrors = new Map() + // Writes reject missing referents. Stored overrides retain a diagnostic + // after catalog removal rather than silently disappearing. for (const [id, override] of Object.entries(overrides)) { if (id.length === 0) invalid(provider, 'has a modelOverrides entry with an empty model id') if (defaults.size === 0) { @@ -841,7 +851,9 @@ export function resolveRouteModels(request: RouteCatalogRequest): RouteCatalog { + ' catalog, so declare the fields on its entries') } if (!defaults.has(id)) { - invalid(provider, `modelOverrides names "${id}", which the installed catalog does not describe`) + const message = `modelOverrides names "${id}", which the installed catalog does not describe` + if (validation === 'strict') invalid(provider, message) + modelErrors.set(id, `llm-pi-ai: provider "${provider}" ${message}`) } // The id lives in the dict key; a value carrying its own would quietly // rename the model it meant to customize. The static shape already omits @@ -865,12 +877,10 @@ export function resolveRouteModels(request: RouteCatalogRequest): RouteCatalog { // wherever it is written, so it cannot look applied on a route whose models // never reach the protocol that would have taken it. assertOfferedCompatFields(provider, 'route', request.compat) - for (const entry of entries) { - assertOfferedCompatFields(provider, `model "${entry.id}"`, entry.compat) - } const seen = new Set() const configuredMaxTokens = new Map() - const models = entries.map((entry) => { + const resolveEntry = (entry: PiAiModelProfile): Model => { + assertOfferedCompatFields(provider, `model "${entry.id}"`, entry.compat) if (entry.id.length === 0) invalid(provider, 'has a model with an empty id') if (seen.has(entry.id)) invalid(provider, `lists model "${entry.id}" more than once`) seen.add(entry.id) @@ -918,16 +928,30 @@ export function resolveRouteModels(request: RouteCatalogRequest): RouteCatalog { ...resolveModelReasoning(provider, entry, base), ...resolveModelCompat(provider, entry, request.compat, base, api), } - }) + } + const models: Model[] = [] + for (const entry of entries) { + let model: Model + try { + model = resolveEntry(entry) + } catch (error) { + if (validation === 'strict' || !(error instanceof PiAiCatalogError)) throw error + modelErrors.set(entry.id, error.message) + continue + } + models.push(model) + } + // A later duplicate invalidates the id, including an earlier resolved entry. + const serviceableModels = models.filter(model => !modelErrors.has(model.id)) // Per field, not per block: a route may default a switch its completions // models take beside one only its anthropic models do, and neither should // fail for the other's sake. What is refused is a route default no model on // the route could ever read, which is a route that will not behave as written. for (const [field] of configuredCompatEntries(request.compat)) { const takers = compatProtocols(field) - if (models.some(model => takers.includes(model.api))) continue + if (serviceableModels.some(model => takers.includes(model.api))) continue invalid(provider, `sets compat "${field}", but no model on the route speaks a protocol that takes it;` + ` it exists on ${takers.join(', ')}`) } - return { models, configuredMaxTokens } + return { models: serviceableModels, configuredMaxTokens, modelErrors } } diff --git a/packages/llm/llm-pi-ai/src/config.ts b/packages/llm/llm-pi-ai/src/config.ts index b50c448af5..25675354e0 100644 --- a/packages/llm/llm-pi-ai/src/config.ts +++ b/packages/llm/llm-pi-ai/src/config.ts @@ -6,10 +6,9 @@ * A route key is not required to name an installed pi-ai provider. When it does, * that provider's endpoint, protocol, display name, and model catalog are the * profile's defaults and the profile overrides them field by field; when it does - * not, the profile is the whole provider declaration. Resolution therefore ends - * in a built pi-ai `Provider` per route: everything a request needs is decided - * once, while the configuration key that made a route unserviceable can still be - * named in the failure. + * not, the profile is the whole provider declaration. Stored reads retain + * catalog diagnostics beside serviceable models; writes validate every changed + * provider before persistence. Self-contained profile constraints apply to both. * * @module dsh-llm-pi-ai/config */ @@ -21,11 +20,13 @@ import type { CredentialRef } from '@deepseek-ai/dsh-credentials' import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout' import { resolveRetryPolicy, RetryPolicySchema } from '@deepseek-ai/dsh-llm' import type { ResolvedRetryPolicy, RetryPolicyConfig } from '@deepseek-ai/dsh-llm' +import { deepEqualJson } from '@deepseek-ai/dsh-util-values' import { CACHE_CONTROL_FORMATS, CHAT_TEMPLATE_VARS, MAX_TOKENS_FIELDS, MODALITIES, + PiAiCatalogError, resolveRouteModels, SUPPORTED_THINKING_FORMATS, THINKING_LEVELS, @@ -37,6 +38,7 @@ import type { PiAiModelOverride, PiAiModelProfile, PiAiReasoningEfforts, + RouteCatalog, } from './catalog.ts' import { buildProvider, supportedProtocols } from './provider.ts' @@ -199,12 +201,14 @@ export interface ResolvedPiAiProviderProfile /** Immutable retry policy captured with this provider route. */ retryPolicy: ResolvedRetryPolicy /** - * The pi-ai provider this route registers, built from the resolved models. - * Construction happens here so an unserviceable protocol or an underspecified - * model fails with the rest of resolution, leaving the last good route set - * serving requests. + * The pi-ai provider containing this route's serviceable models. Absent when + * a stored route cannot be constructed; its configuration remains editable. */ - piProvider: Provider + piProvider?: Provider + /** First stored-catalog diagnostic for settings surfaces; absent for a serviceable route. */ + catalogError?: string + /** Per-model failures reported before attempting a request. */ + modelErrors: ReadonlyMap /** * Per-request output caps this profile explicitly configured, by model id. * The seam materializes one only into a request that names no cap of its @@ -346,19 +350,25 @@ export const Config: z = z.object({ }) /** - * Reject a section this adapter could not serve. Registered as the settings - * namespace's validator, so an unserviceable profile is refused where it is - * *written* — `settings.mutate` answers `settings-rejected` with the offending - * route and model named — instead of being stored and then quietly disabling - * every route in the namespace. It stays a validator rather than a schema - * transform because the schema is also the shape a configuration surface - * renders and the value an absent section resolves to; wrapping it would break - * both. + * Reject new or changed provider profiles that cannot be served. Unchanged + * stored profiles may need repair after a catalog upgrade and do not block + * edits to another provider. Removed profiles require no catalog validation. * @param config - the resolved section to check. + * @param previous - current resolved section; omission checks every provider. * @throws Error naming the route and configuration entry that cannot be served. */ -export function assertServiceable(config: Config): void { - resolveProfiles(config.providers) +export function assertServiceable(config: Config, previous?: Config): void { + const changed = Object.fromEntries(Object.entries(config.providers ?? {}).filter(([provider, profile]) => + !deepEqualJson(profile, previous?.providers?.[provider]))) + resolveProfiles(changed) +} + +/** + * Check self-contained profile requirements without rejecting stored catalog drift. + * @param config - schema-resolved section read from settings. + */ +export function assertReadable(config: Config): void { + resolveProfiles(config.providers, 'deferred') } /** Reject removed pre-release profile fields and name their replacements. */ @@ -394,15 +404,16 @@ function assertValidHeaders(provider: string, headers: Readonly
  • - {configurationError} + {error} {renderProviderEditor({ target, namespace, @@ -421,7 +421,7 @@ function Loaded({ injected, renderSlot }: { injected: ModelsSectionFace; renderS : null} - {configurationError} + {error} {renderSlot( 'settings.models.provider-card', { provider: row.entry, configured: row.configured, keyConfigured: keyConfiguredOf(row) }, diff --git a/packages/client/ui-settings-models/src/client/store.ts b/packages/client/ui-settings-models/src/client/store.ts index 305c583e8d..2af1d54d5c 100644 --- a/packages/client/ui-settings-models/src/client/store.ts +++ b/packages/client/ui-settings-models/src/client/store.ts @@ -30,7 +30,7 @@ export interface ProviderDirectoryEntry { readonly settingsPath: readonly string[] readonly active: boolean readonly declared?: boolean - readonly configurationError?: string + readonly error?: string } /** @@ -44,20 +44,16 @@ export function joinProviderDirectory( directory: readonly LlmConfigurableProvider[], ): ProviderDirectoryEntry[] { const active = new Set(registered.map(provider => provider.id)) - const errors = new Map(registered.map(provider => [provider.id, provider.configurationError])) const declared = new Set(directory.map(entry => entry.provider)) - const rows: ProviderDirectoryEntry[] = directory.map((entry) => { - const configurationError = errors.get(entry.provider) - return { - provider: entry.provider, - displayName: entry.displayName, - settingsNs: entry.settingsNs, - settingsPath: [...entry.settingsPath], - active: active.has(entry.provider), - ...entry.declared === undefined ? {} : { declared: entry.declared }, - ...configurationError === undefined ? {} : { configurationError }, - } - }) + const rows: ProviderDirectoryEntry[] = directory.map(entry => ({ + provider: entry.provider, + displayName: entry.displayName, + settingsNs: entry.settingsNs, + settingsPath: [...entry.settingsPath], + active: active.has(entry.provider), + ...entry.declared === undefined ? {} : { declared: entry.declared }, + ...entry.error === undefined ? {} : { error: entry.error }, + })) for (const provider of registered) { if (declared.has(provider.id)) continue rows.push({ @@ -66,7 +62,6 @@ export function joinProviderDirectory( settingsNs: '', settingsPath: [], active: true, - ...provider.configurationError === undefined ? {} : { configurationError: provider.configurationError }, }) } return rows diff --git a/packages/client/ui-settings-models/tests/components.client.spec.tsx b/packages/client/ui-settings-models/tests/components.client.spec.tsx index 37cc0d832e..e8823c5eea 100644 --- a/packages/client/ui-settings-models/tests/components.client.spec.tsx +++ b/packages/client/ui-settings-models/tests/components.client.spec.tsx @@ -331,8 +331,8 @@ describe('ModelsSection', () => { it('shows a catalog diagnostic while keeping the provider editable', async () => { const scripted = scriptedFace() const failure = 'llm-pi-ai: provider "openai" model "111" needs an api' - scripted.face.llm.listProviders.mockResolvedValue(remoteOk([ - { id: 'openai', name: 'openai', configurationError: failure }, + scripted.face.llm.listConfigurableProviders.mockResolvedValue(remoteOk([ + { provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'], error: failure }, ])) await mountFace(scripted) expect(screen.getByRole('alert').textContent).toBe(failure) diff --git a/packages/client/ui-settings-models/tests/store.client.spec.ts b/packages/client/ui-settings-models/tests/store.client.spec.ts index c963017311..0ce5a995a1 100644 --- a/packages/client/ui-settings-models/tests/store.client.spec.ts +++ b/packages/client/ui-settings-models/tests/store.client.spec.ts @@ -6,12 +6,14 @@ import { SettingsDescribeMirror } from '@deepseek-ai/dsh-client-ui-settings/src/ import { settingsSchema } from './settings-schema.client.ts' import { joinProviderDirectory, ModelsSettingsStore } from '../src/client/store.ts' -it('retains diagnostics for a live provider without a configurable directory entry', () => { - expect(joinProviderDirectory([{ id: 'orphan', name: 'Orphan', configurationError: 'catalog unavailable' }], [])) - .toEqual([{ - provider: 'orphan', displayName: 'Orphan', settingsNs: '', settingsPath: [], active: true, - configurationError: 'catalog unavailable', - }]) +it.each([false, true])('retains configuration diagnostics when the route is active: %s', (active) => { + expect(joinProviderDirectory(active ? [{ id: 'openai', name: 'openai' }] : [], [{ + provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'], + error: 'catalog unavailable', + }])).toEqual([{ + provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'], + active, error: 'catalog unavailable', + }]) }) let nextRpc = 0 diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index a2256daf9f..1620d41648 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -4465,7 +4465,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'LlmConfigurableProvider', - declaration: 'export interface LlmConfigurableProvider {\n provider: string;\n displayName: string;\n settingsNs: string;\n settingsPath: readonly string[];\n declared?: boolean;\n}', + declaration: 'export interface LlmConfigurableProvider {\n provider: string;\n displayName: string;\n settingsNs: string;\n settingsPath: readonly string[];\n declared?: boolean;\n error?: string;\n}', }, { name: 'LlmDiscoveredModel', @@ -4501,7 +4501,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'LlmProviderInfo', - declaration: 'export interface LlmProviderInfo {\n id: string;\n name: string;\n configurationError?: string;\n}', + declaration: 'export interface LlmProviderInfo {\n id: string;\n name: string;\n}', }, { name: 'LlmReasoningEffortInfo', diff --git a/packages/llm/llm-pi-ai/README.i18n.yaml b/packages/llm/llm-pi-ai/README.i18n.yaml index d5c66a842f..51f8132299 100644 --- a/packages/llm/llm-pi-ai/README.i18n.yaml +++ b/packages/llm/llm-pi-ai/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/llm/llm-pi-ai/README.md -README.md: 9ec589315c40ef1a4de90b860f13454da783a2a9 -README.zh.md: 22bde6757fba0dabf60d66751283dd261f5854da +README.md: 799c99386443922bd8d4e24d8f84a826d18ff67e +README.zh.md: 8aecbc840725e02e565f2b228444d03565d931e4 diff --git a/packages/llm/llm-pi-ai/README.md b/packages/llm/llm-pi-ai/README.md index 9ec589315c..799c993864 100644 --- a/packages/llm/llm-pi-ai/README.md +++ b/packages/llm/llm-pi-ai/README.md @@ -114,7 +114,7 @@ The plugin answers "which models can this provider serve?" for a route a configu A route pi-ai does not ship needs `api`, `baseURL`, and a non-empty `models` list; an unserviceable profile is refused where it is written, naming the route and model. Failures carry stable codes: a credential that cannot be used fails with `INVALID_CREDENTIAL` naming the route and reference, a route whose `apiKeyEnv` reference resolves to nothing fails with `MISSING_CREDENTIAL`, an unconfigured model fails with `UNKNOWN_MODEL`, and terminal provider failures distinguish `QUOTA` from transient `RATE_LIMIT`. `GenerateOptions.stop` is rejected with `UNSUPPORTED_OPTION` because pi-ai's common streaming UI cannot guarantee it across providers. -Settings writes strictly validate each new or changed provider after merging its composition and user layers. During namespace registration, stored catalog failures retain the namespace and provider rows, with the first diagnostic in `LlmProviderInfo.configurationError`; unchanged failed providers do not block edits elsewhere. Serviceable models remain selectable, while unresolved models remain in the editable configuration and fail with `INVALID_CONFIG` before network I/O if requested directly. Repairing or deleting the offending configuration clears its diagnostic. Schema and self-contained profile errors still reject loading. Later external edits validate changed providers and retain the last accepted section on failure. +Settings writes strictly validate each new or changed provider after merging its composition and user layers. During namespace registration, stored catalog failures retain the namespace and provider rows, with the first diagnostic in `LlmConfigurableProvider.error`; unchanged failed providers do not block edits elsewhere. Serviceable models remain selectable, while unresolved models remain in the editable configuration and fail with `INVALID_CONFIG` before network I/O if requested directly. Repairing or deleting the offending configuration clears its diagnostic. Schema and self-contained profile errors still reject loading. Later external edits validate changed providers and retain the last accepted section on failure. ----- diff --git a/packages/llm/llm-pi-ai/README.zh.md b/packages/llm/llm-pi-ai/README.zh.md index 22bde6757f..8aecbc8407 100644 --- a/packages/llm/llm-pi-ai/README.zh.md +++ b/packages/llm/llm-pi-ai/README.zh.md @@ -114,7 +114,7 @@ profile 通过可选 settings seam 每次操作重新读取:base 与用户的 pi-ai 不提供的路由需要 `api`、`baseURL` 与非空 `models` 列表;无法服务的 profile 会在写入处被拒绝,并点名路由与模型。失败携带稳定 code:无法使用的凭据以 `INVALID_CREDENTIAL` 失败并点名路由与引用,`apiKeyEnv` 引用解析为空的路由以 `MISSING_CREDENTIAL` 失败,未配置模型以 `UNKNOWN_MODEL` 失败,终止性提供方失败则区分 `QUOTA` 与暂时性 `RATE_LIMIT`。`GenerateOptions.stop` 以 `UNSUPPORTED_OPTION` 被拒绝,因为 pi-ai 的通用流式 UI 无法跨提供方保证它。 -Settings 写入会在合并组合层与用户层后严格校验每个新增或修改的提供方。命名空间注册时,已存储配置的目录解析错误会保留命名空间与提供方行,并通过 `LlmProviderInfo.configurationError` 返回首个诊断;未修改的错误提供方不会阻止其他编辑。可解析的模型仍可选择,无法解析的模型保留在可编辑配置中,直接请求时会在网络 I/O 前以 `INVALID_CONFIG` 失败。修复或删除错误配置会清除诊断。Schema 与 profile 自身的约束错误仍会拒绝加载。后续外部文件编辑会校验变化的提供方,失败时保留最后一次接受的分节。 +Settings 写入会在合并组合层与用户层后严格校验每个新增或修改的提供方。命名空间注册时,已存储配置的目录解析错误会保留命名空间与提供方行,并通过 `LlmConfigurableProvider.error` 返回首个诊断;未修改的错误提供方不会阻止其他编辑。可解析的模型仍可选择,无法解析的模型保留在可编辑配置中,直接请求时会在网络 I/O 前以 `INVALID_CONFIG` 失败。修复或删除错误配置会清除诊断。Schema 与 profile 自身的约束错误仍会拒绝加载。后续外部文件编辑会校验变化的提供方,失败时保留最后一次接受的分节。 ----- diff --git a/packages/llm/llm-pi-ai/src/adapter.ts b/packages/llm/llm-pi-ai/src/adapter.ts index 337761d960..8dba978c31 100644 --- a/packages/llm/llm-pi-ai/src/adapter.ts +++ b/packages/llm/llm-pi-ai/src/adapter.ts @@ -266,12 +266,7 @@ export class PiAiAdapter extends LlmAdapter { // The configured name, not the route key: `displayName` exists so a // deployment can label a route, and a label only the configuration surface // reads would leave every selector showing the raw key. - const profile = this.current().profiles.get(provider) - return { - id: provider, - name: profile?.displayName ?? provider, - ...profile?.catalogError === undefined ? {} : { configurationError: profile.catalogError }, - } + return { id: provider, name: this.current().profiles.get(provider)?.displayName ?? provider } } override providerRetryPolicy(provider: string): ResolvedRetryPolicy | undefined { diff --git a/packages/llm/llm-pi-ai/src/index.ts b/packages/llm/llm-pi-ai/src/index.ts index 4e23112c78..dfa2a8159e 100644 --- a/packages/llm/llm-pi-ai/src/index.ts +++ b/packages/llm/llm-pi-ai/src/index.ts @@ -106,7 +106,6 @@ function registrationFacts(profiles: ReadonlyMap left.provider.localeCompare(right.provider)) } @@ -124,7 +123,7 @@ function directoryEntries( ): LlmConfigurableProvider[] { const catalog = new Set(catalogProviderIds()) const entries = new Map() - const declare = (provider: string, displayName: string): void => { + const declare = (provider: string, displayName: string, error?: string): void => { entries.set(provider, { provider, displayName, @@ -134,10 +133,11 @@ function directoryEntries( // narrowing a shipped provider's models stores a profile too, and that // route is still one pi-ai knows. declared: !catalog.has(provider), + ...error === undefined ? {} : { error }, }) } for (const provider of catalog) declare(provider, provider) - for (const [provider, profile] of profiles) declare(provider, profile.displayName) + for (const [provider, profile] of profiles) declare(provider, profile.displayName, profile.catalogError) return [...entries.values()] } diff --git a/packages/llm/llm-pi-ai/tests/dynamic-config.spec.ts b/packages/llm/llm-pi-ai/tests/dynamic-config.spec.ts index da7937567a..0c6ec7f26f 100644 --- a/packages/llm/llm-pi-ai/tests/dynamic-config.spec.ts +++ b/packages/llm/llm-pi-ai/tests/dynamic-config.spec.ts @@ -113,7 +113,11 @@ describe('request-level dynamic profiles', () => { + 'the installed catalog does not describe it, so set the route\'s api to the wire protocol its endpoint speaks' expect(ctx.settings.describe().map(section => section.ns)).toContain(NS) - expect(ctx.llm.listProviders()).toContainEqual({ id: 'openrouter', name: 'openrouter', configurationError: failure }) + expect(ctx.llm.listProviders()).toEqual([{ id: 'openrouter', name: 'openrouter' }]) + expect(ctx.llm.listConfigurableProviders()).toContainEqual({ + provider: 'openrouter', displayName: 'openrouter', settingsNs: NS, + settingsPath: ['providers', 'openrouter'], declared: false, error: failure, + }) expect(await readFile(path, 'utf8')).toBe(stored) expect((await ctx.llm.listModels('openrouter')).map(model => model.id)).toEqual([known.id]) const bad = await assemble(ctx, { provider: 'openrouter', model: '111', messages: [] }) @@ -128,7 +132,12 @@ describe('request-level dynamic profiles', () => { await expect(ctx.settings.update(NS, { providers: { openrouter: { displayName: 'Edited' } } })).rejects.toThrow(failure) expect(await readFile(path, 'utf8')).toBe(beforeRejected) + const diagnostics: Array = [] + ctx.on('llm/adapters-updated', () => { + diagnostics.push(ctx.llm.listConfigurableProviders().find(entry => entry.provider === 'openrouter')?.error) + }) await ctx.settings.mutate(NS, [{ op: 'set', path: ['providers', 'openrouter', 'api'], value: 'openai-completions' }]) + expect(diagnostics).toEqual([undefined]) expect(ctx.llm.listProviders()[0]).toEqual({ id: 'openrouter', name: 'openrouter' }) const repaired = await assemble(ctx, { provider: 'openrouter', model: '111', messages: [] }) expect(repaired.message.content).toEqual([{ type: 'text', text: 'hello' }]) @@ -143,16 +152,18 @@ describe('request-level dynamic profiles', () => { } } })) const ctx = await boot(dir, {}) expect(ctx.settings.describe().map(section => section.ns)).toContain(NS) - expect(ctx.llm.listProviders().map(provider => provider.configurationError)).toEqual([ - expect.stringContaining('modelOverrides names "removed-model"'), - expect.stringContaining('resolves no models'), - ]) + expect(ctx.llm.listConfigurableProviders().find(entry => entry.provider === 'anthropic')?.error) + .toContain('modelOverrides names "removed-model"') + expect(ctx.llm.listConfigurableProviders().find(entry => entry.provider === 'retired-route')?.error) + .toContain('resolves no models') expect((await ctx.llm.listModels('anthropic')).length).toBeGreaterThan(0) await expect(ctx.llm.resolveModelInfo('anthropic', 'removed-model')).rejects.toThrow('modelOverrides names "removed-model"') await expect(ctx.llm.resolveModelInfo('retired-route', 'anything')).rejects.toThrow('resolves no models') await ctx.settings.mutate(NS, [{ op: 'unset', path: ['providers', 'retired-route'] }]) await ctx.settings.mutate(NS, [{ op: 'unset', path: ['providers', 'anthropic', 'modelOverrides', 'removed-model'] }]) expect(ctx.llm.listProviders()).toEqual([{ id: 'anthropic', name: 'anthropic' }]) + expect(ctx.llm.listConfigurableProviders().find(entry => entry.provider === 'retired-route')).toBeUndefined() + expect(ctx.llm.listConfigurableProviders().find(entry => entry.provider === 'anthropic')?.error).toBeUndefined() }) it('mounts bare and dormant, then registers routes the moment settings supply providers', async () => { diff --git a/packages/llm/llm/README.i18n.yaml b/packages/llm/llm/README.i18n.yaml index 7e5d5ccc35..21ca8ed6b6 100644 --- a/packages/llm/llm/README.i18n.yaml +++ b/packages/llm/llm/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/llm/llm/README.md -README.md: a0f79106765e17437cf6bfd2cbf91e2b0cef34a9 -README.zh.md: fe99f7680e07f1742869c12692668ae426083d60 +README.md: 5fe789f38caf8e2289930ff598868ddab69d5f76 +README.zh.md: 8fb48b01e0c7a888b48f65d189279a8e0f684264 diff --git a/packages/llm/llm/README.md b/packages/llm/llm/README.md index a0f7910676..5fe789f38c 100644 --- a/packages/llm/llm/README.md +++ b/packages/llm/llm/README.md @@ -54,13 +54,13 @@ for await (const chunk of ctx.llm.stream({ } ``` -After a successful mount, `ctx.llm.listProviders()` reports the registered routes in registration order. Adapters may include `configurationError` in provider metadata to keep a failed configuration visible for repair; this diagnostic does not unregister its route. +After a successful mount, `ctx.llm.listProviders()` reports the registered routes in registration order. ### What you can do - **Stream one model call** — `ctx.llm.stream(options)` yields raw chunks (token-level deltas) for any registered provider and model; consumers assemble them with `BlockAssembler`. - **Register provider adapters** — an adapter owns one or more provider routes, and its registration captures that route's retry policy; registering the same route twice fails with `DUPLICATE_ADAPTER`. -- **Expose and activate providers through configuration** — adapters declare configurable-provider routes plus a settings namespace, so configuration surfaces can activate dormant providers and edit connection facts without a restart. +- **Expose and activate providers through configuration** — adapters declare configurable-provider routes plus a settings namespace, so configuration surfaces can activate dormant providers and edit connection facts without a restart. `LlmConfigurableProvider.error` reports a configuration diagnostic for repair; unaffected models can remain serviceable. - **Discover and resolve models** — list the models an adapter advertises, interrogate an endpoint for the models it serves, and resolve one exact model's context window, output default, reasoning efforts, and input modalities. - **Validate call config** — an explicit or configured reasoning effort is checked against the exact model before any provider I/O, and an adapter-configured output cap is materialized when the request omits one. - **Read an embedded Assistant stream without expanding it** — `assistantStreamFirstTokenTime` (first token), `assistantStreamHasVisibleContent` (any visible content), and `assistantStreamHasVisibleText` (any visible text) answer their questions from the compact records with early exit; `lastAssistantStreamChunk` scans backward to the last raw chunk of one type, `assistantStreamChunks` and `joinAssistantStreamText` scan the whole stream, and `assembleAssistantStream` feeds a `BlockAssembler` one joined delta per run with the same blocks, usage, and replay state as the per-member expansion. `runFirstTokenTime` and `runFirstVisibleTime` do the early-exit scan for one packed run, and `isTokenDelta`, `isVisibleChunk`, and `chunkHasVisibleText` define the token and visibility rules for a single chunk. `expandAssistantStream` remains the validating path for records read at a durable boundary; it is not memoized, because a retained expansion costs roughly ten times the compact stream for as long as the event lives. diff --git a/packages/llm/llm/README.zh.md b/packages/llm/llm/README.zh.md index fe99f7680e..8fb48b01e0 100644 --- a/packages/llm/llm/README.zh.md +++ b/packages/llm/llm/README.zh.md @@ -54,13 +54,13 @@ for await (const chunk of ctx.llm.stream({ } ``` -挂载成功后,`ctx.llm.listProviders()` 会按注册顺序报告已注册路由。适配器可以在提供方元数据中携带 `configurationError`,保留错误配置的可见性以供修复;此诊断不会注销路由。 +挂载成功后,`ctx.llm.listProviders()` 会按注册顺序报告已注册路由。 ### 你可以做什么 - **流式发起一次模型调用**——`ctx.llm.stream(options)` 为任何已注册提供方与模型产出原始分片(token 级增量);消费方用 `BlockAssembler` 组装。 - **注册提供方适配器**——一个适配器拥有一个或多个提供方路由,其注册会捕获该路由的重试策略;重复注册同一路由会以 `DUPLICATE_ADAPTER` 失败。 -- **通过配置暴露并激活提供方**——适配器声明可配置提供方路由与 settings namespace,配置界面因此可以激活休眠提供方并编辑连接事实,无需重启。 +- **通过配置暴露并激活提供方**——适配器声明可配置提供方路由与 settings namespace,配置界面因此可以激活休眠提供方并编辑连接事实,无需重启。`LlmConfigurableProvider.error` 报告供修复的配置诊断;未受影响的模型仍可提供服务。 - **发现与解析模型**——列出适配器公布的模型、询问端点它提供哪些模型,并解析某个精确模型的上下文窗口、输出默认值、推理(reasoning)强度与输入模态。 - **校验调用配置**——显式或配置的推理强度会在任何提供方 I/O 之前对照精确模型校验;请求省略输出上限时,会填入适配器配置的输出上限。 - **不展开即读取内嵌 Assistant 流**——`assistantStreamFirstTokenTime`(首 token)、`assistantStreamHasVisibleContent`(任一可见内容)与 `assistantStreamHasVisibleText`(任一可见文本)以提前退出从紧凑记录回答各自的问题;`lastAssistantStreamChunk` 反向扫描到某一类型的最后一个原始 chunk,`assistantStreamChunks` 与 `joinAssistantStreamText` 扫描整个流,`assembleAssistantStream` 向 `BlockAssembler` 每个 run 喂一段拼接 delta,blocks/usage/replayState 与逐成员展开相同。`runFirstTokenTime` 与 `runFirstVisibleTime` 对单个打包 run 做提前退出扫描,`isTokenDelta`、`isVisibleChunk` 与 `chunkHasVisibleText` 定义单个 chunk 的 token 与可见性规则。`expandAssistantStream` 仍是持久边界读取记录的校验路径;它不被记忆化,因为保留的展开在事件生命周期内约花费紧凑流的十倍内存。 diff --git a/packages/llm/llm/src/index.ts b/packages/llm/llm/src/index.ts index 85ec9c996f..6a3cc5de71 100644 --- a/packages/llm/llm/src/index.ts +++ b/packages/llm/llm/src/index.ts @@ -434,10 +434,7 @@ export class LlmRuntime extends TypertRemoteService { ?? resolveRetryPolicy(undefined, `llm: provider "${provider}" retryPolicy`) registrations.push({ adapter, - provider: { - id: info.id, name: info.name, - ...info.configurationError === undefined ? {} : { configurationError: info.configurationError }, - }, + provider: { id: info.id, name: info.name }, retryPolicy, }) } diff --git a/packages/llm/llm/src/types.ts b/packages/llm/llm/src/types.ts index d533180f18..72bae5858b 100644 --- a/packages/llm/llm/src/types.ts +++ b/packages/llm/llm/src/types.ts @@ -198,8 +198,6 @@ export interface LlmProviderInfo { id: string /** Human-readable provider name for selectors and diagnostics. */ name: string - /** Configuration diagnostic; the route stays registered for inspection and repair. */ - configurationError?: string } /** Merge-extensible provider model modality vocabulary. */ @@ -238,6 +236,8 @@ export interface LlmConfigurableProvider { * from outside. */ declared?: boolean + /** Configuration diagnostic for repair; unaffected models may remain serviceable. */ + error?: string } /** diff --git a/packages/llm/llm/tests/service.spec.ts b/packages/llm/llm/tests/service.spec.ts index 49fea78865..2bfcd0f486 100644 --- a/packages/llm/llm/tests/service.spec.ts +++ b/packages/llm/llm/tests/service.spec.ts @@ -266,7 +266,6 @@ describe('LlmRuntime', () => { expect(projected, fixture.name).toMatchObject({ type: 'text' }) if (projected?.type !== 'text') throw new Error(`expected projected text for ${fixture.name}`) expect(projected.text, fixture.name).toContain(fixture.expected) - expect(ctx.llm.fileRequestText(attachment), fixture.name).toBe(projected.text) if (fixture.fs !== undefined) { expect(projected.text, fixture.name).toContain('include this saved path in the delegation prompt') } From ad05ed6ad815f2c6ae4a0bb72d5f577c01df28b2 Mon Sep 17 00:00:00 2001 From: fz Date: Tue, 8 Sep 2026 11:33:24 +0800 Subject: [PATCH 10/58] docs(minimal): describe current editor availability --- ...026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml | 4 ++-- .../2026-09-03-minimal-profiles-persistent-shell-only.md | 2 +- .../2026-09-03-minimal-profiles-persistent-shell-only.zh.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml index c5c44eded0..3470a11031 100644 --- a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml +++ b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md -2026-09-03-minimal-profiles-persistent-shell-only.md: 34229d17913e9fe944bbb458ec439eaf93e57fe8 -2026-09-03-minimal-profiles-persistent-shell-only.zh.md: 4183696f7c1feece4599ec6ce0e000c0d949aceb +2026-09-03-minimal-profiles-persistent-shell-only.md: 1e49006b07645c34f273abf7f707fa0d79cfa14e +2026-09-03-minimal-profiles-persistent-shell-only.zh.md: bd23b1d89665a8093550e6991cd4940cea79a90d diff --git a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md index 34229d1791..1e49006b07 100644 --- a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md +++ b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md @@ -14,7 +14,7 @@ Using one persistent shell gives the model a consistent file-operation interface The shipped minimal compositions expose exactly one platform-selected persistent shell: `bash` on Linux and macOS, or `pwsh` on Windows. Neither composition mounts `@deepseek-ai/dsh-tool-str-replace-editor`, a filesystem tool, or the `fs-local` service that supported the editor. The fixed complete persona, absence of runtime context and compaction, shell timeout, and launch-specific host services remain unchanged. -The standalone editor package remains available in the short term for explicit custom compositions. A trusted user-authored preset or higher profile patch must insert the editor into the Cordis tree with a filesystem provider in the same service scope; the shipped `minimal` and `sdk-minimal` defaults never insert it. The [Python SDK guide](../../../../docs/user/guide/python-sdk.md#opt-in-to-str_replace_editor) provides an executable patch example. +The standalone editor package remains available for explicit custom compositions. A trusted user-authored preset or higher profile patch must insert the editor into the Cordis tree with a filesystem provider in the same service scope; the shipped `minimal` and `sdk-minimal` defaults never insert it. The [Python SDK guide](../../../../docs/user/guide/python-sdk.md#opt-in-to-str_replace_editor) provides an executable patch example. Exact composition tests assert the single tool and the absence of a preset-local filesystem service. The `sdk-minimal` bundle test and built config dump assert that its row and dependency allowlists contain neither `fs-local` nor `dsh-tool-str-replace-editor`. Web and packaged-Python model-visible snapshots pin the one-tool schema roster. SDK profile smoke tests execute the guide's editor patch and verify file creation and viewing. diff --git a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md index 4183696f7c..bd23b1d896 100644 --- a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md +++ b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md @@ -14,7 +14,7 @@ Status: implemented 随附的极简组合只提供一个按平台选择的持久 shell:Linux 与 macOS 使用 `bash`,Windows 使用 `pwsh`。两份组合都不挂载 `@deepseek-ai/dsh-tool-str-replace-editor`、文件系统工具或支撑 editor 的 `fs-local` 服务。固定的 complete persona、运行时上下文与 compaction 的缺失、shell 超时和各启动路径的宿主服务保持不变。 -独立 editor 包在短期内仍可用于显式自定义组合。受信任的用户自定义 preset 或更高优先级的 profile patch 必须将 editor 插入 Cordis tree,并在同一服务作用域内提供文件系统后端;随附的 `minimal` 与 `sdk-minimal` 默认组合不会插入它。[Python SDK 指南](../../../../docs/user/guide/python-sdk.zh.md#opt-in-to-str_replace_editor)提供可执行的 patch 示例。 +独立 editor 包仍可用于显式自定义组合。受信任的用户自定义 preset 或更高优先级的 profile patch 必须将 editor 插入 Cordis tree,并在同一服务作用域内提供文件系统后端;随附的 `minimal` 与 `sdk-minimal` 默认组合不会插入它。[Python SDK 指南](../../../../docs/user/guide/python-sdk.zh.md#opt-in-to-str_replace_editor)提供可执行的 patch 示例。 精确组合测试会断言单工具清单以及 preset 内不存在文件系统服务。`sdk-minimal` bundle 测试与构建后配置转储会断言配置项和依赖 allowlist 都不含 `fs-local` 或 `dsh-tool-str-replace-editor`。Web 与打包 Python 的模型可见快照会固定单工具 schema 清单。SDK profile 冒烟测试会执行指南中的 editor patch,并验证文件创建和查看。 From 8dc30d270c4757571f30ebecad78d15d7530cb83 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Tue, 8 Sep 2026 12:04:18 +0800 Subject: [PATCH 11/58] test(llm-pi-ai): cover recovery diagnostics and document repair limits --- ...-pi-ai-settings-catalog-recovery.i18n.yaml | 4 ++-- ...6-09-07-pi-ai-settings-catalog-recovery.md | 4 ++-- ...9-07-pi-ai-settings-catalog-recovery.zh.md | 6 +++--- .../tests/components.client.spec.tsx | 20 +++++++++++++++++++ packages/llm/llm-pi-ai/README.i18n.yaml | 4 ++-- packages/llm/llm-pi-ai/README.md | 2 ++ packages/llm/llm-pi-ai/README.zh.md | 2 ++ .../llm-pi-ai/tests/dynamic-config.spec.ts | 8 +++++--- 8 files changed, 38 insertions(+), 12 deletions(-) diff --git a/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.i18n.yaml index 0213917daa..a0173208ab 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.md -2026-09-07-pi-ai-settings-catalog-recovery.md: 13c23233e924b6ba2e0b22381e791c5ccdcdc254 -2026-09-07-pi-ai-settings-catalog-recovery.zh.md: 6f001f545108aa2006491127318ae3123722364f +2026-09-07-pi-ai-settings-catalog-recovery.md: 7b9fba06f6d5447654420a5912e45d1684ec5041 +2026-09-07-pi-ai-settings-catalog-recovery.zh.md: d68a36cffd240df63aeb7cbb5d1922b383360fd8 diff --git a/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.md b/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.md index 13c23233e9..7b9fba06f6 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.md +++ b/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.md @@ -14,7 +14,7 @@ The pi-ai consumer uses the existing settings `validate` callback. During namesp Initial profile resolution retains catalog diagnostics, while schema and self-contained profile constraints still reject loading. Writes strictly resolve each new or changed provider, comparing effective provider values against the committed snapshot. Unchanged failed providers do not block another provider's edit, and deletion remains possible. Editing a provider-wide setting validates all models it affects. -Profile resolution keeps valid models beside per-model errors. A missing override retains its diagnostic without disabling the remaining catalog. A route-level catalog failure retains its provider and editable settings but supplies no callable models. The adapter checks the selected model's recorded failure before credentials or network I/O and reports `INVALID_CONFIG`. No protocol is guessed and no user configuration is rewritten during loading. Immutable snapshots still keep an in-flight request on its captured configuration. +Profile resolution keeps valid models beside per-model errors. A missing override retains its diagnostic without disabling the remaining catalog. A route-level catalog failure retains its provider and editable settings but supplies no callable models. When route-wide validation aborts catalog resolution, the incomplete catalog and its collected per-model diagnostics are discarded; model requests on that route report the route-level error. The adapter checks the selected model's recorded failure before credentials or network I/O and reports `INVALID_CONFIG`. No protocol is guessed and no user configuration is rewritten during loading. Immutable snapshots still keep an in-flight request on its captured configuration. `LlmConfigurableProvider.error` carries the first diagnostic for the provider row. The configurable-provider directory publishes diagnostic changes so configuration repair refreshes the browser without re-registering the adapter. Failed model ids remain in settings, while the model selector receives serviceable entries. Models settings displays the diagnostic and retains edit/delete controls. Both add actions require their owning settings namespace; the ordinary add menu filters out unavailable namespaces. @@ -32,7 +32,7 @@ This extends the [provider-routed adapter decision](../architecture/2026-07-14-p ## Consequences -Upgrade-dependent errors remain visible and repairable without weakening validation of new provider edits. Configuration errors remain distinct from remote model existence: a catalog-external id with an explicit protocol is accepted, and its endpoint decides whether that id exists. Scalar or document errors still fail early. No settings API, storage format, or session event is added; configurable-provider entries gain one optional diagnostic field. +Upgrade-dependent errors remain visible and repairable without weakening validation of new provider edits. Configuration errors remain distinct from remote model existence: a catalog-external id with an explicit protocol is accepted, and its endpoint decides whether that id exists. Scalar or document errors still fail early. Models settings does not explain namespace registration failures; those errors require inspecting the configuration and startup diagnostics. No settings API, storage format, or session event is added; configurable-provider entries gain one optional diagnostic field. ## Testing diff --git a/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.zh.md b/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.zh.md index 6f001f5451..d68a36cffd 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.zh.md @@ -2,7 +2,7 @@ Status: implemented -English | [中文](2026-09-07-pi-ai-settings-catalog-recovery.md) +[English](2026-09-07-pi-ai-settings-catalog-recovery.md) | 中文 ## Problem @@ -14,7 +14,7 @@ pi-ai 消费者使用现有 settings `validate` 回调。命名空间注册期 首次解析 profile 时保留目录诊断,而 schema 与 profile 自身的约束仍会拒绝加载。写入会将有效提供方值与已提交快照比较,严格解析每个新增或修改的提供方。未修改的错误提供方不会阻止其他提供方编辑,删除仍然可用。修改提供方级设置会校验其影响的所有模型。 -Profile 解析在有效模型旁保留逐模型错误。失去引用目标的覆盖会保留诊断,而不会禁用其余目录。路由级目录失败会保留提供方与可编辑设置,但不提供可调用模型。适配器在解析凭据和网络 I/O 前检查所选模型已记录的错误,并报告 `INVALID_CONFIG`。加载过程不会猜测协议或改写用户配置。不可变快照仍保证进行中的请求使用其捕获的配置。 +Profile 解析在有效模型旁保留逐模型错误。失去引用目标的覆盖会保留诊断,而不会禁用其余目录。路由级目录失败会保留提供方与可编辑设置,但不提供可调用模型。路由级校验中止目录解析时,不完整的目录及其已收集的逐模型诊断会被丢弃,该路由上的模型请求统一报告路由级错误。适配器在解析凭据和网络 I/O 前检查所选模型已记录的错误,并报告 `INVALID_CONFIG`。加载过程不会猜测协议或改写用户配置。不可变快照仍保证进行中的请求使用其捕获的配置。 `LlmConfigurableProvider.error` 为提供方行携带首个诊断。可配置提供方目录发布诊断变化,修复配置会刷新浏览器,无需重新注册适配器。错误模型 ID 保留在设置中,模型选择器只接收可服务条目。模型设置页显示诊断并保留编辑、删除控件。两个添加操作都要求其所属 settings 命名空间存在;普通添加菜单会过滤不可用的命名空间。 @@ -32,7 +32,7 @@ Profile 解析在有效模型旁保留逐模型错误。失去引用目标的覆 ## Consequences -依赖升级产生的错误仍可见、可修复,而新增提供方编辑的校验不会放宽。配置错误与远端模型是否存在仍然不同:显式指定协议的目录外 ID 可以被接受,由端点决定该 ID 是否存在。标量或文档错误仍尽早失败。不增加 Settings API、存储格式或 session 事件;可配置提供方条目新增一个可选诊断字段。 +依赖升级产生的错误仍可见、可修复,而新增提供方编辑的校验不会放宽。配置错误与远端模型是否存在仍然不同:显式指定协议的目录外 ID 可以被接受,由端点决定该 ID 是否存在。标量或文档错误仍尽早失败。模型设置页不展示命名空间注册失败的原因,此类错误需要检查配置与启动诊断。不增加 Settings API、存储格式或 session 事件;可配置提供方条目新增一个可选诊断字段。 ## Testing diff --git a/packages/client/ui-settings-models/tests/components.client.spec.tsx b/packages/client/ui-settings-models/tests/components.client.spec.tsx index e8823c5eea..0d72c77713 100644 --- a/packages/client/ui-settings-models/tests/components.client.spec.tsx +++ b/packages/client/ui-settings-models/tests/components.client.spec.tsx @@ -348,6 +348,26 @@ describe('ModelsSection', () => { expect(document.body.textContent).toBe('') }) + it('shows a configuration diagnostic inside the first-run setup card', async () => { + const scripted = scriptedFace() + const failure = 'The provider configuration needs repair' + scripted.face.llm.listProviders.mockResolvedValue(remoteOk([ + { id: 'deepseek-official', name: 'DeepSeek' }, + ])) + scripted.face.llm.listConfigurableProviders.mockResolvedValue(remoteOk([ + { provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [], error: failure }, + ])) + scripted.face.credentials.describe.mockResolvedValue(remoteOk({ + DEEPSEEK_API_KEY: { configured: false, writable: true }, + })) + await mountFace(scripted) + + const card = screen.getByRole('listitem') + expect(within(card).getByRole('alert').textContent).toBe(failure) + expect(within(card).getByLabelText(en.keyInput)).toBeTruthy() + expect(within(card).queryByRole('button', { name: deepSeekCopy(en.editProvider) })).toBeNull() + }) + it('dispatches the provider-card seat per rendered row, keyed by the owning namespace', async () => { const { renderSlot } = await mountSection() const cards = cardSeatCalls(renderSlot) diff --git a/packages/llm/llm-pi-ai/README.i18n.yaml b/packages/llm/llm-pi-ai/README.i18n.yaml index 51f8132299..5ea2ba0ea1 100644 --- a/packages/llm/llm-pi-ai/README.i18n.yaml +++ b/packages/llm/llm-pi-ai/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/llm/llm-pi-ai/README.md -README.md: 799c99386443922bd8d4e24d8f84a826d18ff67e -README.zh.md: 8aecbc840725e02e565f2b228444d03565d931e4 +README.md: bef10768bfa758a29781b6eae0bc4c52ada8eba0 +README.zh.md: 870cf78f485de57d50f8a4e70f821022b1686ddd diff --git a/packages/llm/llm-pi-ai/README.md b/packages/llm/llm-pi-ai/README.md index 799c993864..bef10768bf 100644 --- a/packages/llm/llm-pi-ai/README.md +++ b/packages/llm/llm-pi-ai/README.md @@ -116,6 +116,8 @@ A route pi-ai does not ship needs `api`, `baseURL`, and a non-empty `models` lis Settings writes strictly validate each new or changed provider after merging its composition and user layers. During namespace registration, stored catalog failures retain the namespace and provider rows, with the first diagnostic in `LlmConfigurableProvider.error`; unchanged failed providers do not block edits elsewhere. Serviceable models remain selectable, while unresolved models remain in the editable configuration and fail with `INVALID_CONFIG` before network I/O if requested directly. Repairing or deleting the offending configuration clears its diagnostic. Schema and self-contained profile errors still reject loading. Later external edits validate changed providers and retain the last accepted section on failure. +Changing `displayName`, `apiKeyEnv`, or `baseURL` without resolving the provider's model errors still rejects the save. For example, renaming an OpenRouter route whose model `111` needs an `api` cannot be saved on its own: repair or remove that model in the same editor draft, then save the complete provider configuration. Intermediate repairs remain in the draft until the whole provider validates; other providers can be saved independently. + ----- diff --git a/packages/llm/llm-pi-ai/README.zh.md b/packages/llm/llm-pi-ai/README.zh.md index 8aecbc8407..870cf78f48 100644 --- a/packages/llm/llm-pi-ai/README.zh.md +++ b/packages/llm/llm-pi-ai/README.zh.md @@ -116,6 +116,8 @@ pi-ai 不提供的路由需要 `api`、`baseURL` 与非空 `models` 列表;无 Settings 写入会在合并组合层与用户层后严格校验每个新增或修改的提供方。命名空间注册时,已存储配置的目录解析错误会保留命名空间与提供方行,并通过 `LlmConfigurableProvider.error` 返回首个诊断;未修改的错误提供方不会阻止其他编辑。可解析的模型仍可选择,无法解析的模型保留在可编辑配置中,直接请求时会在网络 I/O 前以 `INVALID_CONFIG` 失败。修复或删除错误配置会清除诊断。Schema 与 profile 自身的约束错误仍会拒绝加载。后续外部文件编辑会校验变化的提供方,失败时保留最后一次接受的分节。 +只修改 `displayName`、`apiKeyEnv` 或 `baseURL` 而未解决提供方的模型配置错误时,保存仍会被拒绝。例如,OpenRouter 路由的模型 `111` 缺少 `api` 时,不能单独保存路由名称的修改:需要在同一份编辑草稿中修复或删除该模型,再保存完整的提供方配置。中间修复状态保留在草稿中,直到整条提供方配置通过校验;其他提供方可以独立保存。 + ----- diff --git a/packages/llm/llm-pi-ai/tests/dynamic-config.spec.ts b/packages/llm/llm-pi-ai/tests/dynamic-config.spec.ts index 0c6ec7f26f..ad6eddf547 100644 --- a/packages/llm/llm-pi-ai/tests/dynamic-config.spec.ts +++ b/packages/llm/llm-pi-ai/tests/dynamic-config.spec.ts @@ -75,7 +75,8 @@ describe('login flows in a real composition', () => { }) describe('request-level dynamic profiles', () => { - it('retains the last accepted profiles after an invalid external edit and accepts a repaired file', async () => { + // Real filesystem notifications can lag behind chokidar's stability window on busy hosts. + it('retains the last accepted profiles after an invalid external edit and accepts a repaired file', { timeout: 30_000 }, async () => { const dir = await home() const path = join(dir, 'settings.yaml') await writeFile(path, JSON.stringify({ [NS]: { providers: { deepseek: {} } } })) @@ -83,14 +84,15 @@ describe('request-level dynamic profiles', () => { await writeFile(path, JSON.stringify({ [NS]: { providers: { openrouter: { models: [{ id: '111' }] } } } })) // The raw section proves the watcher processed the edit even though validation kept the old resolved value. - await expect.poll(() => ctx.settings.describe().find(section => section.ns === NS)?.user) + await expect.poll(() => ctx.settings.describe().find(section => section.ns === NS)?.user, { timeout: 10_000 }) .toEqual({ providers: { openrouter: { models: [{ id: '111' }] } } }) expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek', name: 'deepseek' }]) await writeFile(path, JSON.stringify({ [NS]: { providers: { openrouter: { api: 'openai-completions', models: [{ id: '111' }] }, } } })) - await expect.poll(() => ctx.llm.listProviders()).toEqual([{ id: 'openrouter', name: 'openrouter' }]) + await expect.poll(() => ctx.llm.listProviders(), { timeout: 10_000 }) + .toEqual([{ id: 'openrouter', name: 'openrouter' }]) expect((await ctx.llm.listModels('openrouter')).map(model => model.id)).toEqual(['111']) }) From da313e4fdc0681318a54be7b9733c977e3b0ca97 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Tue, 8 Sep 2026 13:35:09 +0800 Subject: [PATCH 12/58] fix(llm-pi-ai): preserve actionable model diagnostics --- ...6-09-07-pi-ai-settings-catalog-recovery.i18n.yaml | 4 ++-- .../2026-09-07-pi-ai-settings-catalog-recovery.md | 2 +- .../2026-09-07-pi-ai-settings-catalog-recovery.zh.md | 2 +- .../stored-error.expected.md | 5 +++++ apps/web/tests/models-settings-recovery.e2e.ts | 7 ++++++- packages/llm/llm-pi-ai/README.i18n.yaml | 4 ++-- packages/llm/llm-pi-ai/README.md | 2 +- packages/llm/llm-pi-ai/README.zh.md | 2 +- packages/llm/llm-pi-ai/src/config.ts | 6 +++--- packages/llm/llm-pi-ai/tests/catalog.spec.ts | 12 ++++++++++++ 10 files changed, 34 insertions(+), 12 deletions(-) diff --git a/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.i18n.yaml index a0173208ab..43f6f3b14c 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.md -2026-09-07-pi-ai-settings-catalog-recovery.md: 7b9fba06f6d5447654420a5912e45d1684ec5041 -2026-09-07-pi-ai-settings-catalog-recovery.zh.md: d68a36cffd240df63aeb7cbb5d1922b383360fd8 +2026-09-07-pi-ai-settings-catalog-recovery.md: 21fe532a491775ff875f6b9bcb000d917a95e13c +2026-09-07-pi-ai-settings-catalog-recovery.zh.md: 80bd0758e13b767671f4cec52ee3832c9250651f diff --git a/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.md b/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.md index 7b9fba06f6..21fe532a49 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.md +++ b/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.md @@ -16,7 +16,7 @@ Initial profile resolution retains catalog diagnostics, while schema and self-co Profile resolution keeps valid models beside per-model errors. A missing override retains its diagnostic without disabling the remaining catalog. A route-level catalog failure retains its provider and editable settings but supplies no callable models. When route-wide validation aborts catalog resolution, the incomplete catalog and its collected per-model diagnostics are discarded; model requests on that route report the route-level error. The adapter checks the selected model's recorded failure before credentials or network I/O and reports `INVALID_CONFIG`. No protocol is guessed and no user configuration is rewritten during loading. Immutable snapshots still keep an in-flight request on its captured configuration. -`LlmConfigurableProvider.error` carries the first diagnostic for the provider row. The configurable-provider directory publishes diagnostic changes so configuration repair refreshes the browser without re-registering the adapter. Failed model ids remain in settings, while the model selector receives serviceable entries. Models settings displays the diagnostic and retains edit/delete controls. Both add actions require their owning settings namespace; the ordinary add menu filters out unavailable namespaces. +`LlmConfigurableProvider.error` carries the first available model diagnostic for the provider row, falling back to the route error. A provider-construction failure does not overwrite a collected model diagnostic, preserving the specific correction for a missing protocol. The configurable-provider directory publishes diagnostic changes so configuration repair refreshes the browser without re-registering the adapter. Failed model ids remain in settings, while the model selector receives serviceable entries. Models settings displays the diagnostic and retains edit/delete controls. Both add actions require their owning settings namespace; the ordinary add menu filters out unavailable namespaces. This extends the [provider-routed adapter decision](../architecture/2026-07-14-provider-routed-llm-adapters.md): provider ownership and request snapshots remain unchanged, while catalog validity does not determine whether settings can be managed. That note remains active for routing, ownership, and replay rationale. diff --git a/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.zh.md b/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.zh.md index d68a36cffd..80bd0758e1 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.zh.md @@ -16,7 +16,7 @@ pi-ai 消费者使用现有 settings `validate` 回调。命名空间注册期 Profile 解析在有效模型旁保留逐模型错误。失去引用目标的覆盖会保留诊断,而不会禁用其余目录。路由级目录失败会保留提供方与可编辑设置,但不提供可调用模型。路由级校验中止目录解析时,不完整的目录及其已收集的逐模型诊断会被丢弃,该路由上的模型请求统一报告路由级错误。适配器在解析凭据和网络 I/O 前检查所选模型已记录的错误,并报告 `INVALID_CONFIG`。加载过程不会猜测协议或改写用户配置。不可变快照仍保证进行中的请求使用其捕获的配置。 -`LlmConfigurableProvider.error` 为提供方行携带首个诊断。可配置提供方目录发布诊断变化,修复配置会刷新浏览器,无需重新注册适配器。错误模型 ID 保留在设置中,模型选择器只接收可服务条目。模型设置页显示诊断并保留编辑、删除控件。两个添加操作都要求其所属 settings 命名空间存在;普通添加菜单会过滤不可用的命名空间。 +`LlmConfigurableProvider.error` 优先为提供方行携带首个模型诊断,无模型诊断时返回路由错误。提供方构造失败不会覆盖已收集的模型诊断,从而保留缺少协议时的具体修复提示。可配置提供方目录发布诊断变化,修复配置会刷新浏览器,无需重新注册适配器。错误模型 ID 保留在设置中,模型选择器只接收可服务条目。模型设置页显示诊断并保留编辑、删除控件。两个添加操作都要求其所属 settings 命名空间存在;普通添加菜单会过滤不可用的命名空间。 本决策扩展了[按提供方路由的适配器决策](../architecture/2026-07-14-provider-routed-llm-adapters.zh.md):提供方所有权与请求快照不变,目录有效性不决定设置是否可管理。旧记录仍保留为路由、所有权与回放设计的依据。 diff --git a/apps/web/tests/expected/models-settings-recovery/stored-error.expected.md b/apps/web/tests/expected/models-settings-recovery/stored-error.expected.md index 31fca1116a..6a1a3e0511 100644 --- a/apps/web/tests/expected/models-settings-recovery/stored-error.expected.md +++ b/apps/web/tests/expected/models-settings-recovery/stored-error.expected.md @@ -29,6 +29,11 @@ - text: zai - button "编辑 zai": 编辑 - button "删除 zai": 删除 + - listitem: + - text: acme-gateway 自定义 + - button "编辑 acme-gateway": 编辑 + - button "删除 acme-gateway": 删除 + - alert: "llm-pi-ai: provider \"acme-gateway\" model \"custom-model\" needs an api; the installed catalog does not describe it, so set the route's api to the wire protocol its endpoint speaks" - button "添加提供方": - img - text: 添加提供方 diff --git a/apps/web/tests/models-settings-recovery.e2e.ts b/apps/web/tests/models-settings-recovery.e2e.ts index c8e06a5aae..1087f8be3d 100644 --- a/apps/web/tests/models-settings-recovery.e2e.ts +++ b/apps/web/tests/models-settings-recovery.e2e.ts @@ -14,6 +14,8 @@ import { saveFailureShot, ZH_BROWSER_LOCALE } from './support.ts' const EXPECTED = fileURLToPath(new URL('./expected/models-settings-recovery/stored-error.expected.md', import.meta.url)) const FAILURE = 'llm-pi-ai: provider "openrouter" model "111" needs an api; ' + 'the installed catalog does not describe it, so set the route\'s api to the wire protocol its endpoint speaks' +const CUSTOM_FAILURE = 'llm-pi-ai: provider "acme-gateway" model "custom-model" needs an api; ' + + 'the installed catalog does not describe it, so set the route\'s api to the wire protocol its endpoint speaks' describe('web e2e: repairs a stored provider after catalog drift', () => { let home: string @@ -26,7 +28,8 @@ describe('web e2e: repairs a stored provider after catalog drift', () => { home = await mkdtemp(join(tmpdir(), 'dsh-models-recovery-')) await writeFile(join(home, 'settings.yaml'), [ 'llm-pi-ai:', ' providers:', ' openrouter:', ' models:', - ' - id: "111"', ' zai: {}', '', + ' - id: "111"', ' zai: {}', ' acme-gateway:', + ' baseURL: https://gateway.example/v1', ' models:', ' - id: "custom-model"', '', ].join('\n')) scaffold = await launchWebScaffold({ harnessHome: home }) browser = await chromium.launch() @@ -56,6 +59,8 @@ describe('web e2e: repairs a stored provider after catalog drift', () => { const dialog = page.getByRole('dialog', { name: '设置' }) expect(await dialog.getByRole('button', { name: '编辑 openrouter', exact: true }).count()).toBe(1) expect(await dialog.getByRole('button', { name: '编辑 zai', exact: true }).count()).toBe(1) + expect(await dialog.getByRole('button', { name: '编辑 acme-gateway', exact: true }).count()).toBe(1) + expect(await dialog.getByText(CUSTOM_FAILURE, { exact: true }).count()).toBe(1) expect(await dialog.getByRole('button', { name: '添加提供方', exact: true }).isEnabled()).toBe(true) expect(await dialog.getByRole('button', { name: '添加自定义提供方', exact: true }).isEnabled()).toBe(true) await compareOrRefreshGolden(EXPECTED, await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd), webSnapshotMode()) diff --git a/packages/llm/llm-pi-ai/README.i18n.yaml b/packages/llm/llm-pi-ai/README.i18n.yaml index 5ea2ba0ea1..200c601464 100644 --- a/packages/llm/llm-pi-ai/README.i18n.yaml +++ b/packages/llm/llm-pi-ai/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/llm/llm-pi-ai/README.md -README.md: bef10768bfa758a29781b6eae0bc4c52ada8eba0 -README.zh.md: 870cf78f485de57d50f8a4e70f821022b1686ddd +README.md: 9e534716f80e4246467de31ab43fb9a7c6381d28 +README.zh.md: 820b69fe6d390aee530ed2da3f5c67f27311e8de diff --git a/packages/llm/llm-pi-ai/README.md b/packages/llm/llm-pi-ai/README.md index bef10768bf..9e534716f8 100644 --- a/packages/llm/llm-pi-ai/README.md +++ b/packages/llm/llm-pi-ai/README.md @@ -114,7 +114,7 @@ The plugin answers "which models can this provider serve?" for a route a configu A route pi-ai does not ship needs `api`, `baseURL`, and a non-empty `models` list; an unserviceable profile is refused where it is written, naming the route and model. Failures carry stable codes: a credential that cannot be used fails with `INVALID_CREDENTIAL` naming the route and reference, a route whose `apiKeyEnv` reference resolves to nothing fails with `MISSING_CREDENTIAL`, an unconfigured model fails with `UNKNOWN_MODEL`, and terminal provider failures distinguish `QUOTA` from transient `RATE_LIMIT`. `GenerateOptions.stop` is rejected with `UNSUPPORTED_OPTION` because pi-ai's common streaming UI cannot guarantee it across providers. -Settings writes strictly validate each new or changed provider after merging its composition and user layers. During namespace registration, stored catalog failures retain the namespace and provider rows, with the first diagnostic in `LlmConfigurableProvider.error`; unchanged failed providers do not block edits elsewhere. Serviceable models remain selectable, while unresolved models remain in the editable configuration and fail with `INVALID_CONFIG` before network I/O if requested directly. Repairing or deleting the offending configuration clears its diagnostic. Schema and self-contained profile errors still reject loading. Later external edits validate changed providers and retain the last accepted section on failure. +Settings writes strictly validate each new or changed provider after merging its composition and user layers. During namespace registration, stored catalog failures retain the namespace and provider rows, with the first available model diagnostic or route failure in `LlmConfigurableProvider.error`; unchanged failed providers do not block edits elsewhere. Serviceable models remain selectable, while unresolved models remain in the editable configuration and fail with `INVALID_CONFIG` before network I/O if requested directly. Repairing or deleting the offending configuration clears its diagnostic. Schema and self-contained profile errors still reject loading. Later external edits validate changed providers and retain the last accepted section on failure. Changing `displayName`, `apiKeyEnv`, or `baseURL` without resolving the provider's model errors still rejects the save. For example, renaming an OpenRouter route whose model `111` needs an `api` cannot be saved on its own: repair or remove that model in the same editor draft, then save the complete provider configuration. Intermediate repairs remain in the draft until the whole provider validates; other providers can be saved independently. diff --git a/packages/llm/llm-pi-ai/README.zh.md b/packages/llm/llm-pi-ai/README.zh.md index 870cf78f48..820b69fe6d 100644 --- a/packages/llm/llm-pi-ai/README.zh.md +++ b/packages/llm/llm-pi-ai/README.zh.md @@ -114,7 +114,7 @@ profile 通过可选 settings seam 每次操作重新读取:base 与用户的 pi-ai 不提供的路由需要 `api`、`baseURL` 与非空 `models` 列表;无法服务的 profile 会在写入处被拒绝,并点名路由与模型。失败携带稳定 code:无法使用的凭据以 `INVALID_CREDENTIAL` 失败并点名路由与引用,`apiKeyEnv` 引用解析为空的路由以 `MISSING_CREDENTIAL` 失败,未配置模型以 `UNKNOWN_MODEL` 失败,终止性提供方失败则区分 `QUOTA` 与暂时性 `RATE_LIMIT`。`GenerateOptions.stop` 以 `UNSUPPORTED_OPTION` 被拒绝,因为 pi-ai 的通用流式 UI 无法跨提供方保证它。 -Settings 写入会在合并组合层与用户层后严格校验每个新增或修改的提供方。命名空间注册时,已存储配置的目录解析错误会保留命名空间与提供方行,并通过 `LlmConfigurableProvider.error` 返回首个诊断;未修改的错误提供方不会阻止其他编辑。可解析的模型仍可选择,无法解析的模型保留在可编辑配置中,直接请求时会在网络 I/O 前以 `INVALID_CONFIG` 失败。修复或删除错误配置会清除诊断。Schema 与 profile 自身的约束错误仍会拒绝加载。后续外部文件编辑会校验变化的提供方,失败时保留最后一次接受的分节。 +Settings 写入会在合并组合层与用户层后严格校验每个新增或修改的提供方。命名空间注册时,已存储配置的目录解析错误会保留命名空间与提供方行,并通过 `LlmConfigurableProvider.error` 优先返回首个模型诊断,无模型诊断时返回路由错误;未修改的错误提供方不会阻止其他编辑。可解析的模型仍可选择,无法解析的模型保留在可编辑配置中,直接请求时会在网络 I/O 前以 `INVALID_CONFIG` 失败。修复或删除错误配置会清除诊断。Schema 与 profile 自身的约束错误仍会拒绝加载。后续外部文件编辑会校验变化的提供方,失败时保留最后一次接受的分节。 只修改 `displayName`、`apiKeyEnv` 或 `baseURL` 而未解决提供方的模型配置错误时,保存仍会被拒绝。例如,OpenRouter 路由的模型 `111` 缺少 `api` 时,不能单独保存路由名称的修改:需要在同一份编辑草稿中修复或删除该模型,再保存完整的提供方配置。中间修复状态保留在草稿中,直到整条提供方配置通过校验;其他提供方可以独立保存。 diff --git a/packages/llm/llm-pi-ai/src/config.ts b/packages/llm/llm-pi-ai/src/config.ts index 34516503a2..2f9e563008 100644 --- a/packages/llm/llm-pi-ai/src/config.ts +++ b/packages/llm/llm-pi-ai/src/config.ts @@ -205,7 +205,7 @@ export interface ResolvedPiAiProviderProfile * a stored route cannot be constructed; its configuration remains editable. */ piProvider?: Provider - /** First stored-catalog diagnostic for settings surfaces; absent for a serviceable route. */ + /** First model diagnostic, or the route failure when no model diagnostic is available. */ catalogError?: string /** Per-model failures reported before attempting a request. */ modelErrors: ReadonlyMap @@ -470,6 +470,7 @@ export function resolveProfiles( defaultContextWindow: source.defaultContextWindow ?? DEFAULT_CONTEXT_WINDOW, defaultMaxTokens: source.defaultMaxTokens ?? DEFAULT_MAX_TOKENS, }, validation) + catalogError = catalog.modelErrors.values().next().value piProvider = buildProvider({ provider, displayName, @@ -478,10 +479,9 @@ export function resolveProfiles( models: catalog.models, namesCredential: source.apiKeyEnv !== undefined, }) - catalogError = catalog.modelErrors.values().next().value } catch (error) { if (validation === 'strict' || !(error instanceof PiAiCatalogError)) throw error - catalogError = error.message + catalogError ??= error.message } const { apiKeyEnv, retryPolicy, models: _models, displayName: _displayName, ...rest } = source resolved.set(provider, { diff --git a/packages/llm/llm-pi-ai/tests/catalog.spec.ts b/packages/llm/llm-pi-ai/tests/catalog.spec.ts index 02644744d2..934c2faf64 100644 --- a/packages/llm/llm-pi-ai/tests/catalog.spec.ts +++ b/packages/llm/llm-pi-ai/tests/catalog.spec.ts @@ -323,6 +323,18 @@ describe('hand-declared providers', () => { })).toThrow(/needs a baseURL/) }) + it('retains the missing-api model diagnostic when a stored custom provider cannot be built', () => { + const profile = resolveProfiles({ + 'acme-gateway': { baseURL: 'https://acme.test', models: [{ id: '111' }] }, + }, 'deferred').get('acme-gateway')! + const failure = 'llm-pi-ai: provider "acme-gateway" model "111" needs an api; ' + + 'the installed catalog does not describe it, so set the route\'s api to the wire protocol its endpoint speaks' + + expect(profile.catalogError).toBe(failure) + expect(profile.modelErrors.get('111')).toBe(failure) + expect(profile.piProvider).toBeUndefined() + }) + it.each(['bedrock-converse-stream', 'google-vertex', 'azure-openai-responses', 'openai-codex-responses'])( 'refuses %s, whose authentication a profile cannot express', (api) => { From 1f1537914fb7605ecad13f0af5def9be3db4abd6 Mon Sep 17 00:00:00 2001 From: fz Date: Tue, 8 Sep 2026 19:48:21 +0800 Subject: [PATCH 13/58] feat(minimal): align persistent bash output with the one-shot shell contract The minimal compositions serve the same models that trained against a one-shot `bash` tool; three surface differences remained visible to them. - Every settled command now appends `[Command finished with exit code N]`, including exit code 0, instead of `[exit code: N]` only on failure. A timeout appends `[Command timed out or OOM]` after the partial output. - Trailing newlines are all dropped before the status trailer rather than only the last one. - The `bash` description in the `minimal` preset and the `sdk-minimal` bundle replaces the two fixed network lines with a single environment- dependent line; the pwsh description is unchanged. Tests, READMEs, the web minimal-preset schema snapshot, and the packaged Python minimal model-visible snapshot follow the new wording. The standalone `persistent-tools` SDK replay fixture keeps its own copy of the old description since it is a per-launch customization, not the profile. --- apps/cli/tests/web-agent-presets.e2e.ts | 3 +-- packages/bundle/sdk-minimal/cordis.patch.yml | 3 +-- .../presets/minimal/agent.cordis.yml | 3 +-- packages/shell/tool-bash-persistent/README.md | 4 ++-- .../shell/tool-bash-persistent/README.zh.md | 4 ++-- .../shell/tool-bash-persistent/src/index.ts | 11 +++++---- .../tests/loader-composition.spec.ts | 14 +++++++---- .../tool-bash-persistent/tests/tools.spec.ts | 23 ++++++++++--------- .../minimal/model-visible.json | 6 ++--- .../minimal-preset/tool-schemas.expected.json | 2 +- 10 files changed, 40 insertions(+), 33 deletions(-) diff --git a/apps/cli/tests/web-agent-presets.e2e.ts b/apps/cli/tests/web-agent-presets.e2e.ts index 75d5947be3..f83e781169 100644 --- a/apps/cli/tests/web-agent-presets.e2e.ts +++ b/apps/cli/tests/web-agent-presets.e2e.ts @@ -32,8 +32,7 @@ const INSTALL_ANCHOR = join(REPO_ROOT, 'apps/cli/package.json') const MINIMAL_PROMPT = 'You are a helpful software engineer assistant.' const MINIMAL_BASH_DESCRIPTION = `Run commands in a bash shell * When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped. -* You don't have access to the internet via this tool. -* You do have access to a mirror of common linux and python packages via apt and pip. +* Network access depends on the task environment. Prefer configured mirrors/proxies when they are available. * State is persistent across command calls and discussions with the user. * To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'. * Please avoid commands that may produce a very large amount of output. diff --git a/packages/bundle/sdk-minimal/cordis.patch.yml b/packages/bundle/sdk-minimal/cordis.patch.yml index ec6f12db77..c96f07d550 100644 --- a/packages/bundle/sdk-minimal/cordis.patch.yml +++ b/packages/bundle/sdk-minimal/cordis.patch.yml @@ -128,8 +128,7 @@ description: |- Run commands in a bash shell * When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped. - * You don't have access to the internet via this tool. - * You do have access to a mirror of common linux and python packages via apt and pip. + * Network access depends on the task environment. Prefer configured mirrors/proxies when they are available. * State is persistent across command calls and discussions with the user. * To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'. * Please avoid commands that may produce a very large amount of output. diff --git a/packages/preset/agent-presets/presets/minimal/agent.cordis.yml b/packages/preset/agent-presets/presets/minimal/agent.cordis.yml index 197807ed0e..549a73cba2 100644 --- a/packages/preset/agent-presets/presets/minimal/agent.cordis.yml +++ b/packages/preset/agent-presets/presets/minimal/agent.cordis.yml @@ -41,8 +41,7 @@ description: |- Run commands in a bash shell * When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped. - * You don't have access to the internet via this tool. - * You do have access to a mirror of common linux and python packages via apt and pip. + * Network access depends on the task environment. Prefer configured mirrors/proxies when they are available. * State is persistent across command calls and discussions with the user. * To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'. * Please avoid commands that may produce a very large amount of output. diff --git a/packages/shell/tool-bash-persistent/README.md b/packages/shell/tool-bash-persistent/README.md index 8aaae0f848..7976916b60 100644 --- a/packages/shell/tool-bash-persistent/README.md +++ b/packages/shell/tool-bash-persistent/README.md @@ -52,7 +52,7 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a ### What the agent can rely on -Commands share one shell per agent, so state persists until an `exit`, a timeout, or a reset — each of which closes the shell and tells the agent the next call starts from the workspace with a fresh directory and environment. Results exclude the private completion markers; a non-zero wrapped command appends `[exit code: N]`, and a shell that exits before reporting that status instead appends `[shell exited: code N]`, `[shell killed by signal: SIG]`, or `[shell exited]`, then resets. Long output keeps the earliest retained prefix plus a clipping notice; if the terminal has already dropped that prefix, the result says so explicitly rather than presenting a tail as complete output. +Commands share one shell per agent, so state persists until an `exit`, a timeout, or a reset — each of which closes the shell and tells the agent the next call starts from the workspace with a fresh directory and environment. Results exclude the private completion markers; every settled command appends `[Command finished with exit code N]`, and a shell that exits before reporting that status instead appends `[shell exited: code N]`, `[shell killed by signal: SIG]`, or `[shell exited]`, then resets. Long output keeps the earliest retained prefix plus a clipping notice; if the terminal has already dropped that prefix, the result says so explicitly rather than presenting a tail as complete output. ### What can go wrong @@ -126,7 +126,7 @@ Prefix-stable while the configured description and schema remain unchanged. #### What the model sees -Commands share one shell per Agent, so cwd, exported variables, activated environments, functions, and background jobs persist across calls. Results exclude private completion markers. When the shell reads stdin again without having printed the completion marker — after `exec`, an interrupt, or an interactive foreground child whose stdin wait the provider proves — the call returns the captured partial output, which can end with the backend's own prompt text. A nonzero wrapped command appends `[exit code: N]`; a shell that exits before reporting that status instead appends `[shell exited: code N]`, `[shell killed by signal: SIG]`, or `[shell exited]` when the backend supplies neither, then resets and tells the model that the next call starts fresh. Long output keeps the earliest retained prefix plus a clipping notice. If the PTY has already dropped that prefix, the result says so explicitly instead of presenting a tail as complete output. Timeout returns bounded partial output, closes the uncertain shell, and reports the reset. +Commands share one shell per Agent, so cwd, exported variables, activated environments, functions, and background jobs persist across calls. Results exclude private completion markers. When the shell reads stdin again without having printed the completion marker — after `exec`, an interrupt, or an interactive foreground child whose stdin wait the provider proves — the call returns the captured partial output, which can end with the backend's own prompt text. Every settled command appends `[Command finished with exit code N]`; a shell that exits before reporting that status instead appends `[shell exited: code N]`, `[shell killed by signal: SIG]`, or `[shell exited]` when the backend supplies neither, then resets and tells the model that the next call starts fresh. Long output keeps the earliest retained prefix plus a clipping notice. If the PTY has already dropped that prefix, the result says so explicitly instead of presenting a tail as complete output. Timeout returns bounded partial output followed by `[Command timed out or OOM]`, closes the uncertain shell, and reports the reset. #### Token effect diff --git a/packages/shell/tool-bash-persistent/README.zh.md b/packages/shell/tool-bash-persistent/README.zh.md index 6072985023..b2850979e0 100644 --- a/packages/shell/tool-bash-persistent/README.zh.md +++ b/packages/shell/tool-bash-persistent/README.zh.md @@ -52,7 +52,7 @@ kind: "package-reference" ### agent 可以依赖什么 -命令共享每个 agent 一个 shell,因此状态一直保留到 `exit`、超时或重置——每一种都会关闭 shell 并告诉 agent 下一次调用从工作区的新目录与环境开始。结果排除私有完成标记;非零的包装命令追加 `[exit code: N]`,而在报告该状态前就退出的 shell 改为追加 `[shell exited: code N]`、`[shell killed by signal: SIG]` 或 `[shell exited]`,然后重置。长输出保留最早的已保留前缀并附裁剪通知;若 terminal 已经丢弃该前缀,结果会明确说明,而不是把尾部当作完整输出呈现。 +命令共享每个 agent 一个 shell,因此状态一直保留到 `exit`、超时或重置——每一种都会关闭 shell 并告诉 agent 下一次调用从工作区的新目录与环境开始。结果排除私有完成标记;每条完成的命令都追加 `[Command finished with exit code N]`,而在报告该状态前就退出的 shell 改为追加 `[shell exited: code N]`、`[shell killed by signal: SIG]` 或 `[shell exited]`,然后重置。长输出保留最早的已保留前缀并附裁剪通知;若 terminal 已经丢弃该前缀,结果会明确说明,而不是把尾部当作完整输出呈现。 ### 可能出什么问题 @@ -126,7 +126,7 @@ kind: "package-reference" #### 模型看到什么 -命令共享每个 Agent 一个 shell,因此 cwd、导出的变量、已激活的环境、函数与后台任务都会跨调用保留。结果排除私有完成标记。当 shell 在没有打印完成标记的情况下再次读取 stdin——`exec`、中断,或提供方证明其 stdin 等待的交互式前台子进程之后——调用返回捕获的部分输出,它可能以后端自己的提示词文本结尾。非零的包装命令追加 `[exit code: N]`;在报告该状态前就退出的 shell 改为追加 `[shell exited: code N]`、`[shell killed by signal: SIG]`,或后端两者都未提供时的 `[shell exited]`,然后重置并告诉模型下一次调用从全新状态开始。长输出保留最早的已保留前缀并附裁剪通知。若 PTY 已经丢弃该前缀,结果会明确说明,而不是把尾部当作完整输出呈现。超时返回有界部分输出、关闭不确定的 shell 并报告重置。 +命令共享每个 Agent 一个 shell,因此 cwd、导出的变量、已激活的环境、函数与后台任务都会跨调用保留。结果排除私有完成标记。当 shell 在没有打印完成标记的情况下再次读取 stdin——`exec`、中断,或提供方证明其 stdin 等待的交互式前台子进程之后——调用返回捕获的部分输出,它可能以后端自己的提示词文本结尾。每条完成的命令都追加 `[Command finished with exit code N]`;在报告该状态前就退出的 shell 改为追加 `[shell exited: code N]`、`[shell killed by signal: SIG]`,或后端两者都未提供时的 `[shell exited]`,然后重置并告诉模型下一次调用从全新状态开始。长输出保留最早的已保留前缀并附裁剪通知。若 PTY 已经丢弃该前缀,结果会明确说明,而不是把尾部当作完整输出呈现。超时返回有界部分输出并追加 `[Command timed out or OOM]`、关闭不确定的 shell 并报告重置。 #### Token 影响 diff --git a/packages/shell/tool-bash-persistent/src/index.ts b/packages/shell/tool-bash-persistent/src/index.ts index f9cf8d764a..202449d964 100644 --- a/packages/shell/tool-bash-persistent/src/index.ts +++ b/packages/shell/tool-bash-persistent/src/index.ts @@ -15,6 +15,9 @@ import { defineTool } from '@deepseek-ai/dsh-tools' const TRUNCATED_MESSAGE = 'To save on context only part of this file has been shown to you. You should retry this tool after you have searched inside the file with `grep -n` in order to find the line numbers of what you are looking for.' const LOST_PREFIX_MESSAGE = 'The beginning of this command output was dropped by the terminal scrollback limit. The following text is the earliest retained output.\n' const SHELL_RESET_MESSAGE = 'The persistent bash shell was reset; the next bash call starts from the workspace with a fresh current directory and environment.' +// Status trailer for a command that never reported an exit code; settled +// commands append `[Command finished with exit code N]` instead (see renderCaptured). +const TIMEOUT_STATUS_MARKER = '[Command timed out or OOM]' const TIMEOUT_CODE = 'PERSISTENT_BASH_TIMEOUT' // One page is enough to find a just-emitted completion marker; the full // scrollback is assembled only when a command settles or needs partial output. @@ -82,7 +85,7 @@ function wrapCommand(command: string, marker: CommandMarkers): string { } function trimTrailingNewline(text: string): string { - return text.replace(/\r?\n$/, '') + return text.replace(/(?:\r?\n)+$/, '') } function commandOutput( @@ -162,8 +165,8 @@ function renderCaptured(output: CapturedOutput, maxOutputChars: number): string const withPrefix = output.incomplete && output.text.length > 0 ? LOST_PREFIX_MESSAGE + rendered : rendered - const marker = output.exitCode !== undefined && output.exitCode !== 0 - ? `[exit code: ${output.exitCode}]` + const marker = output.exitCode !== undefined + ? `[Command finished with exit code ${output.exitCode}]` : undefined return appendStatusMarker(withPrefix, marker) } @@ -345,7 +348,7 @@ async function executeCommand( return [ // TODO: Report a timeout only; this signal does not establish an OOM. `Your command timed out after ${Math.round(timedOut.timeoutMs / 1000)} seconds or experienced an OOM error. Below is partial output:`, - partial, + appendStatusMarker(partial, TIMEOUT_STATUS_MARKER), SHELL_RESET_MESSAGE, ].join('\n') } diff --git a/packages/shell/tool-bash-persistent/tests/loader-composition.spec.ts b/packages/shell/tool-bash-persistent/tests/loader-composition.spec.ts index 82895a008c..142d7c611c 100644 --- a/packages/shell/tool-bash-persistent/tests/loader-composition.spec.ts +++ b/packages/shell/tool-bash-persistent/tests/loader-composition.spec.ts @@ -149,20 +149,26 @@ suite('persistent Bash through a real cordis.yml Loader composition', () => { 'multiline', 'value="line one"\nprintf "%s:%s\\n" "$value" "it\'s fine"', )) - expect(multiline).toBe("line one:it's fine") + expect(multiline).toBe("line one:it's fine\n[Command finished with exit code 0]") expect(multiline).not.toContain('DSH_PERSISTENT_BASH') const heredoc = text(await execute( 'heredoc', "cat <<'EOF'\nalpha\nbeta\nEOF", )) - expect(heredoc).toBe('alpha\nbeta') + expect(heredoc).toBe('alpha\nbeta\n[Command finished with exit code 0]') const pipeline = text(await execute( 'pipeline', '{ sleep 0.1; printf "delayed\\n"; } | cat', )) - expect(pipeline).toBe('delayed') + expect(pipeline).toBe('delayed\n[Command finished with exit code 0]') + + // Every trailing newline is dropped before the status trailer. + const trailing = text(await execute('trailing-newlines', 'printf "tail\\n\\n\\n"')) + expect(trailing).toBe('tail\n[Command finished with exit code 0]') + expect(text(await execute('nonzero', 'exit_code() { return 3; }; exit_code'))) + .toBe('[Command finished with exit code 3]') const large = text(await execute('large-output', 'seq 1 12050')) expect(large.startsWith('1\n2\n3\n')).toBe(true) @@ -177,6 +183,6 @@ suite('persistent Bash through a real cordis.yml Loader composition', () => { const exited = text(await execute('exit', 'exit')) expect(exited).toContain('next bash call starts from the workspace') - expect(text(await execute('after-exit', 'printf "%s\\n" "$PWD"'))).toBe(root) + expect(text(await execute('after-exit', 'printf "%s\\n" "$PWD"'))).toBe(`${root}\n[Command finished with exit code 0]`) }, 20_000) }) diff --git a/packages/shell/tool-bash-persistent/tests/tools.spec.ts b/packages/shell/tool-bash-persistent/tests/tools.spec.ts index 3afbecfec3..7f3ed90c52 100644 --- a/packages/shell/tool-bash-persistent/tests/tools.spec.ts +++ b/packages/shell/tool-bash-persistent/tests/tools.spec.ts @@ -328,13 +328,13 @@ describe('tool-bash-persistent', () => { expect(ctx.tools.get('bash')?.presentCall?.({ command: 'pwd' })) .toEqual({ card: 'terminal', title: 'pwd' }) - expect(text(await call(ctx, owner, 'echo one'))).toBe('hello from stub') - expect(text(await call(ctx, owner, 'echo two'))).toBe('hello from stub') + expect(text(await call(ctx, owner, 'echo one'))).toBe('hello from stub\n[Command finished with exit code 0]') + expect(text(await call(ctx, owner, 'echo two'))).toBe('hello from stub\n[Command finished with exit code 0]') expect(stub.sessions).toHaveLength(1) expect(stub.sessions[0]?.sends).toBe(3) const ownerWithoutCwd = agent(ctx, undefined) - expect(text(await call(ctx, ownerWithoutCwd, 'pwd'))).toBe('hello from stub') + expect(text(await call(ctx, ownerWithoutCwd, 'pwd'))).toBe('hello from stub\n[Command finished with exit code 0]') expect(stub.sessions).toHaveLength(2) await fiber.dispose() @@ -377,13 +377,13 @@ describe('tool-bash-persistent', () => { expect(text(await call(ctx, owner, 'large'))).toContain('') session.mode = 'nonzero' - expect(text(await call(ctx, owner, 'false'))).toBe('[exit code: 7]') + expect(text(await call(ctx, owner, 'false'))).toBe('[Command finished with exit code 7]') session.mode = 'exit' const exited = text(await call(ctx, owner, 'exit')) expect(exited).toContain('hello from') expect(exited).toContain('[shell exited: code 9]') - expect(exited).not.toContain('[exit code: 9]') + expect(exited).not.toContain('[Command finished with exit code 9]') expect(exited).toContain('next bash call starts from the workspace') expect(session.closed).toContain('persistent bash shell exited') @@ -409,7 +409,7 @@ describe('tool-bash-persistent', () => { stub.sessions[0]!.mode = 'torn-status' stub.sessions[0]!.scrollback = '' - expect(text(await call(ctx, owner, 'torn status'))).toBe('hello from stub\n[exit code: 7]') + expect(text(await call(ctx, owner, 'torn status'))).toBe('hello from stub\n[Command finished with exit code 7]') }) it('reports the exit path when the shell exits between send settlement and the next poll', async () => { @@ -423,7 +423,7 @@ describe('tool-bash-persistent', () => { expect(result).toContain('next bash call starts from the workspace') expect(session.closed).toContain('persistent bash shell exited') - expect(text(await call(ctx, owner, 'echo "$PWD"'))).toBe('hello from stub') + expect(text(await call(ctx, owner, 'echo "$PWD"'))).toBe('hello from stub\n[Command finished with exit code 0]') expect(stub.sessions).toHaveLength(2) }) @@ -462,7 +462,7 @@ describe('tool-bash-persistent', () => { session.mode = 'paged-scrollback' session.scrollback = 'older one\nolder two\nolder three\nolder four\n' - expect(text(await call(ctx, owner, 'paged output'))).toBe('hello from stub') + expect(text(await call(ctx, owner, 'paged output'))).toBe('hello from stub\n[Command finished with exit code 0]') }) it('returns a stdin_read fallback reached after multiple polling rounds', async () => { @@ -484,7 +484,7 @@ describe('tool-bash-persistent', () => { await call(ctx, owner, 'warm up') stub.sessions[0]!.historyTruncated = true const result = text(await call(ctx, owner, 'short command')) - expect(result).toBe('hello from stub') + expect(result).toBe('hello from stub\n[Command finished with exit code 0]') expect(result).not.toContain('') expect(result).not.toContain('beginning of this command output was dropped') }) @@ -496,6 +496,7 @@ describe('tool-bash-persistent', () => { const result = await call(ctx, owner, 'hang') expect(text(result)).toContain('timed out after 0 seconds or experienced an OOM error') expect(text(result)).toContain('partial output') + expect(text(result)).toContain('[Command timed out or OOM]') expect(text(result)).toContain('next bash call starts from the workspace') expect(stub.sessions[0]?.closed).toContain('persistent bash command timed out') }) @@ -514,7 +515,7 @@ describe('tool-bash-persistent', () => { }, 5) expect((await cancelled).isError).toBe(true) - expect(text(await queued)).toBe('hello from stub') + expect(text(await queued)).toBe('hello from stub\n[Command finished with exit code 0]') expect(stub.sessions[0]?.closed).toContain('persistent bash command aborted') expect(stub.sessions).toHaveLength(2) }, @@ -541,7 +542,7 @@ describe('tool-bash-persistent', () => { stub.sessions[0]!.mode = 'send-error' expect((await call(ctx, owner, 'fails')).isError).toBe(true) expect(stub.sessions[0]?.closed).toContain('persistent bash send failed') - expect(text(await call(ctx, owner, 'recovers'))).toBe('hello from stub') + expect(text(await call(ctx, owner, 'recovers'))).toBe('hello from stub\n[Command finished with exit code 0]') expect(stub.sessions).toHaveLength(2) }) diff --git a/scripts/snapshots/python-sdk-single-exe/minimal/model-visible.json b/scripts/snapshots/python-sdk-single-exe/minimal/model-visible.json index 81bb5f3b77..6e8874b63a 100644 --- a/scripts/snapshots/python-sdk-single-exe/minimal/model-visible.json +++ b/scripts/snapshots/python-sdk-single-exe/minimal/model-visible.json @@ -5,7 +5,7 @@ "type": "function", "function": { "name": "bash", - "description": "Run commands in a bash shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* You don't have access to the internet via this tool.\n* You do have access to a mirror of common linux and python packages via apt and pip.\n* State is persistent across command calls and discussions with the user.\n* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.", + "description": "Run commands in a bash shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* Network access depends on the task environment. Prefer configured mirrors/proxies when they are available.\n* State is persistent across command calls and discussions with the user.\n* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.", "parameters": { "type": "object", "properties": { @@ -38,7 +38,7 @@ "type": "function", "function": { "name": "bash", - "description": "Run commands in a bash shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* You don't have access to the internet via this tool.\n* You do have access to a mirror of common linux and python packages via apt and pip.\n* State is persistent across command calls and discussions with the user.\n* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.", + "description": "Run commands in a bash shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* Network access depends on the task environment. Prefer configured mirrors/proxies when they are available.\n* State is persistent across command calls and discussions with the user.\n* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.", "parameters": { "type": "object", "properties": { @@ -85,7 +85,7 @@ "type": "function", "function": { "name": "bash", - "description": "Run commands in a bash shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* You don't have access to the internet via this tool.\n* You do have access to a mirror of common linux and python packages via apt and pip.\n* State is persistent across command calls and discussions with the user.\n* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.", + "description": "Run commands in a bash shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* Network access depends on the task environment. Prefer configured mirrors/proxies when they are available.\n* State is persistent across command calls and discussions with the user.\n* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.", "parameters": { "type": "object", "properties": { diff --git a/snapshots/web/minimal-preset/tool-schemas.expected.json b/snapshots/web/minimal-preset/tool-schemas.expected.json index 9b8ce846b2..c930b96681 100644 --- a/snapshots/web/minimal-preset/tool-schemas.expected.json +++ b/snapshots/web/minimal-preset/tool-schemas.expected.json @@ -2,7 +2,7 @@ "initial": [ { "name": "bash", - "description": "Run commands in a bash shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* You don't have access to the internet via this tool.\n* You do have access to a mirror of common linux and python packages via apt and pip.\n* State is persistent across command calls and discussions with the user.\n* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.", + "description": "Run commands in a bash shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* Network access depends on the task environment. Prefer configured mirrors/proxies when they are available.\n* State is persistent across command calls and discussions with the user.\n* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.", "parameters": { "type": "object", "properties": { From 2db4bdd31d5e92cdf4657bd7d709881215ad8943 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Mon, 7 Sep 2026 15:59:36 +0800 Subject: [PATCH 14/58] feat(subagent): record and observe parent-owned child catalogs --- ...01-parent-owned-subagent-catalog.i18n.yaml | 6 + ...026-09-01-parent-owned-subagent-catalog.md | 43 ++++ ...-09-01-parent-owned-subagent-catalog.zh.md | 43 ++++ docs/event-producer-consumer.i18n.yaml | 2 +- docs/event-producer-consumer.md | 8 +- docs/persistence-catalog.i18n.yaml | 4 +- docs/persistence-catalog.md | 14 ++ docs/persistence-catalog.zh.md | 14 ++ .../core/session/src/known-event-types.ts | 1 + .../extensions/tool-cordis/src/api-catalog.ts | 4 +- .../session-query/README.i18n.yaml | 4 +- .../session-query/session-query/README.md | 2 +- .../session-query/session-query/README.zh.md | 2 +- .../session-query/src/observation.ts | 58 +++++- .../session-query/tests/observation.spec.ts | 74 +++++++ packages/subagent/subagent/README.i18n.yaml | 4 +- packages/subagent/subagent/README.md | 2 + packages/subagent/subagent/README.zh.md | 2 + packages/subagent/subagent/src/catalog.ts | 186 ++++++++++++++++++ .../subagent/src/continuation-activation.ts | 1 - .../subagent/subagent/src/continuation.ts | 27 ++- packages/subagent/subagent/src/index.ts | 20 +- .../subagent/subagent/tests/catalog.spec.ts | 117 +++++++++++ .../subagent/tests/continuation.spec.ts | 63 +++++- .../subagent/subagent/tests/service.spec.ts | 50 ++++- .../session-snapshot/README.i18n.yaml | 4 +- .../test-support/session-snapshot/README.md | 2 +- .../session-snapshot/README.zh.md | 2 +- .../session-snapshot/src/index.ts | 1 + .../session-snapshot/src/normalize.ts | 78 +++++++- .../session-snapshot/tests/normalize.spec.ts | 89 ++++++++- python/sdk/tests/test_smoke_model.py | 21 ++ scripts/smoke-python-runtime.py | 4 + .../advanced/session.v2.jsonl | 12 +- .../session.v2.jsonl | 7 +- .../sdk/subagent-continuable/session.v2.jsonl | 19 +- .../subagent-fork-in-process/session.v2.jsonl | 7 +- .../sdk/subagent-list-agents/session.v2.jsonl | 13 +- snapshots/sdk/subagent-mixed/session.v2.jsonl | 12 +- .../subagent-send-message/session.v2.jsonl | 9 +- .../session.v2.jsonl | 5 +- .../session.v2.jsonl | 16 +- .../advanced-toolchain/session.v2.jsonl | 18 +- snapshots/session/headless.snapshot.ts | 4 + snapshots/session/ralph-loop/session.v2.jsonl | 6 +- .../session.v2.jsonl | 5 +- .../session.1.v2.jsonl | 5 +- .../session.v2.jsonl | 5 +- .../session.v2.jsonl | 5 +- .../subagent-parallel/session.v2.jsonl | 6 +- .../session.v2.jsonl | 5 +- .../session.v2.jsonl | 5 +- .../session/workflow-run/session.v2.jsonl | 5 +- 53 files changed, 1010 insertions(+), 111 deletions(-) create mode 100644 .agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml create mode 100644 .agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md create mode 100644 .agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md create mode 100644 packages/subagent/subagent/src/catalog.ts create mode 100644 packages/subagent/subagent/tests/catalog.spec.ts diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml new file mode 100644 index 0000000000..d85dec23a0 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md +2026-09-01-parent-owned-subagent-catalog.md: 7b2082bb6c8e046d3083ab95abffb739895d13b8 +2026-09-01-parent-owned-subagent-catalog.zh.md: 5f8413a3b88a747de6308b1220bb3139543ba83c diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md new file mode 100644 index 0000000000..7b2082bb6c --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md @@ -0,0 +1,43 @@ +# Agent Note: Parent-owned subagent catalog events + +Status: implemented + +English | [中文](2026-09-01-parent-owned-subagent-catalog.zh.md) + +## Problem + +Direct-child discovery once reconstructed a catalog from the global Session corpus and each selected child's log. Creation already knows the direct parent, child id, mode, and label, so repository-wide enumeration and child-log reads duplicated an owned fact and made browser refresh cost depend on unrelated Sessions. + +The child descriptor remains necessary for recovery and composition, but it cannot be the discovery source because a reader must find and open the child before it can read the descriptor. Forks add a separate requirement: a seeded copy of a parent log must not inherit the original Session's children. + +## Decision + +The parent Session's required `subagent/catalog` events are the persistent authority for direct-child discovery. Each event is one successful creation fact containing `childId`, `childCreatedAt`, mode, and the mode-discriminated label. Remote one-shot runs without a local Session remain outside this catalog. + +Creation publishes only successful facts. A one-shot run appends the catalog event after its provider returns a local child and before the run reaches its caller. A continuable run admits the initial prompt, appends the catalog event, then returns the child id. If admission or catalog append fails, creation fails and releases the activation; there is no compensating catalog event or rollback protocol. + +The child header and `subagent/descriptor` remain authoritative for recovery and composition. An Activation and the exact parent relationship remain authoritative for authorization and delivery. Mode and label are snapshotted once and the same detached values reach the parent catalog fact and child descriptor. + +The registered host-only `subagentCatalog` projection materializes the parent facts. It stores facts in a persistent stack of 64-entry chunks, so an append copies at most the head chunk in bounded O(1) work. Materialization visits D facts and sorts them by `childCreatedAt`, then child id, in O(D log D). A projection checkpoint clones the state once in O(D); projection-cache writes remain asynchronous and use the existing mandatory creation, turn-end, and disposal points. + +Fork isolation uses the exact `Session.inheritedEventCount` supplied to projection initialization. The fold ignores `subagent/catalog` events below that offset. The state stores the inherited offset but not each event seq because acceptance is decided during folding. + +Snapshot normalizers zero `childCreatedAt` because it originates from the process clock. The TypeScript normalizer also sorts adjacent catalog facts by distinct child id because parallel successful creations may append in either order. Non-catalog events remain ordering barriers. + +## Alternatives considered + +**A flat immutable array.** Appending with `[...facts, fact]` copies D facts, so creation is O(D). Mutating a shared array would violate projection state ownership and checkpoint safety. + +**A node-per-fact linked list.** It provides O(1) append and O(D) read, but persisted projection checkpoints form JSON nested D levels deep. Sixty-four-entry chunks preserve the asymptotic costs while reducing nesting. + +**A client-visible projection.** Publishing the complete catalog after every creation would turn constant-time folding into O(D²) cumulative materialization. The Session Controller already owns the observation needed for a cold read. + +**A durable SQLite child index.** An index would create another write path, reconciliation protocol, schema, and corruption surface for a fact already ordered in the parent Session log. + +**A compensating failure event.** Recording catalog membership before initial prompt admission requires a second operation, pairing rules, rollback cleanup, and client reconciliation. Delaying the success fact until admission completes removes that protocol. + +## Consequences + +A caller can request `projectionStateKeys: ['subagentCatalog']` from `observeSession`. Live observations clone the maintained registry state; cold observations hydrate their prepared Session and detach the same state at the observation cursor. Direct-child and descendant listing still use the Session corpus and child identity projection. + +Backends that do not know the required event refuse the log under the existing Session event mechanism. Pre-release format policy requires no fallback scan for old logs. diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md new file mode 100644 index 0000000000..5f8413a3b8 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md @@ -0,0 +1,43 @@ +# Agent Note: parent 自有的 subagent 目录事件 + +Status: implemented + +[English](2026-09-01-parent-owned-subagent-catalog.md) | 中文 + +## 问题 + +直接 child discovery 曾从全局 Session 语料与每个入选 child 的日志重建目录。创建过程已经知道直接 parent、child id、mode 与 label,因此仓库范围枚举和 child 日志读取重复推导了已有归属的事实,并让浏览器刷新成本取决于无关 Session。 + +child descriptor 对恢复与 composition 仍然必要,但它不能作为 discovery 来源,因为读取方必须先找到并打开 child 才能读取 descriptor。fork 还有独立要求:从 parent 日志播种的副本不能继承原 Session 的 child。 + +## 决策 + +parent Session 的 required `subagent/catalog` 事件是直接 child discovery 的持久化权威。每个事件都是一条成功创建事实,包含 `childId`、`childCreatedAt`、mode 与按 mode 区分的 label。没有本地 Session 的远程 one-shot run 不进入该目录。 + +创建只发布成功事实。one-shot run 在 provider 返回本地 child 后、run 到达调用方前追加目录事件。continuable run 先准入初始 prompt,再追加目录事件,最后返回 child id。准入或目录追加失败时,创建失败并释放 activation;不存在补偿目录事件或 rollback 协议。 + +child header 与 `subagent/descriptor` 继续拥有恢复与 composition 权威。Activation 与精确 parent 关系继续拥有授权与投递权威。mode 与 label 只快照一次,同一份分离值写入 parent catalog fact 与 child descriptor。 + +注册的 host-only `subagentCatalog` projection 物化 parent fact。它以每块 64 项的持久 stack 保存事实,因此 append 最多复制 head chunk,以有界 O(1) 工作完成。materialization 访问 D 条事实,再按 `childCreatedAt` 与 child id 以 O(D log D) 排序。projection checkpoint 以 O(D) 克隆 state;projection-cache 继续异步写入,并使用既有创建、turn-end 与 disposal 强制点。 + +fork 隔离使用 projection 初始化时提供的精确 `Session.inheritedEventCount`。fold 忽略该 offset 之前的 `subagent/catalog` 事件。state 保存 inherited offset,但不保存每条 event seq,因为接受判定已在 fold 时完成。 + +snapshot normalizer 会把 `childCreatedAt` 归零,因为它来自 process clock。TypeScript normalizer 还会按不同 child id 排序相邻 catalog fact,因为并行成功创建的 append 顺序可能不同。非 catalog event 继续作为顺序 barrier。 + +## 考虑过的替代方案 + +**扁平不可变数组。** 用 `[...facts, fact]` append 会复制 D 个 fact,因此创建是 O(D)。修改共享数组会违反 projection state ownership 与 checkpoint 安全。 + +**每 fact 一个 node 的 linked list。** 它提供 O(1) append 与 O(D) read,但持久 projection checkpoint 会形成 D 层 JSON 嵌套。每块 64 项保留渐进复杂度,同时降低嵌套深度。 + +**client-visible projection。** 每次创建后发布完整 catalog 会把常数时间 fold 变成 O(D²) 累积 materialization。Session Controller 已经拥有 cold read 所需的 observation。 + +**持久 SQLite child index。** index 会为 parent Session 日志中已有顺序的 fact 增加另一套写路径、reconciliation protocol、schema 与 corruption surface。 + +**补偿失败事件。** 在初始 prompt 准入前记录 catalog membership 会引入第二种 operation、配对规则、rollback 清理与 client reconciliation。把成功事实推迟到准入完成后即可删除该协议。 + +## 后果 + +调用方可以向 `observeSession` 请求 `projectionStateKeys: ['subagentCatalog']`。实时观察克隆 registry 维护中的 state;冷观察 hydrate 已准备的 Session,并在观察 cursor 处分离出同一 state。直接子级和后代列表仍使用 Session 语料库与子级身份 projection。 + +不认识该 required event 的 backend 会按既有 Session event 机制拒绝日志。pre-release format policy 不要求为旧日志保留 fallback scan。 diff --git a/docs/event-producer-consumer.i18n.yaml b/docs/event-producer-consumer.i18n.yaml index f0492f3a9f..1e314eaacc 100644 --- a/docs/event-producer-consumer.i18n.yaml +++ b/docs/event-producer-consumer.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/event-producer-consumer.md -event-producer-consumer.md: 7c33981cb58d4bad7c1b410fcf61d4649f6322c1 +event-producer-consumer.md: 81b6fc83f0c3922df402106789c5788b4c0a6b3e event-producer-consumer.zh.md: d857561a7d9dbdf4c4d971a1854d43d3ecc4fd2a diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 7c33981cb5..81b6fc83f0 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -55,10 +55,10 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:105`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `remotes` | | `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:92`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) | | `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:298`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - | -| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:168`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), `server`, [`subagent`](../packages/subagent/subagent) | -| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:142`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:148`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:159`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`subagent`](../packages/subagent/subagent) | +| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:169`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), `server`, [`subagent`](../packages/subagent/subagent) | +| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:143`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:149`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:160`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`subagent`](../packages/subagent/subagent) | | `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`session-reference`](../packages/context/session-reference), [`system-prompt`](../packages/core/system-prompt) | | `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - | | `tools/change` | `emit` | [`packages/core/tools/src/index.ts:199`](../packages/core/tools/src/index.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`), [`tools`](../packages/core/tools) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index 1e60882ff3..ab3b610505 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-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/persistence-catalog.md -persistence-catalog.md: b239204810f92d866d309214678a22901e6f3ef6 -persistence-catalog.zh.md: 86d81a5a64322c98ea288c0aa0289550153eedf0 +persistence-catalog.md: 1859ca37bec3ba06033a209103d4471eaa3fa3c7 +persistence-catalog.zh.md: d967b46c9e60df10a60d4599640b58f5e8b97bf5 diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index b239204810..1859ca37be 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -761,6 +761,20 @@ Source: [`packages/core/session/src/types.ts:287`](../packages/core/session/src/ ### `subagent/*` + + +#### `subagent/catalog` — log-only + +```ts persistence-catalog +/** + * A direct child's complete discovery fact. + * @param data - versioned parent-owned catalog entry. + */ +'subagent/catalog': SubagentCatalogEvent +``` + +Source: [`packages/subagent/subagent/src/catalog.ts:37`](../packages/subagent/subagent/src/catalog.ts) + #### `subagent/descriptor` — log-only diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index 86d81a5a64..d967b46c9e 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -763,6 +763,20 @@ export type SessionEvent = { ### `subagent/*` + + +#### `subagent/catalog` — log-only + +```ts persistence-catalog +/** + * A direct child's complete discovery fact. + * @param data - versioned parent-owned catalog entry. + */ +'subagent/catalog': SubagentCatalogEvent +``` + +来源:[`packages/subagent/subagent/src/catalog.ts:37`](../packages/subagent/subagent/src/catalog.ts) + #### `subagent/descriptor` — log-only diff --git a/packages/core/session/src/known-event-types.ts b/packages/core/session/src/known-event-types.ts index b21b7ec576..decb838cb5 100644 --- a/packages/core/session/src/known-event-types.ts +++ b/packages/core/session/src/known-event-types.ts @@ -54,6 +54,7 @@ export const KNOWN_SESSION_EVENT_TYPES: ReadonlySet = new Set([ 'session/title-llm-request', 'step/end', 'step/start', + 'subagent/catalog', 'subagent/descriptor', 'subagent/model-selection-policy', 'system/message', diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index f74e1d9e51..de254c190b 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -5213,11 +5213,11 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SessionObservation', - declaration: 'export interface SessionObservation extends Disposable {\n readonly source: \'live\' | \'prepared\';\n readonly header: SessionHeader;\n readonly inheritedEventCount: SessionLogOffsetType;\n readonly events: readonly SessionEvent[];\n readonly cursor: SessionSeqCursor;\n readonly revision?: SessionPersistenceRevision;\n readonly projections?: ProjectionSnapshot;\n retain(): SessionObservation;\n}', + declaration: 'export interface SessionObservation extends Disposable {\n readonly source: \'live\' | \'prepared\';\n readonly header: SessionHeader;\n readonly inheritedEventCount: SessionLogOffsetType;\n readonly events: readonly SessionEvent[];\n readonly cursor: SessionSeqCursor;\n readonly revision?: SessionPersistenceRevision;\n readonly projections?: ProjectionSnapshot;\n readonly projectionStates?: Partial;\n retain(): SessionObservation;\n}', }, { name: 'SessionObservationOptions', - declaration: 'export interface SessionObservationOptions {\n readonly signal?: AbortSignal;\n readonly projectionMode?: \'all\' | \'none\';\n}', + declaration: 'export interface SessionObservationOptions {\n readonly signal?: AbortSignal;\n readonly projectionMode?: \'all\' | \'none\';\n readonly projectionStateKeys?: readonly Extract[];\n}', }, { name: 'SessionOpenWorkspacePathRequest', diff --git a/packages/session-query/session-query/README.i18n.yaml b/packages/session-query/session-query/README.i18n.yaml index 122ec31432..e5ac8d1d9b 100644 --- a/packages/session-query/session-query/README.i18n.yaml +++ b/packages/session-query/session-query/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session-query/session-query/README.md -README.md: cbd7379b290397101ab3178e9d742c87a4afab08 -README.zh.md: 02bf708e4e8dc0104f5f031c8b963f29a5675b75 +README.md: 52c6870fed7f39c2e2c5a49c1aa43c7e498488ea +README.zh.md: e062d0a2a37252378ae32431d8ae8d1a264ec200 diff --git a/packages/session-query/session-query/README.md b/packages/session-query/session-query/README.md index cbd7379b29..52c6870fed 100644 --- a/packages/session-query/session-query/README.md +++ b/packages/session-query/session-query/README.md @@ -108,7 +108,7 @@ The decision history lives in the [unified service decision](../../../.agents/no ### Observation cache -`observeSession` builds point observations without a listing preflight. A live observation fixes its cut as the current log length and materializes `events` on first read, so header-, cursor-, or projection-only consumers never copy the log; the log only appends, so a late first read still yields exactly that prefix. The cold path stats the stored session first and consults an own bounded cache keyed by the persistence instance and the `stat` revision: an unchanged revision reuses the restored unpublished Session without re-reading the log; a changed revision, or a replaced persistence instance, reloads through the handle seam and replaces the entry. The cache holds `preparedSessionCacheSize` entries with least-recently-used eviction, entries pinned by active observation leases are never evicted, and a session that goes live mid-read retries the live path. +`observeSession` builds point observations without a listing preflight. A live observation fixes its cut as the current log length and materializes `events` on first read, so header-, cursor-, or projection-only consumers never copy the log; the log only appends, so a late first read still yields exactly that prefix. The cold path stats the stored session first and consults an own bounded cache keyed by the persistence instance and the `stat` revision: an unchanged revision reuses the restored unpublished Session without re-reading the log; a changed revision, or a replaced persistence instance, reloads through the handle seam and replaces the entry. The cache holds `preparedSessionCacheSize` entries with least-recently-used eviction, entries pinned by active observation leases are never evicted, and a session that goes live mid-read retries the live path. `projectionStateKeys` detaches named host projection states at the same cut independently of `projectionMode`: a live observation clones the registry's maintained cells, and a cold observation hydrates its prepared Session's cells once so states and any views share one cut; unregistered keys are omitted and an absent registry leaves `projectionStates` undefined. ### Reads and traces diff --git a/packages/session-query/session-query/README.zh.md b/packages/session-query/session-query/README.zh.md index 02bf708e4e..e062d0a2a3 100644 --- a/packages/session-query/session-query/README.zh.md +++ b/packages/session-query/session-query/README.zh.md @@ -108,7 +108,7 @@ kind: "package-reference" ### 观察缓存 -`observeSession` 不经过列表预检直接构建定点观察。实时观察以当前日志长度固定 cut,并在首次读取时才物化 `events`,因此只需要 header、cursor 或 projection 的消费者永远不会复制日志;日志只会追加,所以延后的首次读取得到的仍然正好是该前缀。冷路径先对存储会话执行 `stat`,再查询自有的有界缓存,缓存键为持久化实例加 `stat` 修订:修订未变则复用已恢复的未发布 Session,不再重读日志;修订变化或持久化实例被替换则经 handle 缝重新加载并替换条目。缓存保留 `preparedSessionCacheSize` 个条目并按最久未用淘汰,被活跃观察租约钉住的条目从不被淘汰;读取中途转为实时的会话会重试实时路径。 +`observeSession` 不经过列表预检直接构建定点观察。实时观察以当前日志长度固定 cut,并在首次读取时才物化 `events`,因此只需要 header、cursor 或 projection 的消费者永远不会复制日志;日志只会追加,所以延后的首次读取得到的仍然正好是该前缀。冷路径先对存储会话执行 `stat`,再查询自有的有界缓存,缓存键为持久化实例加 `stat` 修订:修订未变则复用已恢复的未发布 Session,不再重读日志;修订变化或持久化实例被替换则经 handle 缝重新加载并替换条目。缓存保留 `preparedSessionCacheSize` 个条目并按最久未用淘汰,被活跃观察租约钉住的条目从不被淘汰;读取中途转为实时的会话会重试实时路径。 `projectionStateKeys` 在同一 cut 上分离出指定的 host projection state,与 `projectionMode` 无关:实时观察克隆 registry 维护中的 cell,冷观察则一次性 hydrate 其已准备 Session 的 cell,使 state 与任何 view 共享同一 cut;未注册的 key 被省略,registry 缺失时 `projectionStates` 为 undefined。 ### 读取与追踪 diff --git a/packages/session-query/session-query/src/observation.ts b/packages/session-query/session-query/src/observation.ts index 7ef86cebd4..7ba5951710 100644 --- a/packages/session-query/session-query/src/observation.ts +++ b/packages/session-query/session-query/src/observation.ts @@ -9,6 +9,7 @@ import type { SessionPersistenceSnapshot, } from '@deepseek-ai/dsh-session-persistence' import type { ProjectionSnapshot } from '@deepseek-ai/dsh-session-projection' +import type { SessionProjectionStateMap } from '@deepseek-ai/dsh-session-projection/types' import type {} from '@deepseek-ai/dsh-session-projection-cache' import { SESSION_QUERY_DEFAULT_PREPARED_SESSION_CACHE_SIZE, SessionQueryError } from './config.ts' import { readColdSessionLog, type ColdSessionLog } from './cold-read.ts' @@ -33,6 +34,11 @@ export interface SessionObservation extends Disposable { readonly revision?: SessionPersistenceRevision /** Exact projection baseline at {@link cursor}, when the registry is mounted. */ readonly projections?: ProjectionSnapshot + /** + * Requested host projection states detached at {@link cursor}; present only + * when the registry is mounted, and contains only the registered keys. + */ + readonly projectionStates?: Partial /** * Retain the same immutable cut for another Host owner. * @returns an independently disposable lease over this observation. @@ -46,6 +52,12 @@ export interface SessionObservationOptions { readonly signal?: AbortSignal /** Whether to compute every projection or leave projection state untouched. */ readonly projectionMode?: 'all' | 'none' + /** + * Host projection states to detach from the same exact cut, independent of + * {@link projectionMode}. A cold observation hydrates its registry cells to + * serve them. + */ + readonly projectionStateKeys?: readonly Extract[] } /** @@ -98,23 +110,25 @@ export class SessionObservationReader { sessionId: SessionId, options: SessionObservationOptions = {}, ): Promise { - const { signal, projectionMode = 'all' } = options + const { signal, projectionMode = 'all', projectionStateKeys } = options for (;;) { throwIfObservationAborted(signal) const live = this.ctx.sessions.get(sessionId) - if (live !== undefined) return this.live(live, projectionMode) + if (live !== undefined) return this.live(live, projectionMode, projectionStateKeys) const persistence = this.ctx.get('sessionPersistence') if (persistence === undefined) throw notFound(sessionId) const snapshot = await this.statSource(persistence, sessionId, signal) const attachedDuringStat = this.ctx.sessions.get(sessionId) - if (attachedDuringStat !== undefined) return this.live(attachedDuringStat, projectionMode) + if (attachedDuringStat !== undefined) { + return this.live(attachedDuringStat, projectionMode, projectionStateKeys) + } let entry = this.cachedEntry(persistence, sessionId, snapshot.revision) if (entry === undefined) { const loaded = await this.loadSource(persistence, sessionId, signal) throwIfObservationAborted(signal) const attached = this.ctx.sessions.get(sessionId) - if (attached !== undefined) return this.live(attached, projectionMode) + if (attached !== undefined) return this.live(attached, projectionMode, projectionStateKeys) // The handle marks persisted events as adoptable; synthetic closers // are owned by this read, so the combined seed needs no copy. const seed = loaded.events @@ -148,8 +162,15 @@ export class SessionObservationReader { } let projections: ProjectionSnapshot | undefined + let projectionStates: Partial | undefined try { - projections = projectionMode === 'none' ? undefined : this.preparedProjections(entry) + if (projectionMode === 'all' || projectionStateKeys !== undefined) { + // Hydration installs every unit's cell on the prepared Session, so a + // requested host state reads the same cut the views were taken from. + const snapshot = this.preparedProjections(entry) + projections = projectionMode === 'all' ? snapshot : undefined + projectionStates = this.projectionStates(entry.session, projectionStateKeys) + } } catch (error: unknown) { throw new SessionQueryError( `failed to project session "${sessionId}": ${errorMessage(error)}`, @@ -157,7 +178,7 @@ export class SessionObservationReader { { cause: error }, ) } - return this.preparedLease(sessionId, entry, projections) + return this.preparedLease(sessionId, entry, projections, projectionStates) } } @@ -243,6 +264,7 @@ export class SessionObservationReader { sessionId: SessionId, entry: PreparedEntry, projections: ProjectionSnapshot | undefined, + projectionStates: Partial | undefined, ): SessionObservation { entry.refs += 1 const lease = (): SessionObservation => { @@ -255,6 +277,7 @@ export class SessionObservationReader { cursor: entry.events.at(-1)?.seq ?? -1, revision: entry.revision, ...projections === undefined ? {} : { projections }, + ...projectionStates === undefined ? {} : { projectionStates }, retain: () => { if (disposed) throw new Error(`session observation "${sessionId}" is disposed`) entry.refs += 1 @@ -274,6 +297,7 @@ export class SessionObservationReader { private live( session: Session, projectionMode: NonNullable, + projectionStateKeys: SessionObservationOptions['projectionStateKeys'], ): SessionObservation { // The cut is the log length now. The log only appends, so the prefix // below `seq` is the same array whenever a consumer first reads `events`. @@ -282,6 +306,7 @@ export class SessionObservationReader { const projections = projectionMode === 'none' ? undefined : this.ctx.get('sessionProjections')?.snapshot(session) + const projectionStates = this.projectionStates(session, projectionStateKeys) const lease = (): SessionObservation => { let disposed = false return { @@ -294,6 +319,7 @@ export class SessionObservationReader { }, cursor: seq === 0 ? -1 : SessionSeq(seq - 1), ...projections === undefined ? {} : { projections }, + ...projectionStates === undefined ? {} : { projectionStates }, retain: () => { if (disposed) throw new Error(`session observation "${session.id}" is disposed`) return lease() @@ -312,6 +338,26 @@ export class SessionObservationReader { ? registry.hydrate(entry.session, {}, entry.events, SessionLogOffset(0)) : cache.hydratePrepared(entry.session, entry.events) } + + /** + * Detach the requested host states from the registry cells already at this + * Session's cut. Registry cells are live references that later drives + * advance, so each state is cloned into the immutable observation. + */ + private projectionStates( + session: Session, + keys: SessionObservationOptions['projectionStateKeys'], + ): Partial | undefined { + if (keys === undefined) return undefined + const registry = this.ctx.get('sessionProjections') + if (registry === undefined) return undefined + const states: Record = {} + for (const key of keys) { + const state = registry.stateOf(session, key) + if (state !== undefined) states[key] = structuredClone(state) + } + return states + } } function throwIfObservationAborted(signal: AbortSignal | undefined): void { diff --git a/packages/session-query/session-query/tests/observation.spec.ts b/packages/session-query/session-query/tests/observation.spec.ts index 8e2879ca14..106f3453e1 100644 --- a/packages/session-query/session-query/tests/observation.spec.ts +++ b/packages/session-query/session-query/tests/observation.spec.ts @@ -17,6 +17,7 @@ import type { SessionPersistenceStatOptions, } from '@deepseek-ai/dsh-session-persistence' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' +import { titleProjectionDefinition } from '@deepseek-ai/dsh-session-title' import { describe, expect, it, vi } from 'vitest' import { SessionObservationReader } from '../src/observation.ts' @@ -36,6 +37,15 @@ function messageEvent(seq: number, text: string): SessionEvent { } } +function titleEvent(seq: number, title: string): SessionEvent { + return { + type: 'session/title', + seq: SessionSeq(seq), + time: seq + 1, + data: { title, messageSeqs: [], source: { kind: 'user' } }, + } +} + /** A log whose writer crashed mid-turn: read-only viewing must balance it in memory. */ function interruptedLog(text: string): SessionEvent[] { return [ @@ -786,3 +796,67 @@ describe('SessionObservationReader cold projections', () => { await ctx.fiber.dispose() }) }) + +describe('SessionObservationReader host projection states', () => { + it('detaches a live registry state at the observed cut without computing views', async () => { + const ctx = await readerContext() + await ctx.plugin(SessionProjectionRegistry) + ctx.sessionProjections.register(titleProjectionDefinition) + const session = ctx.sessions.create(SessionId('live-host-state')) + session.append('session/title', { title: 'Live title', messageSeqs: [], source: { kind: 'user' } }) + const reader = new SessionObservationReader(ctx) + + using observed = await reader.read(session.id, { projectionMode: 'none', projectionStateKeys: ['title'] }) + session.append('session/title', { title: 'Later title', messageSeqs: [], source: { kind: 'user' } }) + using later = await reader.read(session.id, { projectionMode: 'none', projectionStateKeys: ['title'] }) + + expect(observed.projections).toBeUndefined() + expect(observed.projectionStates).toEqual({ title: 'Live title' }) + expect(later.projectionStates).toEqual({ title: 'Later title' }) + await ctx.fiber.dispose() + }) + + it('hydrates a prepared Session once and serves the requested state from its cells', async () => { + const ctx = await readerContext() + await ctx.plugin(SessionProjectionRegistry) + ctx.sessionProjections.register(titleProjectionDefinition) + const meta = header('cold-host-state') + const store = new Map([[meta.id, { header: meta, events: [titleEvent(0, 'Cold title')], revision: 'r1' }]]) + ctx.provide('sessionPersistence', stubPersistence(store, { stat: 0, open: 0, read: 0 })) + const hydrate = vi.spyOn(ctx.sessionProjections, 'hydrate') + + using observed = await new SessionObservationReader(ctx).read(meta.id, { + projectionMode: 'none', + projectionStateKeys: ['title'], + }) + + expect(observed.source).toBe('prepared') + expect(observed.projections).toBeUndefined() + expect(observed.projectionStates).toEqual({ title: 'Cold title' }) + expect(hydrate).toHaveBeenCalledOnce() + await ctx.fiber.dispose() + }) + + it('omits unregistered keys and the whole map when no registry is mounted', async () => { + const withRegistry = await readerContext() + await withRegistry.plugin(SessionProjectionRegistry) + const registered = withRegistry.sessions.create(SessionId('unregistered-key')) + using partial = await new SessionObservationReader(withRegistry).read(registered.id, { + projectionMode: 'none', + projectionStateKeys: ['title'], + }) + expect(partial.projectionStates).toEqual({}) + await withRegistry.fiber.dispose() + + const withoutRegistry = await readerContext() + const meta = header('no-registry') + const store = new Map([[meta.id, { header: meta, events: [titleEvent(0, 'Cold title')], revision: 'r1' }]]) + withoutRegistry.provide('sessionPersistence', stubPersistence(store, { stat: 0, open: 0, read: 0 })) + using absent = await new SessionObservationReader(withoutRegistry).read(meta.id, { + projectionMode: 'none', + projectionStateKeys: ['title'], + }) + expect(absent.projectionStates).toBeUndefined() + await withoutRegistry.fiber.dispose() + }) +}) diff --git a/packages/subagent/subagent/README.i18n.yaml b/packages/subagent/subagent/README.i18n.yaml index f9edd9b52a..7ff5a6a616 100644 --- a/packages/subagent/subagent/README.i18n.yaml +++ b/packages/subagent/subagent/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/subagent/subagent/README.md -README.md: 6b58f36fcd4d6e8a198ddc86e80ccc28491a0a34 -README.zh.md: be99e7c91f13c6d83a6375047ec5f21d55b21053 +README.md: 1b57a43bbd565e46bd738fe94e0d324fd1310684 +README.zh.md: f3e49584f3b40984d616ddc6f269d0adacc424de diff --git a/packages/subagent/subagent/README.md b/packages/subagent/subagent/README.md index 6b58f36fcd..1b57a43bbd 100644 --- a/packages/subagent/subagent/README.md +++ b/packages/subagent/subagent/README.md @@ -96,6 +96,8 @@ A request is validated against the provider's advertised capabilities, a durable The manager reserves a child identity, resolves the durable descriptor, creates (or cold-resumes) the child Agent, installs it in an Activation, and submits the prompt. Model-authored messages cross one parent/child edge through fixed Steer scheduling; browser human prompts choose Queue or best-effort Steer through an internal adapter, while other host protocols may retain Queue for distinct turns. A Session queue command admits a live subagent-owned Agent only from its own continuable descriptor. Settlement waits for Agent activity to finish, an empty Inbox, and no owned children, then flushes final Session state with admission open. Under the child lock, the manager revalidates the wake generation, Session sequence, Inbox, and owned children; the synchronous task entry of `Agent.runMaintenance()` claims the idle phase and closes the private subagent Inbox in the same JavaScript turn before handle disposal. An absent direct-child Activation cold-resumes from the persisted session. When a resident Activation settles, the manager tells the child's direct parent in the parent's own turn stream. +Successful local child creation appends a `subagent/catalog` fact to the parent Session. One-shot creation records it after the provider returns; continuable creation records it after initial inbox admission and before returning the child id. Failure releases the child without publishing a compensating catalog event. The host-only `subagentCatalog` projection excludes fork-inherited facts and can be read through an observation's requested projection state. [The parent-catalog decision](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md) owns ordering, persistence costs, and alternatives. + ### Ownership and invariants - **Publication is the boundary** — before it the provider owns the setup and must roll back on failure; after it the caller owns the run and must dispose it. diff --git a/packages/subagent/subagent/README.zh.md b/packages/subagent/subagent/README.zh.md index be99e7c91f..f3e49584f3 100644 --- a/packages/subagent/subagent/README.zh.md +++ b/packages/subagent/subagent/README.zh.md @@ -96,6 +96,8 @@ kind: "package-reference" 管理器预留 child 身份、解析持久化描述符、创建(或冷恢复)child、把它安装进 Activation 并提交提示词。模型编写的消息通过固定 Steer 调度跨一条 parent/child 边;浏览器人类 prompt 通过内部适配器选择 Queue 或 best-effort Steer,其他 host 协议仍可保留 Queue 以创建独立轮次。Session queue command 仅根据 child 自身的 continuable descriptor 准入在线 subagent-owned Agent。Settlement 会等待 Agent 活动结束、Inbox 为空且没有所拥有子级,再在准入开放时 flush 最终 Session 状态。管理器随后在 child lock 内重新验证 wake generation、Session 序号、Inbox 与所拥有子级;`Agent.runMaintenance()` 的同步 task 入口会占用 idle 阶段,并在同一个 JavaScript turn 内关闭私有 subagent Inbox,然后才 dispose handle。直接 child 不存在 Activation 时会从持久化会话冷恢复。当驻留 Activation 结算时,管理器会在 parent 自身的轮次流中告知该 child 的直接 parent。 +本地子级创建成功时,父 Session 追加一条 `subagent/catalog` 事实。一次性创建在 provider 返回后记录;可继续创建在初始 inbox 准入后、返回子级 id 前记录。失败会释放子级,不发布补偿性目录事件。仅 host 使用的 `subagentCatalog` projection 排除 fork 继承的事实,并可通过观察请求的 projection state 读取。[父目录决策](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md) 说明排序、持久化成本和替代方案。 + ### 所有权与不变式 - **发布即边界**——发布前提供方拥有设置并须在失败时回滚;发布后调用方拥有运行并须 dispose(资源释放)它。 diff --git a/packages/subagent/subagent/src/catalog.ts b/packages/subagent/subagent/src/catalog.ts new file mode 100644 index 0000000000..1ea5f86d9c --- /dev/null +++ b/packages/subagent/subagent/src/catalog.ts @@ -0,0 +1,186 @@ +/** + * Parent-owned durable subagent catalog events and their chunked projection. + * + * @module @deepseek-ai/dsh-subagent/catalog + */ + +import { z } from 'zod' +import type { + Session, + SessionEvent, + SessionHeader, + SessionId, + SessionLogOffset, +} from '@deepseek-ai/dsh-session' +import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection' + +/** Current payload version for `subagent/catalog` events. */ +export const SUBAGENT_CATALOG_VERSION = 0 + +/** One complete parent-owned catalog fact. */ +export type SubagentCatalogEvent = + & { + readonly version: 0 + readonly childId: SessionId + readonly childCreatedAt: number + } & ( + | { readonly mode: 'one-shot'; readonly label?: string } + | { readonly mode: 'continuable'; readonly label: string } + ) + +declare module '@deepseek-ai/dsh-session/types' { + interface SessionEventMap { + /** + * A direct child's complete discovery fact. + * @param data - versioned parent-owned catalog entry. + */ + 'subagent/catalog': SubagentCatalogEvent + } +} + +/** One current direct-child discovery row materialized from parent facts. */ +export type SubagentCatalogEntry = + & { + readonly id: SessionId + readonly createdAt: number + } + & ( + | { readonly mode: 'one-shot'; readonly label?: string } + | { readonly mode: 'continuable'; readonly label: string } + ) + +/** A fixed-size persistent stack node; newest facts occupy the head chunk. */ +interface CatalogChunk { + readonly facts: readonly SubagentCatalogEvent[] + readonly previous?: CatalogChunk | undefined +} + +/** + * Host fold state for one parent catalog. A cold reader detaches it from a + * Session observation for direct-child materialization. + */ +export interface SubagentCatalogState { + readonly inheritedEventCount: SessionLogOffset + readonly head?: CatalogChunk | undefined +} + +const sessionIdSchema = z.string() as unknown as z.ZodType +const oneShotCatalogSchema = z.object({ + version: z.literal(SUBAGENT_CATALOG_VERSION), + childId: sessionIdSchema, + childCreatedAt: z.number().int().nonnegative(), + mode: z.literal('one-shot'), + label: z.string().optional(), +}).strict() +const continuableCatalogSchema = z.object({ + version: z.literal(SUBAGENT_CATALOG_VERSION), + childId: sessionIdSchema, + childCreatedAt: z.number().int().nonnegative(), + mode: z.literal('continuable'), + label: z.string(), +}).strict() +const eventDataSchema = z.union([ + oneShotCatalogSchema, + continuableCatalogSchema, +]) as unknown as z.ZodType +const chunkSchema: z.ZodType = z.lazy(() => z.object({ + facts: z.array(eventDataSchema).min(1).max(64), + previous: chunkSchema.optional(), +}).strict()) +const stateSchema: z.ZodType = z.object({ + inheritedEventCount: z.number().int().nonnegative() as unknown as z.ZodType, + head: chunkSchema.optional(), +}).strict() + +declare module '@deepseek-ai/dsh-session-projection/types' { + interface SessionProjectionStateMap { + subagentCatalog: SubagentCatalogState + } +} + +/** Append one fact to the persistent chunk stack in constant bounded work. */ +function appendFact(state: SubagentCatalogState, fact: SubagentCatalogEvent): SubagentCatalogState { + const head = state.head + if (head === undefined || head.facts.length === 64) { + return { ...state, head: { facts: [fact], ...head === undefined ? {} : { previous: head } } } + } + return { + ...state, + head: { + facts: [...head.facts, fact], + ...head.previous === undefined ? {} : { previous: head.previous }, + }, + } +} + +/** + * Materialize direct children from their parent's successful creation facts. + * @param state - parent catalog fold state. + * @returns current direct-child rows in creation order. + */ +export function subagentCatalogEntries(state: SubagentCatalogState): SubagentCatalogEntry[] { + const entries: SubagentCatalogEntry[] = [] + for (let chunk = state.head; chunk !== undefined; chunk = chunk.previous) { + for (const data of chunk.facts) { + entries.push(data.mode === 'one-shot' + ? { + id: data.childId, + createdAt: data.childCreatedAt, + mode: data.mode, + ...data.label === undefined ? {} : { label: data.label }, + } + : { + id: data.childId, + createdAt: data.childCreatedAt, + mode: data.mode, + label: data.label, + }) + } + } + entries.sort((left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id)) + return entries +} + +/** Parent-owned direct-child catalog projection. */ +export const subagentCatalogProjectionDefinition = { + key: 'subagentCatalog', + stateSchema, + init: (_header: SessionHeader, inheritedEventCount: SessionLogOffset) => ({ inheritedEventCount }), + apply: (state, event: SessionEvent) => { + if (event.type !== 'subagent/catalog' || event.seq < state.inheritedEventCount) return state + const parsed = eventDataSchema.safeParse(event.data) + if (!parsed.success) return state + return appendFact(state, parsed.data) + }, + stateVersion: 1, +} satisfies ProjectionDefinition<'subagentCatalog', SubagentCatalogState> + +/** + * Append a complete direct-child discovery fact to its parent Session. + * @param parent - durable direct parent receiving the discovery fact. + * @param child - established child's immutable Session metadata. + * @param descriptor - mode-discriminated creation label frozen with the child. + */ +export function establishCatalogChild( + parent: Session, + child: SessionHeader, + descriptor: + | { readonly mode: 'one-shot'; readonly label?: string } + | { readonly mode: 'continuable'; readonly label: string }, +): void { + parent.append('subagent/catalog', descriptor.mode === 'one-shot' + ? { + version: SUBAGENT_CATALOG_VERSION, + childId: child.id, + childCreatedAt: child.createdAt, + mode: descriptor.mode, + ...descriptor.label === undefined ? {} : { label: descriptor.label }, + } + : { + version: SUBAGENT_CATALOG_VERSION, + childId: child.id, + childCreatedAt: child.createdAt, + mode: descriptor.mode, + label: descriptor.label, + }) +} diff --git a/packages/subagent/subagent/src/continuation-activation.ts b/packages/subagent/subagent/src/continuation-activation.ts index 2579e4aa5b..df2b6da439 100644 --- a/packages/subagent/subagent/src/continuation-activation.ts +++ b/packages/subagent/subagent/src/continuation-activation.ts @@ -499,7 +499,6 @@ export class ContinuableActivationRegistry { } finally { this.wake(activation) } - activation.announced = true return message.id } diff --git a/packages/subagent/subagent/src/continuation.ts b/packages/subagent/subagent/src/continuation.ts index b5ffced75e..b86d5cd836 100644 --- a/packages/subagent/subagent/src/continuation.ts +++ b/packages/subagent/subagent/src/continuation.ts @@ -39,6 +39,7 @@ import { } from './continuation-messages.ts' import { assertSubagentMaxDepth } from './depth.ts' import { foldSubagentDescriptor, snapshotSubagentDescriptor } from './descriptor.ts' +import { establishCatalogChild } from './catalog.ts' import { SubagentError } from './error.ts' import { isAdjacentAgentSendMessageTool } from './internal.ts' import type { ActivationObserver } from './lifecycle.ts' @@ -170,13 +171,15 @@ export class SubagentContinuationManager { composition: { persona: request.persona, toolFilter: request.toolFilter }, signal: spec.signal, }) - return this.submitMaterialized( + const childHeader = activation.handle.agent.session.header + return await this.submitMaterialized( activation, isAdjacentAgentSendMessageTool(this.ctx.get('tools')?.get('send_message', activation.handle.agent)) ? withContinuableReturnGuidance(parent.id, request.prompt) : request.prompt, { source: { kind: 'user' }, signal: spec.signal, delivery: 'queue' }, parent, + () => { establishCatalogChild(parent.session, childHeader, descriptor) }, ) }) return { childId, messageId } @@ -307,7 +310,9 @@ export class SubagentContinuationManager { return undefined } } - return this.submitAdmitted(activation, content, options, parent) + const messageId = this.submitAdmitted(activation, content, options, parent) + activation.announced = true + return messageId }) /* v8 ignore start -- only a delivery that lost the disposal cutoff retries. */ if (live !== undefined) return live @@ -448,12 +453,13 @@ export class SubagentContinuationManager { return await this.submitMaterialized(activation, content, options, parent) } - /** Submit to a freshly materialized Activation or roll it back completely. */ + /** Admit a materialized child, commit its creation fact, and release it on failure. */ private async submitMaterialized( activation: Activation, content: ContentBlock[], options: ChildDeliveryOptions, parent: Agent, + commit?: () => void, ): Promise { try { if (contentHasImage(content)) { @@ -462,11 +468,18 @@ export class SubagentContinuationManager { throw new SubagentError(`subagent "${activation.childId}" is closing`, 'ACTIVATION_CLOSING') } } - return this.submitAdmitted(activation, content, options, parent) + const messageId = this.submitAdmitted(activation, content, options, parent) + commit?.() + activation.announced = true + return messageId } catch (error: unknown) { - /* v8 ignore next -- rollback disposal failures must not mask the - * pre-acceptance signal, drain, or lifecycle failure. */ - await this.activations.dispose(activation).catch(() => undefined) + try { + await this.activations.dispose(activation) + } catch (cleanupError: unknown) { + this.ctx.logger.warn( + `subagent continuation: disposal after admission or catalog append failure also failed: ${String(cleanupError)}`, + ) + } throw error } } diff --git a/packages/subagent/subagent/src/index.ts b/packages/subagent/subagent/src/index.ts index 88977cdcae..aa11318ec3 100644 --- a/packages/subagent/subagent/src/index.ts +++ b/packages/subagent/subagent/src/index.ts @@ -74,6 +74,7 @@ import { listChildren as listSubagentChildren, listDescendants as listSubagentDe import type { SubagentDescendantListEntry, SubagentListEntry } from './list-children.ts' import { snapshotSubagentDescriptor } from './descriptor.ts' import { subagentIdentityProjectionDefinition, subagentTimingProjectionDefinition } from './projection.ts' +import { establishCatalogChild, subagentCatalogProjectionDefinition } from './catalog.ts' import { deliverSubagentPrompt } from './internal.ts' export * from './out-of-process.ts' @@ -208,6 +209,7 @@ export class SubagentRuntime extends TypertRemoteService { }, 'subagents.continuationBinding()') }) ctx.inject(['sessionProjections'], (projectionCtx) => { + projectionCtx.sessionProjections.register(subagentCatalogProjectionDefinition) projectionCtx.sessionProjections.register(subagentTimingProjectionDefinition) projectionCtx.sessionProjections.register(subagentIdentityProjectionDefinition) }) @@ -559,7 +561,23 @@ export class SubagentRuntime extends TypertRemoteService { ...request.label !== undefined ? { label: request.label } : {}, }) const resolved: ResolvedSubagentStartRequest = { ...request, descriptor } - return observeRun(this.emitLifecycle, name, request.parent, await provider.start(resolved)) + const run = await provider.start(resolved) + const child = run.localAgent?.session + if (child !== undefined) { + try { + establishCatalogChild(request.parent.session, child.header, descriptor) + } catch (error: unknown) { + try { + await run.dispose() + } catch (cleanupError: unknown) { + this.ctx.logger.warn( + `subagent: disposal after catalog append failure also failed: ${String(cleanupError)}`, + ) + } + throw error + } + } + return observeRun(this.emitLifecycle, name, request.parent, run) } /** diff --git a/packages/subagent/subagent/tests/catalog.spec.ts b/packages/subagent/subagent/tests/catalog.spec.ts new file mode 100644 index 0000000000..9fb134f4b8 --- /dev/null +++ b/packages/subagent/subagent/tests/catalog.spec.ts @@ -0,0 +1,117 @@ +import { describe, expect, it } from 'vitest' +import { + SessionLogOffset, + SessionSeq, + SESSION_FORMAT_VERSION, + SessionId, +} from '@deepseek-ai/dsh-session' +import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session' +import { + subagentCatalogEntries, + subagentCatalogProjectionDefinition, +} from '../src/catalog.ts' +import type { SubagentCatalogState } from '../src/catalog.ts' + +const header: SessionHeader = { + version: SESSION_FORMAT_VERSION, + id: SessionId('catalog-parent'), + createdAt: 0, + isSeeded: false, +} + +/** Fold one event list through the pure definition, as the registry does for a log. */ +function fold(events: readonly SessionEvent[]): SubagentCatalogState { + let state = subagentCatalogProjectionDefinition.init(header, SessionLogOffset(0)) + for (const event of events) state = subagentCatalogProjectionDefinition.apply(state, event) + return state +} + +function fact( + seq: number, + childId: string, + childCreatedAt: number, + descriptor: + | { readonly mode: 'one-shot'; readonly label?: string } + | { readonly mode: 'continuable'; readonly label: string }, +): SessionEvent<'subagent/catalog'> { + return { + type: 'subagent/catalog', + seq: SessionSeq(seq), + time: 0, + data: { + version: 0, + childId: SessionId(childId), + childCreatedAt, + ...descriptor, + }, + } +} + +describe('subagent catalog projection', () => { + it('preserves earlier checkpoints across chunk rollover and restores every fact', () => { + const first = fold(Array.from({ length: 64 }, (_, i) => + fact(i, `child-${i}`, i, { mode: 'one-shot' }))) + const checkpoint = JSON.stringify(first) + let state = first + for (let i = 64; i < 130; i += 1) { + state = subagentCatalogProjectionDefinition.apply( + state, fact(i, `child-${i}`, i, { mode: 'one-shot' }), + ) + } + const restored = subagentCatalogProjectionDefinition.stateSchema.parse( + JSON.parse(JSON.stringify(state)), + ) + + expect(JSON.stringify(first)).toBe(checkpoint) + expect(subagentCatalogEntries(first)).toHaveLength(64) + expect(subagentCatalogEntries(restored).map(entry => entry.id)) + .toEqual(Array.from({ length: 130 }, (_, i) => SessionId(`child-${i}`))) + }) + + it('validates recursive chunk state and materializes every label variant in stable order', () => { + const events: SessionEvent[] = [ + fact(0, 'child-b', 1, { mode: 'one-shot' }), + fact(1, 'child-a', 1, { mode: 'one-shot', label: 'once' }), + { + type: 'subagent/catalog', + seq: SessionSeq(2), + time: 0, + data: { + version: 0, + childId: SessionId('child-c'), + childCreatedAt: 2, + mode: 'continuable', + }, + } as unknown as SessionEvent, + fact(3, 'child-d', 3, { mode: 'continuable', label: 'later' }), + ] + const state = fold(events) + + expect(subagentCatalogProjectionDefinition.stateSchema.parse(JSON.parse(JSON.stringify(state)))) + .toEqual(state) + expect(subagentCatalogEntries(state)).toEqual([ + { id: SessionId('child-a'), createdAt: 1, mode: 'one-shot', label: 'once' }, + { id: SessionId('child-b'), createdAt: 1, mode: 'one-shot' }, + { id: SessionId('child-d'), createdAt: 3, mode: 'continuable', label: 'later' }, + ]) + }) + + it('ignores unrelated, inherited, and malformed events without changing state', () => { + const seededHeader = { ...header, isSeeded: true } + const initial = subagentCatalogProjectionDefinition.init(seededHeader, SessionLogOffset(2)) + const unrelated = subagentCatalogProjectionDefinition.apply(initial, { + type: 'turn/start', seq: SessionSeq(2), time: 0, data: { turn: 1 }, + }) + const inherited = subagentCatalogProjectionDefinition.apply( + unrelated, + fact(1, 'inherited', 1, { mode: 'one-shot' }), + ) + const malformed = subagentCatalogProjectionDefinition.apply(inherited, { + type: 'subagent/catalog', seq: SessionSeq(2), time: 0, data: { version: 9 }, + } as unknown as SessionEvent) + + expect(unrelated).toBe(initial) + expect(inherited).toBe(initial) + expect(malformed).toBe(initial) + }) +}) diff --git a/packages/subagent/subagent/tests/continuation.spec.ts b/packages/subagent/subagent/tests/continuation.spec.ts index 298183cdc1..f2bbd16ef5 100644 --- a/packages/subagent/subagent/tests/continuation.spec.ts +++ b/packages/subagent/subagent/tests/continuation.spec.ts @@ -7,7 +7,7 @@ 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 { SessionId } from '@deepseek-ai/dsh-session' -import type { SessionEvent } from '@deepseek-ai/dsh-session' +import type { SessionEvent, SessionEventMap } from '@deepseek-ai/dsh-session' import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' import * as toolSchedule from '@deepseek-ai/dsh-schedule' import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process' @@ -421,6 +421,67 @@ describe('SubagentRuntime.startContinuable', () => { }) }) + it('releases the Activation when the parent catalog append fails', async () => { + const { ctx, parent } = await setup([textResponse('unused')]) + const childId = SessionId('00000000-0000-4000-8000-000000000321') + const catalogFailure = new Error('catalog append failed') + const cleanupFailure = new Error('activation disposal also failed') + const appendCatalog = parent.session.append.bind(parent.session) as ( + type: 'subagent/catalog', + data: SessionEventMap['subagent/catalog'], + ) => SessionEvent<'subagent/catalog'> + vi.spyOn(parent.session, 'append').mockImplementation(((type: string, data: unknown) => { + if (type === 'subagent/catalog') { + const activation = continuationActivations(ctx).get(childId) + if (activation === undefined) throw new Error('expected live Activation') + const dispose = activation.handle.dispose.bind(activation.handle) + vi.spyOn(activation.handle, 'dispose').mockImplementation(async () => { + await dispose() + throw cleanupFailure + }) + throw catalogFailure + } + return appendCatalog('subagent/catalog', data as SessionEventMap['subagent/catalog']) + }) as typeof parent.session.append) + + await expect(ctx.subagents.startContinuable({ + ...startSpec(parent), + childId, + })).rejects.toBe(catalogFailure) + + await vi.waitFor(() => { expect(ctx.agents.get(childId)).toBeUndefined() }) + expect(parent.session.snapshotEvents().filter(event => event.type === 'subagent/catalog')).toEqual([]) + }) + + it('preserves prompt admission failure when Activation disposal also fails', async () => { + const { ctx, parent } = await setup([textResponse('unused')]) + const childId = SessionId('00000000-0000-4000-8000-000000000322') + const admissionFailure = new Error('prompt admission failed') + const cleanupFailure = new Error('activation disposal also failed') + const controller = new AbortController() + const warnings: string[] = [] + ctx.logger.warn = (message: string) => { warnings.push(message) } + ctx.on('subagent/start', () => { + const activation = continuationActivations(ctx).get(childId) + if (activation === undefined) throw new Error('expected live Activation') + const dispose = activation.handle.dispose.bind(activation.handle) + vi.spyOn(activation.handle, 'dispose').mockImplementation(async () => { + await dispose() + throw cleanupFailure + }) + controller.abort(admissionFailure) + }) + + await expect(ctx.subagents.startContinuable({ + ...startSpec(parent, 'spawn', controller.signal), + childId, + })).rejects.toBe(admissionFailure) + await vi.waitFor(() => { expect(ctx.agents.get(childId)).toBeUndefined() }) + expect(warnings.some(warning => warning.startsWith( + 'subagent continuation: disposal after admission or catalog append failure also failed:', + ))).toBe(true) + }) + it('rolls an unpublished Activation back when lifecycle publication fails', async () => { const { ctx, parent } = await setup([textResponse('unused')]) const ends: SubagentRunEndInfo[] = [] diff --git a/packages/subagent/subagent/tests/service.spec.ts b/packages/subagent/subagent/tests/service.spec.ts index e74c684405..899f34eb9b 100644 --- a/packages/subagent/subagent/tests/service.spec.ts +++ b/packages/subagent/subagent/tests/service.spec.ts @@ -18,7 +18,7 @@ import SubagentRuntime, { type SubagentRunEndInfo, type SubagentStartRequest, } from '@deepseek-ai/dsh-subagent' -import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session' +import { Session, SessionId, type SessionEvent } from '@deepseek-ai/dsh-session' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' function fakeParent(id = 'parent-1'): Agent { @@ -73,6 +73,24 @@ async function service(): Promise<{ ctx: Context; subagents: SubagentRuntime }> } describe('SubagentRuntime', () => { + it('releases its catalog projection binding with the service fiber', async () => { + const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) + const fiber = await ctx.plugin(SubagentRuntime) + const parent = Session.create(SessionId('catalog-parent')) + parent.append('subagent/catalog', { + version: 0, + childId: SessionId('catalog-child'), + childCreatedAt: 1, + mode: 'one-shot', + }) + expect(ctx.sessionProjections.stateOf(parent, 'subagentCatalog')?.head?.facts).toHaveLength(1) + + await fiber.dispose() + + expect(ctx.sessionProjections.stateOf(parent, 'subagentCatalog')).toBeUndefined() + }) + it('registers, lists, looks up, starts, and removes providers', async () => { const { ctx, subagents } = await service() const added: string[] = [] @@ -252,6 +270,36 @@ describe('SubagentRuntime', () => { expect(lifecycle).not.toHaveBeenCalled() }) + it('quiesces a local run when the parent catalog append fails', async () => { + const { subagents } = await service() + const parentSession = Session.create(SessionId('catalog-parent')) + const childSession = Session.create(SessionId('catalog-child')) + const parent = { id: parentSession.id, session: parentSession } as Agent + const localAgent = { id: childSession.id, session: childSession } as Agent + const cleanupFailure = new Error('dispose also failed') + const dispose = vi.fn().mockRejectedValue(cleanupFailure) + subagents.registerProvider({ + name: 'catalog-failure', + capabilities: NO_CAPS, + inheritsParentContext: false, + start: () => Promise.resolve({ + id: childSession.id, + localAgent, + result: Promise.resolve({ output: [], stopReason: 'completed' }), + dispose, + }), + }) + const catalogFailure = new Error('catalog unavailable') + const append = vi.spyOn(parentSession, 'append').mockImplementation(() => { + throw catalogFailure + }) + + await expect(subagents.start('catalog-failure', baseRequest({ parent }))) + .rejects.toBe(catalogFailure) + expect(append).toHaveBeenCalledOnce() + expect(dispose).toHaveBeenCalledOnce() + }) + it('emits an enriched end event and maps result rejection to error telemetry', async () => { const { ctx, subagents } = await service() const completed = new StubProvider('completed', NO_CAPS, { diff --git a/packages/test-support/session-snapshot/README.i18n.yaml b/packages/test-support/session-snapshot/README.i18n.yaml index 7eeb15e7c5..9f145f4ecb 100644 --- a/packages/test-support/session-snapshot/README.i18n.yaml +++ b/packages/test-support/session-snapshot/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/test-support/session-snapshot/README.md -README.md: 533baf73e4913d1c64ea018e7c99099da1a1348b -README.zh.md: c3926c3e7d7ec53064a5d3dac3048e0d934b4c5c +README.md: 55580faef36cd99b2179371e50b8a8ac680056c6 +README.zh.md: 9c23a09a75e3ba37305a3209098039623011a0f3 diff --git a/packages/test-support/session-snapshot/README.md b/packages/test-support/session-snapshot/README.md index 533baf73e4..55580faef3 100644 --- a/packages/test-support/session-snapshot/README.md +++ b/packages/test-support/session-snapshot/README.md @@ -70,7 +70,7 @@ defineAcpSnapshotSuite({ Each recorded-session directory carries a closed `snapshot.yml` manifest plus canonical parent and contiguous child roles. Parent filenames are `session[.vN].jsonl`; children are `session.[.vN].jsonl`; v0 omits `.v0`, positive versions use lowercase `.vN`, and every filename agrees with its header. A role may retain older generations, but the harness selects the numerically highest one. An owning manifest may declare `sessionFormat.version` plus one or more closed `coverage` names to keep that historical generation as an explicit migration fixture; absence tracks the current writer. The manifest also names the scenario, shipped profile, composition/header class, recording source, and only the replay, platform, permission, environment, workspace, or input facts the completed Session cannot reconstruct. Storage guards check tool results and portable paths in every selected parent and child role. Prompt/schema scrubbing, message identity, and prompt-before-request ordering apply to current generations; retained predecessors keep their historical representation. The adapter registers expected-output, Session-log, and optional `workspace.expected/` comparisons; guards reject orphan directories, missing roles, noncanonical names, absolute paths, malformed manifests, and platform-specific separators. -`normalizeSessionSnapshot` retains the complete Session header and event payloads but omits top-level `seq`/`time` envelopes from committed fixtures after normalizing paths and scrubbing system-prompt text and tool schemas; it also normalizes embedded stream clocks and historical packed-row `seq0`/`time0` envelopes. Replay synthesizes the top-level envelopes in memory, while runtime persistence continues to write complete logs. Multi-session comparison validates expected and harvested logs through the strict build-static Session format catalog before identity redaction and normalization; source filenames cannot alter format validation. A retained historical replay input is not the native V3 writer-output oracle: structural migration preserves request meaning but can produce a different event layout. Normalization preserves unexpected request-header fields, including `system`, so regressions remain visible. Versionless protocol-adapter unit fixtures remain outside the released Session format corpus. Current v3 fixtures use one row per event; retained v0/v1 fixtures may use canonical packed rows. The [temporary repository migrator](../../../scripts/migrate-packed-session-fixtures.ts) (`pnpm run migrate:packed-session-fixtures`) rewrites older historical layouts, and its [removal proposal](../../../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.md) owns its deletion. +`normalizeSessionSnapshot` retains the complete Session header and event payloads but omits top-level `seq`/`time` envelopes from committed fixtures after normalizing paths and scrubbing system-prompt text and tool schemas; it also normalizes embedded stream clocks and historical packed-row `seq0`/`time0` envelopes, and catalog child-creation clocks. Replay synthesizes the top-level envelopes in memory, while runtime persistence continues to write complete logs. Multi-session comparison validates expected and harvested logs through the strict build-static Session format catalog before identity redaction and normalization; source filenames cannot alter format validation. A retained historical replay input is not the native V3 writer-output oracle: structural migration preserves request meaning but can produce a different event layout. Normalization preserves unexpected request-header fields, including `system`, so regressions remain visible. Versionless protocol-adapter unit fixtures remain outside the released Session format corpus. Current v3 fixtures use one row per event; retained v0/v1 fixtures may use canonical packed rows. The [temporary repository migrator](../../../scripts/migrate-packed-session-fixtures.ts) (`pnpm run migrate:packed-session-fixtures`) rewrites older historical layouts, and its [removal proposal](../../../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.md) owns its deletion. Spill scenarios save through the real local provider under a private temporary root. Their fixture adapter exposes fixed-length logical locators and maps only this run’s saved locators back to real files for retrieval, preserving preview budgets without writing into a shared logical path. Known snapshot spill paths normalize to stable locator tokens, including paths quoted inside JSON omission notices with JSON-escaped Windows separators. Refresh extraction preserves the matched serialized path spelling for literal replacement. Normalization changes only the locator: saved byte lengths and omission counts remain comparison evidence. diff --git a/packages/test-support/session-snapshot/README.zh.md b/packages/test-support/session-snapshot/README.zh.md index c3926c3e7d..9c23a09a75 100644 --- a/packages/test-support/session-snapshot/README.zh.md +++ b/packages/test-support/session-snapshot/README.zh.md @@ -70,7 +70,7 @@ defineAcpSnapshotSuite({ 每个已记录 Session 目录携带封闭的 `snapshot.yml` manifest,以及规范 parent 与连续 child 角色。parent 文件名是 `session[.vN].jsonl`;child 是 `session.[.vN].jsonl`;v0 省略 `.v0`,正版本使用小写 `.vN`,且每个文件名与其 header 一致。一个角色可以保留旧 generation,但 harness 会选择数值最高的一项。拥有 fixture 的 manifest 可以声明 `sessionFormat.version` 与一个或多个封闭 `coverage` 名称,把该历史 generation 保留为显式迁移 fixture;省略此字段时跟随当前 writer。manifest 还会指名场景、随附 profile、组合/header 类别、录制来源,以及已完成 Session 无法重建的 replay、平台、权限、环境、workspace 或输入事实。存储保护检查每个选定 parent 与 child 角色的工具结果和可移植路径。提示词/schema 擦除、消息身份及提示词先于请求的顺序检查适用于当前 generation;保留的前代维持其历史表示。适配器注册预期输出、Session 日志与可选 `workspace.expected/` 比较;保护会拒绝遗留目录、缺失角色、非规范名称、绝对路径、malformed manifest 与平台专用分隔符。 -`normalizeSessionSnapshot` 在规范化路径并擦除系统提示文本与工具 schema 后,会保留完整 Session header 与事件 payload,但从已提交 fixture 中省略顶层 `seq`/`time` envelope;它还会规范化嵌入式 stream clock 与历史 packed-row 的 `seq0`/`time0` envelope。Replay 只在内存中合成顶层 envelope,而运行时持久化仍写入完整日志。多 Session 比较会先通过严格的构建期静态 Session 格式目录校验预期日志与收集日志,再进行身份脱敏与规范化;来源文件名不能改变格式校验。保留的历史 replay 输入不是原生 V3 writer 输出的比较基准:结构迁移保留请求含义,但可以产生不同的事件布局。归一化保留意外的 request-header 字段(包括 `system`),使回归保持可见。无版本的协议适配器单元测试 fixture 不属于已发布 Session 格式语料。当前 v3 fixture 每个事件占一行;保留的 v0/v1 fixture 可以使用规范 packed row。[临时仓库迁移器](../../../scripts/migrate-packed-session-fixtures.ts)(`pnpm run migrate:packed-session-fixtures`)会改写更旧的历史布局,由其[移除提案](../../../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.zh.md)负责删除该迁移器。 +`normalizeSessionSnapshot` 在规范化路径并擦除系统提示文本与工具 schema 后,会保留完整 Session header 与事件 payload,但从已提交 fixture 中省略顶层 `seq`/`time` envelope;它还会规范化嵌入式 stream clock 与历史 packed-row 的 `seq0`/`time0` envelope 与 catalog child 创建时钟。Replay 只在内存中合成顶层 envelope,而运行时持久化仍写入完整日志。多 Session 比较会先通过严格的构建期静态 Session 格式目录校验预期日志与收集日志,再进行身份脱敏与规范化;来源文件名不能改变格式校验。保留的历史 replay 输入不是原生 V3 writer 输出的比较基准:结构迁移保留请求含义,但可以产生不同的事件布局。归一化保留意外的 request-header 字段(包括 `system`),使回归保持可见。无版本的协议适配器单元测试 fixture 不属于已发布 Session 格式语料。当前 v3 fixture 每个事件占一行;保留的 v0/v1 fixture 可以使用规范 packed row。[临时仓库迁移器](../../../scripts/migrate-packed-session-fixtures.ts)(`pnpm run migrate:packed-session-fixtures`)会改写更旧的历史布局,由其[移除提案](../../../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.zh.md)负责删除该迁移器。 Spill 场景通过真实本地 provider 保存到私有临时根目录。夹具适配器提供固定长度的逻辑定位符,并仅将本次运行已保存的定位符映射回实际文件以供检索,在不写入共享逻辑路径的情况下保留预览预算。已知的快照 spill 路径会规范化为稳定的定位信息 token,包括 JSON 省略通知中带引号、使用 JSON 转义 Windows 分隔符的路径。刷新提取会保留匹配路径的序列化写法,以便进行字面替换。规范化只改变定位信息:保存字节数与省略计数仍作为比较证据。 diff --git a/packages/test-support/session-snapshot/src/index.ts b/packages/test-support/session-snapshot/src/index.ts index 1b53cccfe2..eb4d900a74 100644 --- a/packages/test-support/session-snapshot/src/index.ts +++ b/packages/test-support/session-snapshot/src/index.ts @@ -44,6 +44,7 @@ export { normalizeSessionSnapshot, normalizeSessionSnapshots, normalizeStdout, + omitSubagentCatalogForHistoricalComparison, scrubModelRequestBulk, scrubSessionSnapshot, scrubSystemPrompts, diff --git a/packages/test-support/session-snapshot/src/normalize.ts b/packages/test-support/session-snapshot/src/normalize.ts index c0624918f3..2a4145fe80 100644 --- a/packages/test-support/session-snapshot/src/normalize.ts +++ b/packages/test-support/session-snapshot/src/normalize.ts @@ -33,6 +33,31 @@ function omitFixtureEnvelope(record: Record): void { delete record.time0 } +/** Return a catalog event's child id, or undefined for every ordering barrier. */ +function catalogChildId(event: { readonly type: string; readonly data?: unknown }): string | undefined { + if (event.type !== 'subagent/catalog' || event.data === null || typeof event.data !== 'object') return undefined + const data = event.data as { childId?: unknown } + return typeof data.childId === 'string' ? data.childId : undefined +} + +/** Sort commutative adjacent catalog facts without crossing another event. */ +function canonicalizeCatalogRuns(events: readonly T[]): T[] { + const canonical = [...events] + let start = 0 + while (start < canonical.length) { + if (catalogChildId(canonical[start] as T) === undefined) { + start += 1 + continue + } + let end = start + 1 + while (end < canonical.length && catalogChildId(canonical[end] as T) !== undefined) end += 1 + canonical.splice(start, end - start, ...canonical.slice(start, end).sort((left, right) => + (catalogChildId(left) as string).localeCompare(catalogChildId(right) as string))) + start = end + } + return canonical +} + /** A cwd-rooted path after volatile cwd replacement, through its last separator-delimited segment. */ const CWD_ROOTED_PATH_RE = /\{\{cwd\}\}(?:[\\/][^\s<>"'`]+)+/g const PATH_TAG_RE = /()([^<]*)(<\/path>)/g @@ -325,8 +350,9 @@ export function normalizeStdout( /** * Normalize a session JSONL log into a stable expected output: the header line's * volatile fields (`createdAt`, `id`, `cwd`) are zeroed/scrubbed; event, - * historical packed-row, embedded Assistant-stream, and goal lifecycle clocks - * are zeroed; and all volatile strings are scrubbed. Projected inputs remain + * historical packed-row, embedded Assistant-stream, goal lifecycle, and + * catalog child-creation clocks are zeroed; and all volatile strings are + * scrubbed. Projected inputs remain * projected. Packed `data.dt` gaps are normalized even when the projected row * omits its `time0` anchor. * Output is JSONL in the same shape as the input — one compact record per @@ -381,6 +407,10 @@ export function normalizeSessionLog( if ('createdAt' in data) data.createdAt = 0 if ('updatedAt' in data) data.updatedAt = 0 } + if (record.type === 'subagent/catalog' && record.data !== null && typeof record.data === 'object') { + const data = record.data as Record + if ('childCreatedAt' in data) data.childCreatedAt = 0 + } if (Object.hasOwn(record, 'sourceEventSeqs')) { record.sourceEventSeqs = decodeSeqRanges(record.sourceEventSeqs) } @@ -397,8 +427,10 @@ function projectSessionSnapshot(rawLog: string): string { const lines = rawLog.split('\n').filter(line => line.trim().length > 0) const header = lines.shift() as string - const body = lines.map((line) => { - const record = JSON.parse(line) as Record + const records = canonicalizeCatalogRuns(lines.map((line) => { + return JSON.parse(line) as Record & { type: string } + })) + const body = records.map((record) => { const projected = { ...record } omitFixtureEnvelope(projected) return JSON.stringify(projected) @@ -450,6 +482,44 @@ export function normalizeSessionSnapshots( )) } +/** + * Omit current subagent discovery facts when comparing a fresh run with an + * immutable historical Session generation. The remaining source-event + * references are rebased to the projected stream and refuse to cite an + * omitted catalog event. + * + * @param rawLog - normalized current-format snapshot JSONL. + * @returns the snapshot without `subagent/catalog` events. + */ +export function omitSubagentCatalogForHistoricalComparison(rawLog: string): string { + const lines = rawLog.split('\n').filter(line => line.trim().length > 0) + const header = lines.shift() + if (header === undefined) throw new Error('session snapshot must start with a session header') + const events = lines.map(line => JSON.parse(line) as Record) + const omitted = new Set(events.flatMap((event, seq) => event.type === 'subagent/catalog' ? [seq] : [])) + if (omitted.size === 0) return `${[header, ...lines].join('\n')}\n` + + const retained = events.flatMap((event, seq) => { + if (omitted.has(seq)) return [] + if (!Array.isArray(event.sourceEventSeqs)) return [event] + const sourceEventSeqs = event.sourceEventSeqs.map((source) => { + if (typeof source !== 'number' || !Number.isSafeInteger(source) || source < 0) { + throw new Error('normalized sourceEventSeqs must contain non-negative safe integers') + } + if (omitted.has(source)) { + throw new Error(`historical comparison cannot omit cited catalog event ${source}`) + } + let preceding = 0 + for (const omittedSeq of omitted) { + if (omittedSeq < source) preceding += 1 + } + return source - preceding + }) + return [{ ...event, sourceEventSeqs }] + }) + return [header, ...retained.map(event => JSON.stringify(event)), ''].join('\n') +} + /** * Omit the artifact header generation after official migration for comparison. * Delivery and captured-source generations retain their opaque recorded values. diff --git a/packages/test-support/session-snapshot/tests/normalize.spec.ts b/packages/test-support/session-snapshot/tests/normalize.spec.ts index ffd6d2f24e..cfc02aaeab 100644 --- a/packages/test-support/session-snapshot/tests/normalize.spec.ts +++ b/packages/test-support/session-snapshot/tests/normalize.spec.ts @@ -7,6 +7,7 @@ import { normalizeSessionSnapshot, normalizeSessionSnapshots, normalizeStdout, + omitSubagentCatalogForHistoricalComparison, scrubModelRequestBulk, scrubSessionSnapshot, scrubSystemPrompts, @@ -523,12 +524,35 @@ describe('normalizeSessionLog', () => { expect(out).toContain('"operation":"resume"') }) + it('normalizes subagent catalog child creation clocks', () => { + const catalog = JSON.stringify({ + type: 'subagent/catalog', + seq: 2, + time: 5, + data: { + version: 0, + childId: 'child', + childCreatedAt: 123, + mode: 'one-shot', + }, + }) + const out = normalizeSessionLog(`${header({})}\n${catalog}\n`, ctx) + expect(out).toContain('"childCreatedAt":0') + }) + it('handles complete envelopes when optional normalized fields are absent', () => { const bareHeader = JSON.stringify({ type: 'session', id: 's' }) const bareHook = JSON.stringify({ type: 'hook/result', seq: 2, time: 5, data: { decision: 'allow' } }) const nullDataHook = JSON.stringify({ type: 'hook/result', seq: 3, time: 6, data: null }) - const out = normalizeSessionLog(`${bareHeader}\n${bareHook}\n${nullDataHook}\n`, ctx) + const bareCatalog = JSON.stringify({ + type: 'subagent/catalog', + seq: 4, + time: 7, + data: { version: 0 }, + }) + const out = normalizeSessionLog(`${bareHeader}\n${bareHook}\n${nullDataHook}\n${bareCatalog}\n`, ctx) expect(out).toContain('"decision":"allow"') + expect(out).toContain('"version":0') expect(out).not.toContain('durationMs') }) }) @@ -597,6 +621,69 @@ describe('normalizeSessionSnapshot', () => { ].join('\n')) }) + it('orders adjacent catalog facts by child id', () => { + const raw = [ + JSON.stringify({ type: 'session', version: 0 }), + JSON.stringify({ type: 'tool/call', data: { callId: 'parallel' } }), + JSON.stringify({ + type: 'subagent/catalog', + data: { version: 0, childId: '{{session:3}}', childCreatedAt: 123, mode: 'one-shot' }, + }), + JSON.stringify({ + type: 'subagent/catalog', + data: { version: 0, childId: '{{session:2}}', childCreatedAt: 124, mode: 'one-shot' }, + }), + JSON.stringify({ type: 'tool/result', data: { callId: 'parallel' } }), + ].join('\n') + '\n' + const normalized = normalizeSessionSnapshot(raw, ctx) + expect(normalized.indexOf('{{session:2}}')).toBeLessThan(normalized.indexOf('{{session:3}}')) + expect(normalized).toContain('"childCreatedAt":0') + }) + + it('treats malformed catalog facts as ordering barriers', () => { + const raw = [ + JSON.stringify({ type: 'session', version: 0 }), + JSON.stringify({ + type: 'subagent/catalog', + data: { version: 0, childId: '{{session:2}}', childCreatedAt: 1, mode: 'one-shot' }, + }), + JSON.stringify({ + type: 'subagent/catalog', + data: { version: 0, childId: 3, childCreatedAt: 3, mode: 'one-shot' }, + }), + ].join('\n') + '\n' + const normalized = normalizeSessionSnapshot(raw, ctx) + expect(normalized).toContain('{{session:2}}') + expect(normalized).toContain('"childId":3') + }) + + it('omits current catalog facts and rebases source references for historical comparison', () => { + const normalized = [ + JSON.stringify({ type: 'session', id: '{{session:1}}' }), + JSON.stringify({ type: 'tool/call', data: { callId: 'first' } }), + JSON.stringify({ + type: 'subagent/catalog', + data: { version: 0, childId: '{{session:2}}', childCreatedAt: 0, mode: 'one-shot' }, + }), + JSON.stringify({ type: 'tool/result', data: { callId: 'first' }, sourceEventSeqs: [0] }), + JSON.stringify({ type: 'tool/call', data: { callId: 'second' } }), + JSON.stringify({ + type: 'subagent/catalog', + data: { version: 0, childId: '{{session:3}}', childCreatedAt: 0, mode: 'one-shot' }, + }), + JSON.stringify({ type: 'tool/result', data: { callId: 'second' }, sourceEventSeqs: [3] }), + '', + ].join('\n') + expect(omitSubagentCatalogForHistoricalComparison(normalized)).toBe([ + JSON.stringify({ type: 'session', id: '{{session:1}}' }), + JSON.stringify({ type: 'tool/call', data: { callId: 'first' } }), + JSON.stringify({ type: 'tool/result', data: { callId: 'first' }, sourceEventSeqs: [0] }), + JSON.stringify({ type: 'tool/call', data: { callId: 'second' } }), + JSON.stringify({ type: 'tool/result', data: { callId: 'second' }, sourceEventSeqs: [2] }), + '', + ].join('\n')) + }) + it('migrates and re-packs multi-session fixtures after relationship-preserving id redaction', () => { const raw = [ JSON.stringify({ type: 'session', version: 0, id: '{{session:1}}', createdAt: 0, delegationDepth: 0 }), diff --git a/python/sdk/tests/test_smoke_model.py b/python/sdk/tests/test_smoke_model.py index 37d584f76e..5b950f98e3 100644 --- a/python/sdk/tests/test_smoke_model.py +++ b/python/sdk/tests/test_smoke_model.py @@ -223,6 +223,27 @@ def test_mcp_smoke_accepts_the_external_server_result() -> None: ) +def test_advanced_snapshot_normalizes_catalog_child_creation_time() -> None: + value = { + "type": "subagent/catalog", + "data": { + "childCreatedAt": 1788246207176, + }, + } + + assert SMOKE["normalize_snapshot_value"](value, []) == { + "type": "subagent/catalog", + "data": { + "childCreatedAt": 0, + }, + } + + assert SMOKE["normalize_snapshot_value"]( + {"type": "fixture/event", "data": {"childCreatedAt": 1788246207176}}, + [], + ) == {"type": "fixture/event", "data": {"childCreatedAt": 1788246207176}} + + def test_snapshot_comparison_preserves_opaque_generation_provenance() -> None: normalize = SMOKE["normalize_session_format_comparison"] expected = { diff --git a/scripts/smoke-python-runtime.py b/scripts/smoke-python-runtime.py index 760e6c7bae..e8719f0709 100644 --- a/scripts/smoke-python-runtime.py +++ b/scripts/smoke-python-runtime.py @@ -2158,6 +2158,10 @@ def normalize_snapshot_value( } if normalized.get("type") == "session" and "createdAt" in normalized: normalized["createdAt"] = 0 + if normalized.get("type") == "subagent/catalog": + data = normalized.get("data") + if isinstance(data, dict) and "childCreatedAt" in data: + data["childCreatedAt"] = 0 if "seq" in normalized and "time" in normalized: normalized["time"] = 0 if normalized.get("type") in ("assistant/message", "assistant/attempt"): diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.v2.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.v2.jsonl index 651f662c19..a3c3e49f68 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.v2.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.v2.jsonl @@ -35,27 +35,29 @@ {"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":2,"throughSeq":32}} {"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-direct-child","name":"subagent","args":["{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":4,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{child-1}}","childCreatedAt":0,"mode":"one-shot","label":"Check direct child"}} {"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[35],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":4}} {"type":"step/start","data":{"turn":1,"step":5}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":2,"throughSeq":38}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":2,"throughSeq":39}} {"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-workflow","name":"workflow","args":["{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":5,"callId":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}} {"type":"tool-workflow/run-start","data":{"runId":"{{workflow-run}}","name":"advanced-exe-snapshot"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{child-2}}","childCreatedAt":0,"mode":"one-shot"}} {"type":"tool-workflow/agent-start","data":{"runId":"{{workflow-run}}","seq":1,"label":"workflow-child","phase":"Delegate","childId":"{{child-2}}"}} {"type":"tool-workflow/agent-end","data":{"runId":"{{workflow-run}}","seq":1,"outcome":"completed"}} {"type":"tool-workflow/run-end","data":{"runId":"{{workflow-run}}","stopReason":"completed"}} -{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[41],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[42],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":5}} {"type":"step/start","data":{"turn":1,"step":6}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":2,"throughSeq":48}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":2,"throughSeq":50}} {"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-undefine","name":"cordis_undefine","args":["{\"pluginId\": \"snap-1\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":6,"callId":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}} -{"type":"tool/result","data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[51],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[53],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":6}} {"type":"step/start","data":{"turn":1,"step":7}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"change"}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":2,"throughSeq":55}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":2,"throughSeq":57}} {"type":"assistant/message","data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{message:1}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["ADVANCED_EXECUTABLE_OK"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":7}} {"type":"command/run","data":{"commandId":"{{command:1}}","name":"feedback","source":{"kind":"user"}}} diff --git a/snapshots/sdk/subagent-continuable-inheritance/session.v2.jsonl b/snapshots/sdk/subagent-continuable-inheritance/session.v2.jsonl index a4d9939d8d..53e7059320 100644 --- a/snapshots/sdk/subagent-continuable-inheritance/session.v2.jsonl +++ b/snapshots/sdk/subagent-continuable-inheritance/session.v2.jsonl @@ -12,12 +12,13 @@ {"type":"session/title","data":{"title":"Follow these steps exactly, then","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788199056889,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788199056889,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"]},{"type":"chunk","time":1788199056889,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}},{"type":"chunk","time":1788199056889,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788199056889,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500533971,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500533971,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"]},{"type":"chunk","time":1788500533971,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}},{"type":"chunk","time":1788500533971,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500533971,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500533982,"mode":"continuable","label":"Reply with CHILD_OK"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788199056914,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788199056914,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1788199056914,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1788199056914,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788199056914,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500534017,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500534017,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1788500534017,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1788500534017,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500534017,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:6}}"}]}} @@ -25,6 +26,6 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":2,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:6}}"},"surfaceOp":"append"} -{"type":"assistant/attempt","data":{"turn":2,"step":1,"stream":[{"type":"chunk","time":1788199056967,"chunk":{"type":"finish","reason":{"kind":"error","failure":{"message":"llm-replay: script exhausted — session requested model call #4 but its script has only 3; re-record the scenario","code":"UNKNOWN"}}}}]}} +{"type":"assistant/attempt","data":{"turn":2,"step":1,"stream":[{"type":"chunk","time":1788500534037,"chunk":{"type":"finish","reason":{"kind":"error","failure":{"message":"llm-replay: script exhausted — session requested model call #4 but its script has only 3; re-record the scenario","code":"UNKNOWN"}}}}]}} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"error","error":{"message":"llm-replay: script exhausted — session requested model call #4 but its script has only 3; re-record the scenario","code":"UNKNOWN"}}}} diff --git a/snapshots/sdk/subagent-continuable/session.v2.jsonl b/snapshots/sdk/subagent-continuable/session.v2.jsonl index 99fa92dba8..fe561fdf71 100644 --- a/snapshots/sdk/subagent-continuable/session.v2.jsonl +++ b/snapshots/sdk/subagent-continuable/session.v2.jsonl @@ -11,27 +11,28 @@ {"type":"session/title","data":{"title":"Follow these steps exactly, then","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788269696592,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788269696592,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"]},{"type":"chunk","time":1788269696592,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}},{"type":"chunk","time":1788269696592,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788269696592,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500520579,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500520579,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"]},{"type":"chunk","time":1788500520579,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}},{"type":"chunk","time":1788500520579,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500520579,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500520585,"mode":"continuable","label":"Reply with CHILD_OK"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_1","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788269696618,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788269696618,"index":0,"dt":[],"id":"call_followup_1","name":"send_message","args":["{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly SECOND_OK.\"}"]},{"type":"chunk","time":1788269696618,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_1","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}}},{"type":"chunk","time":1788269696618,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788269696618,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_1","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500520615,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500520615,"index":0,"dt":[],"id":"call_followup_1","name":"send_message","args":["{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly SECOND_OK.\"}"]},{"type":"chunk","time":1788500520615,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_1","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}}},{"type":"chunk","time":1788500520615,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500520615,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"call_followup_1","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}} -{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_followup_1"},"content":[{"type":"tool-result","toolCallId":"call_followup_1","content":[{"type":"text","text":"message delivered to agent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:6}}"}},"sourceEventSeqs":[18],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_followup_1"},"content":[{"type":"tool-result","toolCallId":"call_followup_1","content":[{"type":"text","text":"message delivered to agent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:6}}"}},"sourceEventSeqs":[19],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} -{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_2","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788269696641,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788269696642,"index":0,"dt":[],"id":"call_followup_2","name":"send_message","args":["{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly THIRD_OK.\"}"]},{"type":"chunk","time":1788269696642,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_2","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}}},{"type":"chunk","time":1788269696642,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788269696642,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_2","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500520635,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500520635,"index":0,"dt":[],"id":"call_followup_2","name":"send_message","args":["{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly THIRD_OK.\"}"]},{"type":"chunk","time":1788500520635,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_2","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}}},{"type":"chunk","time":1788500520635,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500520635,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":3,"callId":"call_followup_2","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}} -{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"call_followup_2"},"content":[{"type":"tool-result","toolCallId":"call_followup_2","content":[{"type":"text","text":"message delivered to agent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:8}}"}},"sourceEventSeqs":[23],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"call_followup_2"},"content":[{"type":"tool-result","toolCallId":"call_followup_2","content":[{"type":"text","text":"message delivered to agent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:8}}"}},"sourceEventSeqs":[24],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} {"type":"step/start","data":{"turn":1,"step":4}} -{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_unknown","name":"send_message","arguments":"{\"agent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788269696663,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788269696664,"index":0,"dt":[],"id":"call_followup_unknown","name":"send_message","args":["{\"agent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"]},{"type":"chunk","time":1788269696664,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_unknown","name":"send_message","arguments":"{\"agent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}}},{"type":"chunk","time":1788269696664,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788269696664,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_unknown","name":"send_message","arguments":"{\"agent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500520648,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500520648,"index":0,"dt":[],"id":"call_followup_unknown","name":"send_message","args":["{\"agent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"]},{"type":"chunk","time":1788500520648,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_unknown","name":"send_message","arguments":"{\"agent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}}},{"type":"chunk","time":1788500520648,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500520648,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":4,"callId":"call_followup_unknown","name":"send_message","arguments":"{\"agent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}} -{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"call_followup_unknown"},"content":[{"type":"tool-result","toolCallId":"call_followup_unknown","content":[{"type":"text","text":"Error: subagent \"22222222-2222-4222-8222-222222222222\" is unavailable"}],"isError":true}],"role":"user","id":"{{message:10}}"},"error":{"name":"SubagentError","code":"NOT_RESUMABLE"}},"sourceEventSeqs":[28],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"call_followup_unknown"},"content":[{"type":"tool-result","toolCallId":"call_followup_unknown","content":[{"type":"text","text":"Error: subagent \"22222222-2222-4222-8222-222222222222\" is unavailable"}],"isError":true}],"role":"user","id":"{{message:10}}"},"error":{"name":"SubagentError","code":"NOT_RESUMABLE"}},"sourceEventSeqs":[29],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":4}} {"type":"step/start","data":{"turn":1,"step":5}} -{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788269696680,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788269696680,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1788269696681,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1788269696681,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788269696681,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500520662,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500520662,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1788500520662,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1788500520662,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500520662,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":5}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"SECOND_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:12}}"}]}} @@ -39,6 +40,6 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":2,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"SECOND_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:12}}"},"surfaceOp":"append"} -{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:13}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788269696729,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788269696729,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1788269696729,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1788269696729,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788269696729,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:13}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500520702,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500520702,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1788500520702,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1788500520702,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500520702,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}} diff --git a/snapshots/sdk/subagent-fork-in-process/session.v2.jsonl b/snapshots/sdk/subagent-fork-in-process/session.v2.jsonl index 5f0fdd60e1..f9f4398bfc 100644 --- a/snapshots/sdk/subagent-fork-in-process/session.v2.jsonl +++ b/snapshots/sdk/subagent-fork-in-process/session.v2.jsonl @@ -11,7 +11,7 @@ {"type":"session/title","data":{"title":"Remember this fact for later:","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to remember the codeword \"MARMALADE\" and reply with just \"OK\"."},{"type":"text","text":"OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":2885,"outputTokens":25,"cacheReadTokens":0,"reasoningTokens":23},"stream":[{"type":"chunk","time":1788199063113,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788199063113,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," remember"," the"," cod","ew","ord"," \"","M","ARM","AL","ADE","\""," and"," reply"," with"," just"," \"","OK","\"."]},{"type":"chunk","time":1788199063113,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788199063113,"index":1,"dt":[],"texts":["OK"]},{"type":"chunk","time":1788199063113,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to remember the codeword \"MARMALADE\" and reply with just \"OK\"."}}},{"type":"chunk","time":1788199063113,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"OK"}}},{"type":"chunk","time":1788199063113,"chunk":{"type":"usage","usage":{"inputTokens":2885,"outputTokens":25,"cacheReadTokens":0,"reasoningTokens":23}}},{"type":"chunk","time":1788199063113,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to remember the codeword \"MARMALADE\" and reply with just \"OK\"."},{"type":"text","text":"OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":2885,"outputTokens":25,"cacheReadTokens":0,"reasoningTokens":23},"stream":[{"type":"chunk","time":1788500537271,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788500537272,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," remember"," the"," cod","ew","ord"," \"","M","ARM","AL","ADE","\""," and"," reply"," with"," just"," \"","OK","\"."]},{"type":"chunk","time":1788500537272,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788500537272,"index":1,"dt":[],"texts":["OK"]},{"type":"chunk","time":1788500537272,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to remember the codeword \"MARMALADE\" and reply with just \"OK\"."}}},{"type":"chunk","time":1788500537272,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"OK"}}},{"type":"chunk","time":1788500537272,"chunk":{"type":"usage","usage":{"inputTokens":2885,"outputTokens":25,"cacheReadTokens":0,"reasoningTokens":23}}},{"type":"chunk","time":1788500537272,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Use the subagent_fork tool exactly once to delegate this subtask to a forked child agent: 'What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.' The forked child inherits this conversation, so it can answer. After the subagent returns, reply with the single word PARENT_DONE and stop. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"{{message:4}}"}]}} @@ -19,11 +19,12 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":2,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Use the subagent_fork tool exactly once to delegate this subtask to a forked child agent: 'What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.' The forked child inherits this conversation, so it can answer. After the subagent returns, reply with the single word PARENT_DONE and stop. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"{{message:4}}"},"surfaceOp":"append"} -{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to use subagent_fork to delegate a question to a child agent. The child agent inherits this conversation and should be able to answer: the project codeword is MARMALADE. After the subagent returns, I should reply with PARENT_DONE."},{"type":"tool-call","id":"call_00_sAtKUseRzHRBvL4CF7XF1334","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":158,"outputTokens":147,"cacheReadTokens":2816,"reasoningTokens":59},"stream":[{"type":"chunk","time":1788199063133,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788199063133,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," use"," sub","agent","_f","ork"," to"," delegate"," a"," question"," to"," a"," child"," agent","."," The"," child"," agent"," inher","its"," this"," conversation"," and"," should"," be"," able"," to"," answer",":"," the"," project"," cod","ew","ord"," is"," MAR","M","AL","ADE","."," After"," the"," sub","agent"," returns",","," I"," should"," reply"," with"," PAR","ENT","_D","ONE","."]},{"type":"chunk","time":1788199063133,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788199063133,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_sAtKUseRzHRBvL4CF7XF1334","name":"subagent_fork","args":["","{","\"","description","\"",": ","\"","Recall"," project"," cod","ew","ord","\"",", ","\"","prom","pt","\"",": ","\"","What"," is"," the"," project"," cod","ew","ord"," mentioned"," earlier"," in"," this"," conversation","?"," Reply"," with"," exactly"," that"," one"," word"," and"," nothing"," else",".","\"","}"]},{"type":"chunk","time":1788199063133,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to use subagent_fork to delegate a question to a child agent. The child agent inherits this conversation and should be able to answer: the project codeword is MARMALADE. After the subagent returns, I should reply with PARENT_DONE."}}},{"type":"chunk","time":1788199063133,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_sAtKUseRzHRBvL4CF7XF1334","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}}},{"type":"chunk","time":1788199063133,"chunk":{"type":"usage","usage":{"inputTokens":158,"outputTokens":147,"cacheReadTokens":2816,"reasoningTokens":59}}},{"type":"chunk","time":1788199063133,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to use subagent_fork to delegate a question to a child agent. The child agent inherits this conversation and should be able to answer: the project codeword is MARMALADE. After the subagent returns, I should reply with PARENT_DONE."},{"type":"tool-call","id":"call_00_sAtKUseRzHRBvL4CF7XF1334","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":158,"outputTokens":147,"cacheReadTokens":2816,"reasoningTokens":59},"stream":[{"type":"chunk","time":1788500537291,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788500537291,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," use"," sub","agent","_f","ork"," to"," delegate"," a"," question"," to"," a"," child"," agent","."," The"," child"," agent"," inher","its"," this"," conversation"," and"," should"," be"," able"," to"," answer",":"," the"," project"," cod","ew","ord"," is"," MAR","M","AL","ADE","."," After"," the"," sub","agent"," returns",","," I"," should"," reply"," with"," PAR","ENT","_D","ONE","."]},{"type":"chunk","time":1788500537291,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500537291,"index":1,"dt":[0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_sAtKUseRzHRBvL4CF7XF1334","name":"subagent_fork","args":["","{","\"","description","\"",": ","\"","Recall"," project"," cod","ew","ord","\"",", ","\"","prom","pt","\"",": ","\"","What"," is"," the"," project"," cod","ew","ord"," mentioned"," earlier"," in"," this"," conversation","?"," Reply"," with"," exactly"," that"," one"," word"," and"," nothing"," else",".","\"","}"]},{"type":"chunk","time":1788500537292,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to use subagent_fork to delegate a question to a child agent. The child agent inherits this conversation and should be able to answer: the project codeword is MARMALADE. After the subagent returns, I should reply with PARENT_DONE."}}},{"type":"chunk","time":1788500537292,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_sAtKUseRzHRBvL4CF7XF1334","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}}},{"type":"chunk","time":1788500537292,"chunk":{"type":"usage","usage":{"inputTokens":158,"outputTokens":147,"cacheReadTokens":2816,"reasoningTokens":59}}},{"type":"chunk","time":1788500537292,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":2,"step":1,"callId":"call_00_sAtKUseRzHRBvL4CF7XF1334","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500537300,"mode":"one-shot","label":"Recall project codeword"}} {"type":"tool/result","data":{"turn":2,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_sAtKUseRzHRBvL4CF7XF1334"},"content":[{"type":"tool-result","toolCallId":"call_00_sAtKUseRzHRBvL4CF7XF1334","content":[{"type":"text","text":"MARMALADE"}],"isError":false}],"role":"user","id":"{{message:6}}"}},"sourceEventSeqs":[21],"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"step/start","data":{"turn":2,"step":2}} -{"type":"assistant/message","data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The forked child agent correctly returned \"MARMALADE\". Now I need to reply with \"PARENT_DONE\"."},{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":65,"outputTokens":30,"cacheReadTokens":3072,"reasoningTokens":25},"stream":[{"type":"chunk","time":1788199063200,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788199063200,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," for","ked"," child"," agent"," correctly"," returned"," \"","M","ARM","AL","ADE","\"."," Now"," I"," need"," to"," reply"," with"," \"","PAR","ENT","_D","ONE","\"."]},{"type":"chunk","time":1788199063200,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788199063200,"index":1,"dt":[0,0,0],"texts":["PAR","ENT","_D","ONE"]},{"type":"chunk","time":1788199063200,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The forked child agent correctly returned \"MARMALADE\". Now I need to reply with \"PARENT_DONE\"."}}},{"type":"chunk","time":1788199063200,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788199063200,"chunk":{"type":"usage","usage":{"inputTokens":65,"outputTokens":30,"cacheReadTokens":3072,"reasoningTokens":25}}},{"type":"chunk","time":1788199063200,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The forked child agent correctly returned \"MARMALADE\". Now I need to reply with \"PARENT_DONE\"."},{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":65,"outputTokens":30,"cacheReadTokens":3072,"reasoningTokens":25},"stream":[{"type":"chunk","time":1788500537363,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788500537363,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," for","ked"," child"," agent"," correctly"," returned"," \"","M","ARM","AL","ADE","\"."," Now"," I"," need"," to"," reply"," with"," \"","PAR","ENT","_D","ONE","\"."]},{"type":"chunk","time":1788500537363,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788500537363,"index":1,"dt":[0,0,0],"texts":["PAR","ENT","_D","ONE"]},{"type":"chunk","time":1788500537363,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The forked child agent correctly returned \"MARMALADE\". Now I need to reply with \"PARENT_DONE\"."}}},{"type":"chunk","time":1788500537363,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788500537363,"chunk":{"type":"usage","usage":{"inputTokens":65,"outputTokens":30,"cacheReadTokens":3072,"reasoningTokens":25}}},{"type":"chunk","time":1788500537363,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":2}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}} diff --git a/snapshots/sdk/subagent-list-agents/session.v2.jsonl b/snapshots/sdk/subagent-list-agents/session.v2.jsonl index bc0a67bfad..685bafd019 100644 --- a/snapshots/sdk/subagent-list-agents/session.v2.jsonl +++ b/snapshots/sdk/subagent-list-agents/session.v2.jsonl @@ -11,12 +11,13 @@ {"type":"session/title","data":{"title":"Call the subagent tool once","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788199064253,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788199064253,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"]},{"type":"chunk","time":1788199064253,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}},{"type":"chunk","time":1788199064253,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788199064253,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500537942,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500537942,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"]},{"type":"chunk","time":1788500537942,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}},{"type":"chunk","time":1788500537942,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500537942,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500537949,"mode":"continuable","label":"Reply with CHILD_OK"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788199064280,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788199064280,"index":0,"dt":[],"texts":["STARTED"]},{"type":"chunk","time":1788199064280,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"STARTED"}}},{"type":"chunk","time":1788199064280,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788199064280,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500537988,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500537988,"index":0,"dt":[],"texts":["STARTED"]},{"type":"chunk","time":1788500537988,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"STARTED"}}},{"type":"chunk","time":1788500537988,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500537988,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:6}}"}]}} @@ -24,7 +25,7 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":2,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:6}}"},"surfaceOp":"append"} -{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788199064342,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788199064342,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1788199064342,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1788199064342,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788199064342,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500538018,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500538018,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1788500538018,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1788500538018,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500538018,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Call list_agents once with scope set to descendants and observe the subagent you started. Then call interrupt_agent once with agent_id set to {{session:2}}. Then reply with the single word DONE. Do not call any other tool."}],"source":{"kind":"user"},"role":"user","id":"{{message:8}}"}]}} @@ -32,11 +33,11 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":3,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Call list_agents once with scope set to descendants and observe the subagent you started. Then call interrupt_agent once with agent_id set to {{session:2}}. Then reply with the single word DONE. Do not call any other tool."}],"source":{"kind":"user"},"role":"user","id":"{{message:8}}"},"surfaceOp":"append"} -{"type":"assistant/message","data":{"turn":3,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_list","name":"list_agents","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788199064368,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788199064368,"index":0,"dt":[],"id":"call_list","name":"list_agents","args":["{}"]},{"type":"chunk","time":1788199064368,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_list","name":"list_agents","arguments":"{}"}}},{"type":"chunk","time":1788199064368,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788199064368,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":3,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_list","name":"list_agents","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500538040,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500538040,"index":0,"dt":[],"id":"call_list","name":"list_agents","args":["{}"]},{"type":"chunk","time":1788500538040,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_list","name":"list_agents","arguments":"{}"}}},{"type":"chunk","time":1788500538040,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500538040,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":3,"step":1,"callId":"call_list","name":"list_agents","arguments":"{}"}} -{"type":"tool/result","data":{"turn":3,"step":1,"message":{"source":{"kind":"tool","callId":"call_list"},"content":[{"type":"tool-result","toolCallId":"call_list","content":[{"type":"text","text":"{{session:2}} [ready] — Reply with CHILD_OK"}],"isError":false}],"role":"user","id":"{{message:10}}"}},"sourceEventSeqs":[34],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":3,"step":1,"message":{"source":{"kind":"tool","callId":"call_list"},"content":[{"type":"tool-result","toolCallId":"call_list","content":[{"type":"text","text":"{{session:2}} [ready] — Reply with CHILD_OK"}],"isError":false}],"role":"user","id":"{{message:10}}"}},"sourceEventSeqs":[35],"surfaceOp":"append"} {"type":"step/end","data":{"turn":3,"step":1}} {"type":"step/start","data":{"turn":3,"step":2}} -{"type":"assistant/message","data":{"turn":3,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788199064389,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788199064389,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1788199064389,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1788199064389,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788199064389,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":3,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788500538061,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500538061,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1788500538061,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1788500538061,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788500538061,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":3,"step":2}} {"type":"turn/end","data":{"turn":3,"reason":{"kind":"completed"}}} diff --git a/snapshots/sdk/subagent-mixed/session.v2.jsonl b/snapshots/sdk/subagent-mixed/session.v2.jsonl index b221888a97..312dad21c5 100644 --- a/snapshots/sdk/subagent-mixed/session.v2.jsonl +++ b/snapshots/sdk/subagent-mixed/session.v2.jsonl @@ -11,7 +11,7 @@ {"type":"session/title","data":{"title":"Remember this fact for later:","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to remember a codeword and just reply with \"OK\"."},{"type":"text","text":"OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":2883,"outputTokens":19,"cacheReadTokens":0,"reasoningTokens":17},"stream":[{"type":"chunk","time":1788199065455,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788199065455,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," remember"," a"," cod","ew","ord"," and"," just"," reply"," with"," \"","OK","\"."]},{"type":"chunk","time":1788199065455,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788199065455,"index":1,"dt":[],"texts":["OK"]},{"type":"chunk","time":1788199065456,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to remember a codeword and just reply with \"OK\"."}}},{"type":"chunk","time":1788199065456,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"OK"}}},{"type":"chunk","time":1788199065456,"chunk":{"type":"usage","usage":{"inputTokens":2883,"outputTokens":19,"cacheReadTokens":0,"reasoningTokens":17}}},{"type":"chunk","time":1788199065456,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to remember a codeword and just reply with \"OK\"."},{"type":"text","text":"OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":2883,"outputTokens":19,"cacheReadTokens":0,"reasoningTokens":17},"stream":[{"type":"chunk","time":1788500538590,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788500538590,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," remember"," a"," cod","ew","ord"," and"," just"," reply"," with"," \"","OK","\"."]},{"type":"chunk","time":1788500538590,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788500538590,"index":1,"dt":[],"texts":["OK"]},{"type":"chunk","time":1788500538590,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to remember a codeword and just reply with \"OK\"."}}},{"type":"chunk","time":1788500538590,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"OK"}}},{"type":"chunk","time":1788500538590,"chunk":{"type":"usage","usage":{"inputTokens":2883,"outputTokens":19,"cacheReadTokens":0,"reasoningTokens":17}}},{"type":"chunk","time":1788500538590,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Do these two delegations, once at a time. First, use the subagent tool (fresh child) exactly once: 'Reply with exactly the word ALPHA and nothing else.' Then, after it returns, use the subagent_fork tool (forked child that inherits this conversation) exactly once: 'What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.' After both subagents return, reply with the single word PARENT_DONE and stop. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"{{message:4}}"}]}} @@ -19,16 +19,18 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":2,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Do these two delegations, once at a time. First, use the subagent tool (fresh child) exactly once: 'Reply with exactly the word ALPHA and nothing else.' Then, after it returns, use the subagent_fork tool (forked child that inherits this conversation) exactly once: 'What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.' After both subagents return, reply with the single word PARENT_DONE and stop. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"{{message:4}}"},"surfaceOp":"append"} -{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"Let me do these two delegations one at a time as requested.\n\nFirst, I'll use the subagent tool (fresh child) to reply with \"ALPHA\"."},{"type":"tool-call","id":"call_00_YvHr2bGomk5HhpgDTvE81896","name":"subagent","arguments":"{\"description\": \"Reply ALPHA only\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":185,"outputTokens":110,"cacheReadTokens":2816,"reasoningTokens":35},"stream":[{"type":"chunk","time":1788199065477,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788199065477,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["Let"," me"," do"," these"," two"," deleg","ations"," one"," at"," a"," time"," as"," requested",".\n\n","First",","," I","'ll"," use"," the"," sub","agent"," tool"," (","fresh"," child",")"," to"," reply"," with"," \"","AL","P","HA","\"."]},{"type":"chunk","time":1788199065477,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788199065477,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_YvHr2bGomk5HhpgDTvE81896","name":"subagent","args":["","{","\"","description","\"",": ","\"","Reply"," AL","P","HA"," only","\"",", ","\"","prom","pt","\"",": ","\"","Reply"," with"," exactly"," the"," word"," AL","P","HA"," and"," nothing"," else",".","\"",", \"run_in_background\":false}"]},{"type":"chunk","time":1788199065477,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Let me do these two delegations one at a time as requested.\n\nFirst, I'll use the subagent tool (fresh child) to reply with \"ALPHA\"."}}},{"type":"chunk","time":1788199065477,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_YvHr2bGomk5HhpgDTvE81896","name":"subagent","arguments":"{\"description\": \"Reply ALPHA only\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788199065477,"chunk":{"type":"usage","usage":{"inputTokens":185,"outputTokens":110,"cacheReadTokens":2816,"reasoningTokens":35}}},{"type":"chunk","time":1788199065477,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"Let me do these two delegations one at a time as requested.\n\nFirst, I'll use the subagent tool (fresh child) to reply with \"ALPHA\"."},{"type":"tool-call","id":"call_00_YvHr2bGomk5HhpgDTvE81896","name":"subagent","arguments":"{\"description\": \"Reply ALPHA only\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":185,"outputTokens":110,"cacheReadTokens":2816,"reasoningTokens":35},"stream":[{"type":"chunk","time":1788500538608,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788500538608,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["Let"," me"," do"," these"," two"," deleg","ations"," one"," at"," a"," time"," as"," requested",".\n\n","First",","," I","'ll"," use"," the"," sub","agent"," tool"," (","fresh"," child",")"," to"," reply"," with"," \"","AL","P","HA","\"."]},{"type":"chunk","time":1788500538608,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500538608,"index":1,"dt":[0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_YvHr2bGomk5HhpgDTvE81896","name":"subagent","args":["","{","\"","description","\"",": ","\"","Reply"," AL","P","HA"," only","\"",", ","\"","prom","pt","\"",": ","\"","Reply"," with"," exactly"," the"," word"," AL","P","HA"," and"," nothing"," else",".","\"",", \"run_in_background\":false}"]},{"type":"chunk","time":1788500538609,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Let me do these two delegations one at a time as requested.\n\nFirst, I'll use the subagent tool (fresh child) to reply with \"ALPHA\"."}}},{"type":"chunk","time":1788500538609,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_YvHr2bGomk5HhpgDTvE81896","name":"subagent","arguments":"{\"description\": \"Reply ALPHA only\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788500538609,"chunk":{"type":"usage","usage":{"inputTokens":185,"outputTokens":110,"cacheReadTokens":2816,"reasoningTokens":35}}},{"type":"chunk","time":1788500538609,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":2,"step":1,"callId":"call_00_YvHr2bGomk5HhpgDTvE81896","name":"subagent","arguments":"{\"description\": \"Reply ALPHA only\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500538615,"mode":"one-shot","label":"Reply ALPHA only"}} {"type":"tool/result","data":{"turn":2,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_YvHr2bGomk5HhpgDTvE81896"},"content":[{"type":"tool-result","toolCallId":"call_00_YvHr2bGomk5HhpgDTvE81896","content":[{"type":"text","text":"ALPHA"}],"isError":false}],"role":"user","id":"{{message:6}}"}},"sourceEventSeqs":[21],"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"step/start","data":{"turn":2,"step":2}} -{"type":"assistant/message","data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The first subagent returned \"ALPHA\". Now I need to use the subagent_fork tool (forked child that inherits this conversation) to ask about the project codeword."},{"type":"tool-call","id":"call_00_JSr5rhREq23wSmwSkCP77184","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":54,"outputTokens":128,"cacheReadTokens":3072,"reasoningTokens":40},"stream":[{"type":"chunk","time":1788199065535,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788199065535,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," first"," sub","agent"," returned"," \"","AL","P","HA","\"."," Now"," I"," need"," to"," use"," the"," sub","agent","_f","ork"," tool"," (","fork","ed"," child"," that"," inher","its"," this"," conversation",")"," to"," ask"," about"," the"," project"," cod","ew","ord","."]},{"type":"chunk","time":1788199065535,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788199065535,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_JSr5rhREq23wSmwSkCP77184","name":"subagent_fork","args":["","{","\"","description","\"",": ","\"","Recall"," project"," cod","ew","ord","\"",", ","\"","prom","pt","\"",": ","\"","What"," is"," the"," project"," cod","ew","ord"," mentioned"," earlier"," in"," this"," conversation","?"," Reply"," with"," exactly"," that"," one"," word"," and"," nothing"," else",".","\"","}"]},{"type":"chunk","time":1788199065535,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The first subagent returned \"ALPHA\". Now I need to use the subagent_fork tool (forked child that inherits this conversation) to ask about the project codeword."}}},{"type":"chunk","time":1788199065535,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_JSr5rhREq23wSmwSkCP77184","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}}},{"type":"chunk","time":1788199065535,"chunk":{"type":"usage","usage":{"inputTokens":54,"outputTokens":128,"cacheReadTokens":3072,"reasoningTokens":40}}},{"type":"chunk","time":1788199065535,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The first subagent returned \"ALPHA\". Now I need to use the subagent_fork tool (forked child that inherits this conversation) to ask about the project codeword."},{"type":"tool-call","id":"call_00_JSr5rhREq23wSmwSkCP77184","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":54,"outputTokens":128,"cacheReadTokens":3072,"reasoningTokens":40},"stream":[{"type":"chunk","time":1788500538670,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788500538670,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," first"," sub","agent"," returned"," \"","AL","P","HA","\"."," Now"," I"," need"," to"," use"," the"," sub","agent","_f","ork"," tool"," (","fork","ed"," child"," that"," inher","its"," this"," conversation",")"," to"," ask"," about"," the"," project"," cod","ew","ord","."]},{"type":"chunk","time":1788500538670,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500538670,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0],"id":"call_00_JSr5rhREq23wSmwSkCP77184","name":"subagent_fork","args":["","{","\"","description","\"",": ","\"","Recall"," project"," cod","ew","ord","\"",", ","\"","prom","pt","\"",": ","\"","What"," is"," the"," project"," cod","ew","ord"," mentioned"," earlier"," in"," this"," conversation","?"," Reply"," with"," exactly"," that"," one"," word"," and"," nothing"," else",".","\"","}"]},{"type":"chunk","time":1788500538671,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The first subagent returned \"ALPHA\". Now I need to use the subagent_fork tool (forked child that inherits this conversation) to ask about the project codeword."}}},{"type":"chunk","time":1788500538671,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_JSr5rhREq23wSmwSkCP77184","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}}},{"type":"chunk","time":1788500538671,"chunk":{"type":"usage","usage":{"inputTokens":54,"outputTokens":128,"cacheReadTokens":3072,"reasoningTokens":40}}},{"type":"chunk","time":1788500538671,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":2,"step":2,"callId":"call_00_JSr5rhREq23wSmwSkCP77184","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}} -{"type":"tool/result","data":{"turn":2,"step":2,"message":{"source":{"kind":"tool","callId":"call_00_JSr5rhREq23wSmwSkCP77184"},"content":[{"type":"tool-result","toolCallId":"call_00_JSr5rhREq23wSmwSkCP77184","content":[{"type":"text","text":"SAFFRON"}],"isError":false}],"role":"user","id":"{{message:8}}"}},"sourceEventSeqs":[26],"surfaceOp":"append"} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:3}}","childCreatedAt":1788500538677,"mode":"one-shot","label":"Recall project codeword"}} +{"type":"tool/result","data":{"turn":2,"step":2,"message":{"source":{"kind":"tool","callId":"call_00_JSr5rhREq23wSmwSkCP77184"},"content":[{"type":"tool-result","toolCallId":"call_00_JSr5rhREq23wSmwSkCP77184","content":[{"type":"text","text":"SAFFRON"}],"isError":false}],"role":"user","id":"{{message:8}}"}},"sourceEventSeqs":[27],"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":2}} {"type":"step/start","data":{"turn":2,"step":3}} -{"type":"assistant/message","data":{"turn":2,"step":3,"message":{"role":"assistant","content":[{"type":"reasoning","text":"Both subagents returned:\n1. First (fresh child): \"ALPHA\"\n2. Second (forked child): \"SAFFRON\" - correctly inherited the conversation context where I was asked to remember the codeword \"SAFFRON\".\n\nNow I reply with \"PARENT_DONE\" as instructed."},{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":69,"outputTokens":69,"cacheReadTokens":3200,"reasoningTokens":64},"stream":[{"type":"chunk","time":1788199065598,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788199065598,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["Both"," sub","agents"," returned",":\n","1","."," First"," (","fresh"," child","):"," \"","AL","P","HA","\"\n","2","."," Second"," (","fork","ed"," child","):"," \"","SA","FF","RON","\""," -"," correctly"," inherited"," the"," conversation"," context"," where"," I"," was"," asked"," to"," remember"," the"," cod","ew","ord"," \"","SA","FF","RON","\".\n\n","Now"," I"," reply"," with"," \"","PAR","ENT","_D","ONE","\""," as"," instructed","."]},{"type":"chunk","time":1788199065598,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788199065598,"index":1,"dt":[0,0,0],"texts":["PAR","ENT","_D","ONE"]},{"type":"chunk","time":1788199065598,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Both subagents returned:\n1. First (fresh child): \"ALPHA\"\n2. Second (forked child): \"SAFFRON\" - correctly inherited the conversation context where I was asked to remember the codeword \"SAFFRON\".\n\nNow I reply with \"PARENT_DONE\" as instructed."}}},{"type":"chunk","time":1788199065598,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788199065598,"chunk":{"type":"usage","usage":{"inputTokens":69,"outputTokens":69,"cacheReadTokens":3200,"reasoningTokens":64}}},{"type":"chunk","time":1788199065598,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":3,"message":{"role":"assistant","content":[{"type":"reasoning","text":"Both subagents returned:\n1. First (fresh child): \"ALPHA\"\n2. Second (forked child): \"SAFFRON\" - correctly inherited the conversation context where I was asked to remember the codeword \"SAFFRON\".\n\nNow I reply with \"PARENT_DONE\" as instructed."},{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":69,"outputTokens":69,"cacheReadTokens":3200,"reasoningTokens":64},"stream":[{"type":"chunk","time":1788500538725,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788500538725,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["Both"," sub","agents"," returned",":\n","1","."," First"," (","fresh"," child","):"," \"","AL","P","HA","\"\n","2","."," Second"," (","fork","ed"," child","):"," \"","SA","FF","RON","\""," -"," correctly"," inherited"," the"," conversation"," context"," where"," I"," was"," asked"," to"," remember"," the"," cod","ew","ord"," \"","SA","FF","RON","\".\n\n","Now"," I"," reply"," with"," \"","PAR","ENT","_D","ONE","\""," as"," instructed","."]},{"type":"chunk","time":1788500538725,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788500538725,"index":1,"dt":[0,0,0],"texts":["PAR","ENT","_D","ONE"]},{"type":"chunk","time":1788500538725,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Both subagents returned:\n1. First (fresh child): \"ALPHA\"\n2. Second (forked child): \"SAFFRON\" - correctly inherited the conversation context where I was asked to remember the codeword \"SAFFRON\".\n\nNow I reply with \"PARENT_DONE\" as instructed."}}},{"type":"chunk","time":1788500538725,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788500538725,"chunk":{"type":"usage","usage":{"inputTokens":69,"outputTokens":69,"cacheReadTokens":3200,"reasoningTokens":64}}},{"type":"chunk","time":1788500538725,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":3}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}} diff --git a/snapshots/sdk/subagent-send-message/session.v2.jsonl b/snapshots/sdk/subagent-send-message/session.v2.jsonl index d80daddee2..6de97f0e4c 100644 --- a/snapshots/sdk/subagent-send-message/session.v2.jsonl +++ b/snapshots/sdk/subagent-send-message/session.v2.jsonl @@ -11,12 +11,13 @@ {"type":"session/title","data":{"title":"Follow these steps exactly, then","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Send a finding\", \"prompt\": \"Send exactly CHILD_MESSAGE_OK to your parent with send_message, then stop.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788269697328,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788269697328,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\": \"Send a finding\", \"prompt\": \"Send exactly CHILD_MESSAGE_OK to your parent with send_message, then stop.\", \"run_in_background\": true}"]},{"type":"chunk","time":1788269697328,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Send a finding\", \"prompt\": \"Send exactly CHILD_MESSAGE_OK to your parent with send_message, then stop.\", \"run_in_background\": true}"}}},{"type":"chunk","time":1788269697328,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788269697328,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Send a finding\", \"prompt\": \"Send exactly CHILD_MESSAGE_OK to your parent with send_message, then stop.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500539308,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500539308,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\": \"Send a finding\", \"prompt\": \"Send exactly CHILD_MESSAGE_OK to your parent with send_message, then stop.\", \"run_in_background\": true}"]},{"type":"chunk","time":1788500539308,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Send a finding\", \"prompt\": \"Send exactly CHILD_MESSAGE_OK to your parent with send_message, then stop.\", \"run_in_background\": true}"}}},{"type":"chunk","time":1788500539308,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500539308,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Send a finding\", \"prompt\": \"Send exactly CHILD_MESSAGE_OK to your parent with send_message, then stop.\", \"run_in_background\": true}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500539315,"mode":"continuable","label":"Send a finding"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788269697354,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788269697354,"index":0,"dt":[],"texts":["STARTED"]},{"type":"chunk","time":1788269697354,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"STARTED"}}},{"type":"chunk","time":1788269697354,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788269697354,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500539349,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500539349,"index":0,"dt":[],"texts":["STARTED"]},{"type":"chunk","time":1788500539349,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"STARTED"}}},{"type":"chunk","time":1788500539349,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500539349,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} {"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Agent {{session:2}} sent a message: "},{"type":"text","text":"CHILD_MESSAGE_OK"}],"source":{"kind":"agent-message","form":"relay","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:6}}"}]}} @@ -27,7 +28,7 @@ {"type":"step/start","data":{"turn":2,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Agent {{session:2}} sent a message: "},{"type":"text","text":"CHILD_MESSAGE_OK"}],"source":{"kind":"agent-message","form":"relay","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:6}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"Message sent."}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:7}}"},"surfaceOp":"append"} -{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788269697428,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788269697428,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1788269697428,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1788269697428,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788269697428,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500539403,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500539403,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1788500539403,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1788500539403,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500539403,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Repeat back, verbatim, the exact message the background subagent sent you. Reply with only that text. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"{{message:9}}"}]}} @@ -35,6 +36,6 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":3,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Repeat back, verbatim, the exact message the background subagent sent you. Reply with only that text. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"{{message:9}}"},"surfaceOp":"append"} -{"type":"assistant/message","data":{"turn":3,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"CHILD_MESSAGE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788269697454,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788269697454,"index":0,"dt":[],"texts":["CHILD_MESSAGE_OK"]},{"type":"chunk","time":1788269697454,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CHILD_MESSAGE_OK"}}},{"type":"chunk","time":1788269697454,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788269697454,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":3,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"CHILD_MESSAGE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500539428,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500539428,"index":0,"dt":[],"texts":["CHILD_MESSAGE_OK"]},{"type":"chunk","time":1788500539428,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CHILD_MESSAGE_OK"}}},{"type":"chunk","time":1788500539428,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500539428,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":3,"step":1}} {"type":"turn/end","data":{"turn":3,"reason":{"kind":"completed"}}} diff --git a/snapshots/sdk/subagent-spawn-in-process/session.v2.jsonl b/snapshots/sdk/subagent-spawn-in-process/session.v2.jsonl index 8b6c5fc4f6..16cecfc4e6 100644 --- a/snapshots/sdk/subagent-spawn-in-process/session.v2.jsonl +++ b/snapshots/sdk/subagent-spawn-in-process/session.v2.jsonl @@ -11,11 +11,12 @@ {"type":"session/title","data":{"title":"Use the subagent tool exactly","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to:\n1. Use the subagent tool exactly once with description 'echo probe' and prompt 'Reply with exactly: child answer 42.'\n2. Then reply with the subagent's final answer verbatim.\n\nLet me do this step by step."},{"type":"tool-call","id":"call_00_oHPNQ1nLoakoaAGXIxCM7404","name":"subagent","arguments":"{\"description\": \"echo probe\", \"prompt\": \"Reply with exactly: child answer 42.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":135,"outputTokens":124,"cacheReadTokens":1664,"reasoningTokens":55},"stream":[{"type":"chunk","time":1788199067994,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788199067994,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to",":\n","1","."," Use"," the"," sub","agent"," tool"," exactly"," once"," with"," description"," '","echo"," probe","'"," and"," prompt"," '","Reply"," with"," exactly",":"," child"," answer"," ","42",".'\n","2","."," Then"," reply"," with"," the"," sub","agent","'s"," final"," answer"," verb","atim",".\n\n","Let"," me"," do"," this"," step"," by"," step","."]},{"type":"chunk","time":1788199067994,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788199067994,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_oHPNQ1nLoakoaAGXIxCM7404","name":"subagent","args":["","{","\"","description","\"",": ","\"","echo"," probe","\"",", ","\"","prom","pt","\"",": ","\"","Reply"," with"," exactly",":"," child"," answer"," ","42",".","\"","}"]},{"type":"chunk","time":1788199067994,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to:\n1. Use the subagent tool exactly once with description 'echo probe' and prompt 'Reply with exactly: child answer 42.'\n2. Then reply with the subagent's final answer verbatim.\n\nLet me do this step by step."}}},{"type":"chunk","time":1788199067994,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_oHPNQ1nLoakoaAGXIxCM7404","name":"subagent","arguments":"{\"description\": \"echo probe\", \"prompt\": \"Reply with exactly: child answer 42.\"}"}}},{"type":"chunk","time":1788199067994,"chunk":{"type":"usage","usage":{"inputTokens":135,"outputTokens":124,"cacheReadTokens":1664,"reasoningTokens":55}}},{"type":"chunk","time":1788199067994,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to:\n1. Use the subagent tool exactly once with description 'echo probe' and prompt 'Reply with exactly: child answer 42.'\n2. Then reply with the subagent's final answer verbatim.\n\nLet me do this step by step."},{"type":"tool-call","id":"call_00_oHPNQ1nLoakoaAGXIxCM7404","name":"subagent","arguments":"{\"description\": \"echo probe\", \"prompt\": \"Reply with exactly: child answer 42.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":135,"outputTokens":124,"cacheReadTokens":1664,"reasoningTokens":55},"stream":[{"type":"chunk","time":1788500539940,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788500539940,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to",":\n","1","."," Use"," the"," sub","agent"," tool"," exactly"," once"," with"," description"," '","echo"," probe","'"," and"," prompt"," '","Reply"," with"," exactly",":"," child"," answer"," ","42",".'\n","2","."," Then"," reply"," with"," the"," sub","agent","'s"," final"," answer"," verb","atim",".\n\n","Let"," me"," do"," this"," step"," by"," step","."]},{"type":"chunk","time":1788500539940,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500539940,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_oHPNQ1nLoakoaAGXIxCM7404","name":"subagent","args":["","{","\"","description","\"",": ","\"","echo"," probe","\"",", ","\"","prom","pt","\"",": ","\"","Reply"," with"," exactly",":"," child"," answer"," ","42",".","\"","}"]},{"type":"chunk","time":1788500539940,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to:\n1. Use the subagent tool exactly once with description 'echo probe' and prompt 'Reply with exactly: child answer 42.'\n2. Then reply with the subagent's final answer verbatim.\n\nLet me do this step by step."}}},{"type":"chunk","time":1788500539940,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_oHPNQ1nLoakoaAGXIxCM7404","name":"subagent","arguments":"{\"description\": \"echo probe\", \"prompt\": \"Reply with exactly: child answer 42.\"}"}}},{"type":"chunk","time":1788500539940,"chunk":{"type":"usage","usage":{"inputTokens":135,"outputTokens":124,"cacheReadTokens":1664,"reasoningTokens":55}}},{"type":"chunk","time":1788500539940,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_00_oHPNQ1nLoakoaAGXIxCM7404","name":"subagent","arguments":"{\"description\": \"echo probe\", \"prompt\": \"Reply with exactly: child answer 42.\"}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500539946,"mode":"one-shot","label":"echo probe"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_oHPNQ1nLoakoaAGXIxCM7404"},"content":[{"type":"tool-result","toolCallId":"call_00_oHPNQ1nLoakoaAGXIxCM7404","content":[{"type":"text","text":"Error: subagent run failed"}],"isError":true}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The subagent replied with \"child answer 42.\" Now I need to reply with the subagent's final answer verbatim."},{"type":"text","text":"child answer 42."}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":19,"outputTokens":32,"cacheReadTokens":1920,"reasoningTokens":26},"stream":[{"type":"chunk","time":1788199068048,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788199068048,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],"texts":["The"," sub","agent"," replied"," with"," \"","child"," answer"," ","42",".\""," Now"," I"," need"," to"," reply"," with"," the"," sub","agent","'s"," final"," answer"," verb","atim","."]},{"type":"chunk","time":1788199068049,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788199068049,"index":1,"dt":[0,0,0,0],"texts":["child"," answer"," ","42","."]},{"type":"chunk","time":1788199068049,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The subagent replied with \"child answer 42.\" Now I need to reply with the subagent's final answer verbatim."}}},{"type":"chunk","time":1788199068049,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"child answer 42."}}},{"type":"chunk","time":1788199068049,"chunk":{"type":"usage","usage":{"inputTokens":19,"outputTokens":32,"cacheReadTokens":1920,"reasoningTokens":26}}},{"type":"chunk","time":1788199068049,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The subagent replied with \"child answer 42.\" Now I need to reply with the subagent's final answer verbatim."},{"type":"text","text":"child answer 42."}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":19,"outputTokens":32,"cacheReadTokens":1920,"reasoningTokens":26},"stream":[{"type":"chunk","time":1788500540000,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788500540000,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," sub","agent"," replied"," with"," \"","child"," answer"," ","42",".\""," Now"," I"," need"," to"," reply"," with"," the"," sub","agent","'s"," final"," answer"," verb","atim","."]},{"type":"chunk","time":1788500540000,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788500540000,"index":1,"dt":[0,0,0,0],"texts":["child"," answer"," ","42","."]},{"type":"chunk","time":1788500540000,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The subagent replied with \"child answer 42.\" Now I need to reply with the subagent's final answer verbatim."}}},{"type":"chunk","time":1788500540000,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"child answer 42."}}},{"type":"chunk","time":1788500540000,"chunk":{"type":"usage","usage":{"inputTokens":19,"outputTokens":32,"cacheReadTokens":1920,"reasoningTokens":26}}},{"type":"chunk","time":1788500540000,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/advanced-toolchain-runtime/session.v2.jsonl b/snapshots/session/advanced-toolchain-runtime/session.v2.jsonl index b3772f0156..31a014ede7 100644 --- a/snapshots/session/advanced-toolchain-runtime/session.v2.jsonl +++ b/snapshots/session/advanced-toolchain-runtime/session.v2.jsonl @@ -11,30 +11,32 @@ {"type":"session/title","data":{"title":"Run this advanced flow exactly","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233482630,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233482630,"index":0,"dt":[],"id":"advanced-define","name":"cordis_define","args":["{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"]},{"type":"chunk","time":1788233482630,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}}},{"type":"chunk","time":1788233482630,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233482630,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788500543707,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500543708,"index":0,"dt":[],"id":"advanced-define","name":"cordis_define","args":["{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"]},{"type":"chunk","time":1788500543708,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}}},{"type":"chunk","time":1788500543708,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788500543708,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-define"},"content":[{"type":"tool-result","toolCallId":"advanced-define","content":[{"type":"text","text":"Defined snap-1/pkg-1 (Snapshot Marker); it is not running yet. Use cordis_run to activate this Package."}],"isError":false}],"role":"user","id":"{{message:4}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233482656,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233482656,"index":0,"dt":[],"id":"advanced-direct-child","name":"subagent","args":["{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788233482656,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788233482656,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233482656,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788500543727,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500543727,"index":0,"dt":[],"id":"advanced-direct-child","name":"subagent","args":["{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788500543727,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788500543727,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788500543727,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500543732,"mode":"one-shot","label":"Check direct child"}} {"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{message:6}}"}},"sourceEventSeqs":[18],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} -{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233482737,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233482737,"index":0,"dt":[],"id":"advanced-workflow","name":"workflow","args":["{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"]},{"type":"chunk","time":1788233482737,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"}}},{"type":"chunk","time":1788233482737,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233482737,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788500543788,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500543788,"index":0,"dt":[],"id":"advanced-workflow","name":"workflow","args":["{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"]},{"type":"chunk","time":1788500543788,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"}}},{"type":"chunk","time":1788500543788,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788500543788,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":3,"callId":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"}} {"type":"tool-workflow/run-start","data":{"runId":"{{workflow:1}}","name":"advanced-acp-snapshot"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:3}}","childCreatedAt":1788500543873,"mode":"one-shot"}} {"type":"tool-workflow/agent-start","data":{"runId":"{{workflow:1}}","seq":1,"label":"workflow-child","phase":"Delegate","childId":"{{session:3}}"}} {"type":"tool-workflow/agent-end","data":{"runId":"{{workflow:1}}","seq":1,"outcome":"completed"}} {"type":"tool-workflow/run-end","data":{"runId":"{{workflow:1}}","stopReason":"completed"}} -{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-acp-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{message:8}}"}},"sourceEventSeqs":[23],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-acp-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{message:8}}"}},"sourceEventSeqs":[24],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} {"type":"step/start","data":{"turn":1,"step":4}} -{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233482842,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233482842,"index":0,"dt":[],"id":"advanced-undefine","name":"cordis_undefine","args":["{\"pluginId\":\"snap-1\"}"]},{"type":"chunk","time":1788233482842,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}}},{"type":"chunk","time":1788233482842,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233482842,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788500543931,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500543931,"index":0,"dt":[],"id":"advanced-undefine","name":"cordis_undefine","args":["{\"pluginId\":\"snap-1\"}"]},{"type":"chunk","time":1788500543931,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}}},{"type":"chunk","time":1788500543931,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788500543931,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":4,"callId":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}} -{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{message:10}}"}},"sourceEventSeqs":[32],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{message:10}}"}},"sourceEventSeqs":[34],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":4}} {"type":"step/start","data":{"turn":1,"step":5}} -{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_ACP_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233482871,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788233482871,"index":0,"dt":[],"texts":["ADVANCED_ACP_OK"]},{"type":"chunk","time":1788233482871,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_ACP_OK"}}},{"type":"chunk","time":1788233482871,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233482871,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_ACP_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788500543947,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500543947,"index":0,"dt":[],"texts":["ADVANCED_ACP_OK"]},{"type":"chunk","time":1788500543947,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_ACP_OK"}}},{"type":"chunk","time":1788500543947,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788500543947,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":5}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/advanced-toolchain/session.v2.jsonl b/snapshots/session/advanced-toolchain/session.v2.jsonl index 35c5b2fbfb..8deeb37cc9 100644 --- a/snapshots/session/advanced-toolchain/session.v2.jsonl +++ b/snapshots/session/advanced-toolchain/session.v2.jsonl @@ -11,12 +11,12 @@ {"type":"session/title","data":{"title":"Run this advanced flow exactly","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233481631,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233481631,"index":0,"dt":[],"id":"advanced-define","name":"cordis_define","args":["{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"]},{"type":"chunk","time":1788233481631,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}}},{"type":"chunk","time":1788233481631,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233481631,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788500542227,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500542227,"index":0,"dt":[],"id":"advanced-define","name":"cordis_define","args":["{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"]},{"type":"chunk","time":1788500542227,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}}},{"type":"chunk","time":1788500542227,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788500542227,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-define"},"content":[{"type":"tool-result","toolCallId":"advanced-define","content":[{"type":"text","text":"Defined snap-1/pkg-1 (Snapshot Marker); it is not running yet. Use cordis_run to activate this Package."}],"isError":false}],"role":"user","id":"{{message:4}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233481651,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233481651,"index":0,"dt":[],"id":"advanced-code","name":"run_code","args":["{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"]},{"type":"chunk","time":1788233481651,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"}}},{"type":"chunk","time":1788233481651,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233481651,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788500542247,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500542247,"index":0,"dt":[],"id":"advanced-code","name":"run_code","args":["{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"]},{"type":"chunk","time":1788500542247,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"}}},{"type":"chunk","time":1788500542247,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788500542247,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"advanced-code","name":"run_code","arguments":"{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"}} {"type":"tool/code-dispatch-start","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"cordis_run","arguments":{"pluginId":"snap-1","packageId":"pkg-1","mode":"run"}}} {"type":"tool/code-dispatch","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"cordis_run","arguments":{"pluginId":"snap-1","packageId":"pkg-1","mode":"run"},"isError":false,"content":[{"type":"text","text":"snap-1/pkg-1 is running (run-1)."}]}} @@ -25,25 +25,27 @@ {"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-code"},"content":[{"type":"tool-result","toolCallId":"advanced-code","content":[{"type":"text","text":"{\n \"run\": {\n \"status\": \"running\",\n \"pluginId\": \"snap-1\",\n \"packageId\": \"pkg-1\",\n \"pluginRunId\": \"run-1\",\n \"currentPackageId\": \"pkg-1\",\n \"host\": {\n \"status\": \"running\",\n \"provides\": [],\n \"waitingFor\": []\n },\n \"client\": {\n \"status\": \"absent\",\n \"waitingFor\": []\n }\n },\n \"inspected\": {\n \"mode\": \"plugin\",\n \"pluginId\": \"snap-1\",\n \"name\": \"Snapshot Marker\",\n \"packageCount\": 1,\n \"state\": \"running\",\n \"currentPackageId\": \"pkg-1\",\n \"activeRun\": {\n \"pluginRunId\": \"run-1\",\n \"packageId\": \"pkg-1\"\n },\n \"packages\": [\n {\n \"packageId\": \"pkg-1\",\n \"name\": \"Snapshot Marker\",\n \"purpose\": \"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\n \"hasHostHalf\": true,\n \"hasClientHalf\": false,\n \"isCurrent\": true,\n \"isNext\": false\n }\n ]\n }\n}"}],"isError":false}],"role":"user","id":"{{message:6}}"}},"sourceEventSeqs":[18],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} -{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233481726,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233481726,"index":0,"dt":[],"id":"advanced-direct-child","name":"subagent","args":["{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788233481726,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788233481726,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233481726,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788500542326,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500542326,"index":0,"dt":[],"id":"advanced-direct-child","name":"subagent","args":["{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788500542326,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788500542326,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788500542326,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":3,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500542331,"mode":"one-shot","label":"Check direct child"}} {"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{message:8}}"}},"sourceEventSeqs":[27],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} {"type":"step/start","data":{"turn":1,"step":4}} -{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233481803,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233481803,"index":0,"dt":[],"id":"advanced-workflow","name":"workflow","args":["{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"]},{"type":"chunk","time":1788233481803,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"}}},{"type":"chunk","time":1788233481803,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233481803,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788500542391,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500542391,"index":0,"dt":[],"id":"advanced-workflow","name":"workflow","args":["{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"]},{"type":"chunk","time":1788500542391,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"}}},{"type":"chunk","time":1788500542391,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788500542391,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":4,"callId":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"}} {"type":"tool-workflow/run-start","data":{"runId":"{{workflow:1}}","name":"advanced-headless-snapshot"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:3}}","childCreatedAt":1788500542517,"mode":"one-shot"}} {"type":"tool-workflow/agent-start","data":{"runId":"{{workflow:1}}","seq":1,"label":"workflow-child","phase":"Delegate","childId":"{{session:3}}"}} {"type":"tool-workflow/agent-end","data":{"runId":"{{workflow:1}}","seq":1,"outcome":"completed"}} {"type":"tool-workflow/run-end","data":{"runId":"{{workflow:1}}","stopReason":"completed"}} -{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-headless-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{message:10}}"}},"sourceEventSeqs":[32],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-headless-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{message:10}}"}},"sourceEventSeqs":[33],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":4}} {"type":"step/start","data":{"turn":1,"step":5}} -{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233481908,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233481908,"index":0,"dt":[],"id":"advanced-undefine","name":"cordis_undefine","args":["{\"pluginId\":\"snap-1\"}"]},{"type":"chunk","time":1788233481908,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}}},{"type":"chunk","time":1788233481908,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233481908,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788500542582,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500542582,"index":0,"dt":[],"id":"advanced-undefine","name":"cordis_undefine","args":["{\"pluginId\":\"snap-1\"}"]},{"type":"chunk","time":1788500542582,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}}},{"type":"chunk","time":1788500542582,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788500542582,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":5,"callId":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}} -{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{message:12}}"}},"sourceEventSeqs":[41],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{message:12}}"}},"sourceEventSeqs":[43],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":5}} {"type":"step/start","data":{"turn":1,"step":6}} -{"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_HEADLESS_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:13}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233481936,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788233481936,"index":0,"dt":[],"texts":["ADVANCED_HEADLESS_OK"]},{"type":"chunk","time":1788233481936,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_HEADLESS_OK"}}},{"type":"chunk","time":1788233481936,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233481936,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_HEADLESS_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:13}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788500542598,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500542598,"index":0,"dt":[],"texts":["ADVANCED_HEADLESS_OK"]},{"type":"chunk","time":1788500542598,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_HEADLESS_OK"}}},{"type":"chunk","time":1788500542598,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788500542598,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":6}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/headless.snapshot.ts b/snapshots/session/headless.snapshot.ts index f6f2212726..b10a87b47f 100644 --- a/snapshots/session/headless.snapshot.ts +++ b/snapshots/session/headless.snapshot.ts @@ -26,6 +26,7 @@ import { normalizedHeaders, normalizedSystemPrompts, normalizedToolSchemas, + omitSubagentCatalogForHistoricalComparison, parseSnapshotManifest, parseToolSchemasSnapshot, redactSessionSnapshotIds, @@ -975,6 +976,9 @@ describe('headless recorded-session snapshots', () => { expect(await fixtureSessions(scenario), 'historical replay input remains unchanged').toEqual(fixtures) } const actualSnapshots = normalizeSessionSnapshots(actualLogs.map(log => log.content), actualContext) + .map(snapshot => scenario.manifest.sessionFormat === undefined + ? snapshot + : omitSubagentCatalogForHistoricalComparison(snapshot)) const expectedSnapshots = normalizeSessionSnapshots(expected, contextOf(expected)) for (const [index, actual] of actualSnapshots.entries()) { expect(records(actual), `${scenario.name}: session ${index}`).toEqual(records(expectedSnapshots[index] as string)) diff --git a/snapshots/session/ralph-loop/session.v2.jsonl b/snapshots/session/ralph-loop/session.v2.jsonl index a7c6797092..aedf01a58c 100644 --- a/snapshots/session/ralph-loop/session.v2.jsonl +++ b/snapshots/session/ralph-loop/session.v2.jsonl @@ -11,11 +11,13 @@ {"type":"session/title","data":{"title":"Run a two-round fresh-agent Ralph","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_ralph","name":"ralph","arguments":"{\"objective\":\"Prove two fresh Ralph rounds through the shipped headless app.\",\"maxRounds\":2}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":20,"outputTokens":8},"stream":[{"type":"chunk","time":1788233515711,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233515711,"index":0,"dt":[],"id":"call_ralph","name":"ralph","args":["{\"objective\":\"Prove two fresh Ralph rounds through the shipped headless app.\",\"maxRounds\":2}"]},{"type":"chunk","time":1788233515711,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_ralph","name":"ralph","arguments":"{\"objective\":\"Prove two fresh Ralph rounds through the shipped headless app.\",\"maxRounds\":2}"}}},{"type":"chunk","time":1788233515711,"chunk":{"type":"usage","usage":{"inputTokens":20,"outputTokens":8}}},{"type":"chunk","time":1788233515711,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_ralph","name":"ralph","arguments":"{\"objective\":\"Prove two fresh Ralph rounds through the shipped headless app.\",\"maxRounds\":2}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":20,"outputTokens":8},"stream":[{"type":"chunk","time":1788500544849,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500544849,"index":0,"dt":[],"id":"call_ralph","name":"ralph","args":["{\"objective\":\"Prove two fresh Ralph rounds through the shipped headless app.\",\"maxRounds\":2}"]},{"type":"chunk","time":1788500544849,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_ralph","name":"ralph","arguments":"{\"objective\":\"Prove two fresh Ralph rounds through the shipped headless app.\",\"maxRounds\":2}"}}},{"type":"chunk","time":1788500544849,"chunk":{"type":"usage","usage":{"inputTokens":20,"outputTokens":8}}},{"type":"chunk","time":1788500544849,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_ralph","name":"ralph","arguments":"{\"objective\":\"Prove two fresh Ralph rounds through the shipped headless app.\",\"maxRounds\":2}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500544927,"mode":"one-shot"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:3}}","childCreatedAt":1788500544976,"mode":"one-shot"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_ralph"},"content":[{"type":"tool-result","toolCallId":"call_ralph","content":[{"type":"text","text":"Ralph worker reported completion after 2 rounds.\nFinal report:\n{\n \"status\": \"complete\",\n \"summary\": \"The Ralph snapshot objective is complete.\",\n \"evidence\": [\n \"Two fresh rounds completed through the shipped app.\"\n ],\n \"nextSteps\": [],\n \"blocker\": \"\"\n}"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"RALPH SNAPSHOT COMPLETE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":30,"outputTokens":4},"stream":[{"type":"chunk","time":1788233515902,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788233515902,"index":0,"dt":[],"texts":["RALPH SNAPSHOT COMPLETE"]},{"type":"chunk","time":1788233515902,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"RALPH SNAPSHOT COMPLETE"}}},{"type":"chunk","time":1788233515902,"chunk":{"type":"usage","usage":{"inputTokens":30,"outputTokens":4}}},{"type":"chunk","time":1788233515902,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"RALPH SNAPSHOT COMPLETE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":30,"outputTokens":4},"stream":[{"type":"chunk","time":1788500545054,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500545054,"index":0,"dt":[],"texts":["RALPH SNAPSHOT COMPLETE"]},{"type":"chunk","time":1788500545054,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"RALPH SNAPSHOT COMPLETE"}}},{"type":"chunk","time":1788500545054,"chunk":{"type":"usage","usage":{"inputTokens":30,"outputTokens":4}}},{"type":"chunk","time":1788500545054,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/subagent-child-question-rejection/session.v2.jsonl b/snapshots/session/subagent-child-question-rejection/session.v2.jsonl index 13ab513d72..7d11041678 100644 --- a/snapshots/session/subagent-child-question-rejection/session.v2.jsonl +++ b/snapshots/session/subagent-child-question-rejection/session.v2.jsonl @@ -11,11 +11,12 @@ {"type":"session/title","data":{"title":"Delegate one question check. Ask","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_question_child","name":"subagent","arguments":"{\"description\":\"Check deployment question\",\"prompt\":\"Call ask_user_question once to ask whether deployment should use the CUDA fallback. If the tool returns an error, include the unresolved question verbatim in your final result.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788233523819,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233523819,"index":0,"dt":[],"id":"call_question_child","name":"subagent","args":["{\"description\":\"Check deployment question\",\"prompt\":\"Call ask_user_question once to ask whether deployment should use the CUDA fallback. If the tool returns an error, include the unresolved question verbatim in your final result.\", \"run_in_background\":false}"]},{"type":"chunk","time":1788233523819,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_question_child","name":"subagent","arguments":"{\"description\":\"Check deployment question\",\"prompt\":\"Call ask_user_question once to ask whether deployment should use the CUDA fallback. If the tool returns an error, include the unresolved question verbatim in your final result.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788233523819,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788233523819,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_question_child","name":"subagent","arguments":"{\"description\":\"Check deployment question\",\"prompt\":\"Call ask_user_question once to ask whether deployment should use the CUDA fallback. If the tool returns an error, include the unresolved question verbatim in your final result.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500547269,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500547269,"index":0,"dt":[],"id":"call_question_child","name":"subagent","args":["{\"description\":\"Check deployment question\",\"prompt\":\"Call ask_user_question once to ask whether deployment should use the CUDA fallback. If the tool returns an error, include the unresolved question verbatim in your final result.\", \"run_in_background\":false}"]},{"type":"chunk","time":1788500547269,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_question_child","name":"subagent","arguments":"{\"description\":\"Check deployment question\",\"prompt\":\"Call ask_user_question once to ask whether deployment should use the CUDA fallback. If the tool returns an error, include the unresolved question verbatim in your final result.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788500547270,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500547270,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_question_child","name":"subagent","arguments":"{\"description\":\"Check deployment question\",\"prompt\":\"Call ask_user_question once to ask whether deployment should use the CUDA fallback. If the tool returns an error, include the unresolved question verbatim in your final result.\", \"run_in_background\":false}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500547276,"mode":"one-shot","label":"Check deployment question"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_question_child"},"content":[{"type":"tool-result","toolCallId":"call_question_child","content":[{"type":"text","text":"UNRESOLVED: Should deployment use the CUDA fallback?"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_COMPLETED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788233523914,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788233523915,"index":0,"dt":[],"texts":["PARENT_COMPLETED"]},{"type":"chunk","time":1788233523915,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_COMPLETED"}}},{"type":"chunk","time":1788233523915,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788233523915,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_COMPLETED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788500547345,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500547345,"index":0,"dt":[],"texts":["PARENT_COMPLETED"]},{"type":"chunk","time":1788500547345,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_COMPLETED"}}},{"type":"chunk","time":1788500547345,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788500547345,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/subagent-depth-two-rejection/session.1.v2.jsonl b/snapshots/session/subagent-depth-two-rejection/session.1.v2.jsonl index 459b241c5d..98334abe6f 100644 --- a/snapshots/session/subagent-depth-two-rejection/session.1.v2.jsonl +++ b/snapshots/session/subagent-depth-two-rejection/session.1.v2.jsonl @@ -12,11 +12,12 @@ {"type":"session/title","data":{"title":"Call subagent once. Ask that","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_depth_one_child","name":"subagent","arguments":"{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788233524598,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233524598,"index":0,"dt":[],"id":"call_depth_one_child","name":"subagent","args":["{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788233524598,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_depth_one_child","name":"subagent","arguments":"{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788233524598,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788233524598,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_depth_one_child","name":"subagent","arguments":"{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500548365,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500548366,"index":0,"dt":[],"id":"call_depth_one_child","name":"subagent","args":["{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788500548366,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_depth_one_child","name":"subagent","arguments":"{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788500548366,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500548366,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_depth_one_child","name":"subagent","arguments":"{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\",\"run_in_background\":false}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:3}}","childCreatedAt":1788500548373,"mode":"one-shot","label":"Start depth two"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_depth_one_child"},"content":[{"type":"tool-result","toolCallId":"call_depth_one_child","content":[{"type":"text","text":"DEPTH_REJECTED"}],"isError":false}],"role":"user","id":"{{message:9}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DEPTH_ONE_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788233524703,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788233524703,"index":0,"dt":[],"texts":["DEPTH_ONE_DONE"]},{"type":"chunk","time":1788233524703,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DEPTH_ONE_DONE"}}},{"type":"chunk","time":1788233524703,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788233524703,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DEPTH_ONE_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788500548476,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500548476,"index":0,"dt":[],"texts":["DEPTH_ONE_DONE"]},{"type":"chunk","time":1788500548477,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DEPTH_ONE_DONE"}}},{"type":"chunk","time":1788500548477,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788500548477,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/subagent-depth-two-rejection/session.v2.jsonl b/snapshots/session/subagent-depth-two-rejection/session.v2.jsonl index 764ab99c92..e4b17cd756 100644 --- a/snapshots/session/subagent-depth-two-rejection/session.v2.jsonl +++ b/snapshots/session/subagent-depth-two-rejection/session.v2.jsonl @@ -11,11 +11,12 @@ {"type":"session/title","data":{"title":"Delegate through two child generations.","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_root_child","name":"subagent","arguments":"{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788233524552,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233524552,"index":0,"dt":[],"id":"call_root_child","name":"subagent","args":["{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788233524552,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_root_child","name":"subagent","arguments":"{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788233524553,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788233524553,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_root_child","name":"subagent","arguments":"{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500548303,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500548303,"index":0,"dt":[],"id":"call_root_child","name":"subagent","args":["{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788500548303,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_root_child","name":"subagent","arguments":"{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788500548303,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500548303,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_root_child","name":"subagent","arguments":"{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\",\"run_in_background\":false}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500548310,"mode":"one-shot","label":"Start depth one"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_root_child"},"content":[{"type":"tool-result","toolCallId":"call_root_child","content":[{"type":"text","text":"DEPTH_ONE_DONE"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"ROOT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788233524723,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788233524723,"index":0,"dt":[],"texts":["ROOT_DONE"]},{"type":"chunk","time":1788233524723,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ROOT_DONE"}}},{"type":"chunk","time":1788233524723,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788233524723,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"ROOT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788500548498,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500548498,"index":0,"dt":[],"texts":["ROOT_DONE"]},{"type":"chunk","time":1788500548498,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ROOT_DONE"}}},{"type":"chunk","time":1788500548498,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788500548498,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/subagent-max-tokens-partial/session.v2.jsonl b/snapshots/session/subagent-max-tokens-partial/session.v2.jsonl index fc7b96f216..82120de5f6 100644 --- a/snapshots/session/subagent-max-tokens-partial/session.v2.jsonl +++ b/snapshots/session/subagent-max-tokens-partial/session.v2.jsonl @@ -11,11 +11,12 @@ {"type":"session/title","data":{"title":"Use the subagent tool exactly","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_parent_1","name":"subagent","arguments":"{\"description\": \"Truncated child\", \"prompt\": \"Write the words 'partial one', call todo_write once, then keep going until you are cut off.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788233525350,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"chunk","time":1788233525350,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_parent_1","name":"subagent","arguments":"{\"description\": \"Truncated child\", \"prompt\": \"Write the words 'partial one', call todo_write once, then keep going until you are cut off.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788233525350,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788233525350,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_parent_1","name":"subagent","arguments":"{\"description\": \"Truncated child\", \"prompt\": \"Write the words 'partial one', call todo_write once, then keep going until you are cut off.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500549527,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"chunk","time":1788500549527,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_parent_1","name":"subagent","arguments":"{\"description\": \"Truncated child\", \"prompt\": \"Write the words 'partial one', call todo_write once, then keep going until you are cut off.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788500549527,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500549527,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_parent_1","name":"subagent","arguments":"{\"description\": \"Truncated child\", \"prompt\": \"Write the words 'partial one', call todo_write once, then keep going until you are cut off.\", \"run_in_background\":false}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500549533,"mode":"one-shot","label":"Truncated child"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_parent_1"},"content":[{"type":"tool-result","toolCallId":"call_parent_1","content":[{"type":"text","text":"Error: subagent run hit its token limit before finishing\nPartial output before the run ended:\npartial one"}],"isError":true}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":12,"outputTokens":2},"stream":[{"type":"chunk","time":1788233525447,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":1788233525447,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788233525447,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2}}},{"type":"chunk","time":1788233525447,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":12,"outputTokens":2},"stream":[{"type":"chunk","time":1788500549606,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":1788500549606,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788500549606,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2}}},{"type":"chunk","time":1788500549606,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/subagent-parallel/session.v2.jsonl b/snapshots/session/subagent-parallel/session.v2.jsonl index a01d33384e..2cbca0e0fb 100644 --- a/snapshots/session/subagent-parallel/session.v2.jsonl +++ b/snapshots/session/subagent-parallel/session.v2.jsonl @@ -11,13 +11,15 @@ {"type":"session/title","data":{"title":"Use the subagent tool TWICE","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_parallel_alpha_1","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"},{"type":"tool-call","id":"call_parallel_alpha_2","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"stream":[{"type":"chunk","time":1788233526821,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"chunk","time":1788233526821,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_parallel_alpha_1","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788233526821,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"chunk","time":1788233526821,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_parallel_alpha_2","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788233526821,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_parallel_alpha_1","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"},{"type":"tool-call","id":"call_parallel_alpha_2","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"stream":[{"type":"chunk","time":1788500551334,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"chunk","time":1788500551334,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_parallel_alpha_1","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788500551334,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"chunk","time":1788500551334,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_parallel_alpha_2","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788500551334,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_parallel_alpha_1","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_parallel_alpha_2","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500551344,"mode":"one-shot","label":"Say the word ALPHA"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:3}}","childCreatedAt":1788500551344,"mode":"one-shot","label":"Say the word ALPHA"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_parallel_alpha_1"},"content":[{"type":"tool-result","toolCallId":"call_parallel_alpha_1","content":[{"type":"text","text":"ALPHA"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_parallel_alpha_2"},"content":[{"type":"tool-result","toolCallId":"call_parallel_alpha_2","content":[{"type":"text","text":"ALPHA"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"stream":[{"type":"chunk","time":1788233526904,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":1788233526904,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788233526904,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"stream":[{"type":"chunk","time":1788500551413,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":1788500551413,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788500551413,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/subagent-published-run-failure/session.v2.jsonl b/snapshots/session/subagent-published-run-failure/session.v2.jsonl index 62755d8d49..e723042e93 100644 --- a/snapshots/session/subagent-published-run-failure/session.v2.jsonl +++ b/snapshots/session/subagent-published-run-failure/session.v2.jsonl @@ -11,11 +11,12 @@ {"type":"session/title","data":{"title":"Delegate one foreground subagent. Its","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_published_failure","name":"subagent","arguments":"{\"description\":\"Fail published run\",\"prompt\":\"This child prompt must never run.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788233527588,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233527588,"index":0,"dt":[],"id":"call_published_failure","name":"subagent","args":["{\"description\":\"Fail published run\",\"prompt\":\"This child prompt must never run.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788233527588,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_published_failure","name":"subagent","arguments":"{\"description\":\"Fail published run\",\"prompt\":\"This child prompt must never run.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788233527588,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788233527588,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_published_failure","name":"subagent","arguments":"{\"description\":\"Fail published run\",\"prompt\":\"This child prompt must never run.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500552312,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500552312,"index":0,"dt":[],"id":"call_published_failure","name":"subagent","args":["{\"description\":\"Fail published run\",\"prompt\":\"This child prompt must never run.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788500552312,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_published_failure","name":"subagent","arguments":"{\"description\":\"Fail published run\",\"prompt\":\"This child prompt must never run.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788500552312,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500552312,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_published_failure","name":"subagent","arguments":"{\"description\":\"Fail published run\",\"prompt\":\"This child prompt must never run.\",\"run_in_background\":false}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500552318,"mode":"one-shot","label":"Fail published run"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_published_failure"},"content":[{"type":"tool-result","toolCallId":"call_published_failure","content":[{"type":"text","text":"Error: subagent run failed: Error: snapshot published run failed; dispose failed: Error: snapshot published handle disposal failed"}],"isError":true}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_OBSERVED_ERROR"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788233527613,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788233527613,"index":0,"dt":[],"texts":["PARENT_OBSERVED_ERROR"]},{"type":"chunk","time":1788233527613,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_OBSERVED_ERROR"}}},{"type":"chunk","time":1788233527613,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788233527613,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_OBSERVED_ERROR"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788500552354,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500552354,"index":0,"dt":[],"texts":["PARENT_OBSERVED_ERROR"]},{"type":"chunk","time":1788500552354,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_OBSERVED_ERROR"}}},{"type":"chunk","time":1788500552354,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788500552354,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/subagent-spawn-in-process/session.v2.jsonl b/snapshots/session/subagent-spawn-in-process/session.v2.jsonl index 81f85f4de9..a2f3f9c449 100644 --- a/snapshots/session/subagent-spawn-in-process/session.v2.jsonl +++ b/snapshots/session/subagent-spawn-in-process/session.v2.jsonl @@ -11,11 +11,12 @@ {"type":"session/title","data":{"title":"Use the subagent tool exactly","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to:\n1. Use the subagent tool exactly once to delegate the task: \"Reply with exactly the word CHILD_OK and nothing else.\"\n2. After the subagent returns, reply with the single word PARENT_DONE and stop.\n3. Do not use the bash tool.\n\nLet me do this."},{"type":"tool-call","id":"call_00_gVbLWC12Qu8JheZpVRRz8749","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":2907,"outputTokens":142,"cacheReadTokens":0,"reasoningTokens":67},"stream":[{"type":"chunk","time":1788233528227,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788233528227,"index":0,"dt":[0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to",":\n","1","."," Use"," the"," sub","agent"," tool"," exactly"," once"," to"," delegate"," the"," task",":"," \"","Reply"," with"," exactly"," the"," word"," CH","ILD","_OK"," and"," nothing"," else",".\"\n","2","."," After"," the"," sub","agent"," returns",","," reply"," with"," the"," single"," word"," PAR","ENT","_D","ONE"," and"," stop",".\n","3","."," Do"," not"," use"," the"," bash"," tool",".\n\n","Let"," me"," do"," this","."]},{"type":"chunk","time":1788233528228,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233528228,"index":1,"dt":[0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_gVbLWC12Qu8JheZpVRRz8749","name":"subagent","args":["","{","\"","description","\"",": ","\"","Reply"," with"," CH","ILD","_OK","\"",", ","\"","prom","pt","\"",": ","\"","Reply"," with"," exactly"," the"," word"," CH","ILD","_OK"," and"," nothing"," else",".","\"",", \"run_in_background\":false}"]},{"type":"chunk","time":1788233528229,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to:\n1. Use the subagent tool exactly once to delegate the task: \"Reply with exactly the word CHILD_OK and nothing else.\"\n2. After the subagent returns, reply with the single word PARENT_DONE and stop.\n3. Do not use the bash tool.\n\nLet me do this."}}},{"type":"chunk","time":1788233528229,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_gVbLWC12Qu8JheZpVRRz8749","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788233528229,"chunk":{"type":"usage","usage":{"inputTokens":2907,"outputTokens":142,"cacheReadTokens":0,"reasoningTokens":67}}},{"type":"chunk","time":1788233528229,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to:\n1. Use the subagent tool exactly once to delegate the task: \"Reply with exactly the word CHILD_OK and nothing else.\"\n2. After the subagent returns, reply with the single word PARENT_DONE and stop.\n3. Do not use the bash tool.\n\nLet me do this."},{"type":"tool-call","id":"call_00_gVbLWC12Qu8JheZpVRRz8749","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":2907,"outputTokens":142,"cacheReadTokens":0,"reasoningTokens":67},"stream":[{"type":"chunk","time":1788500553301,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788500553301,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to",":\n","1","."," Use"," the"," sub","agent"," tool"," exactly"," once"," to"," delegate"," the"," task",":"," \"","Reply"," with"," exactly"," the"," word"," CH","ILD","_OK"," and"," nothing"," else",".\"\n","2","."," After"," the"," sub","agent"," returns",","," reply"," with"," the"," single"," word"," PAR","ENT","_D","ONE"," and"," stop",".\n","3","."," Do"," not"," use"," the"," bash"," tool",".\n\n","Let"," me"," do"," this","."]},{"type":"chunk","time":1788500553302,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500553302,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_gVbLWC12Qu8JheZpVRRz8749","name":"subagent","args":["","{","\"","description","\"",": ","\"","Reply"," with"," CH","ILD","_OK","\"",", ","\"","prom","pt","\"",": ","\"","Reply"," with"," exactly"," the"," word"," CH","ILD","_OK"," and"," nothing"," else",".","\"",", \"run_in_background\":false}"]},{"type":"chunk","time":1788500553302,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to:\n1. Use the subagent tool exactly once to delegate the task: \"Reply with exactly the word CHILD_OK and nothing else.\"\n2. After the subagent returns, reply with the single word PARENT_DONE and stop.\n3. Do not use the bash tool.\n\nLet me do this."}}},{"type":"chunk","time":1788500553302,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_gVbLWC12Qu8JheZpVRRz8749","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788500553302,"chunk":{"type":"usage","usage":{"inputTokens":2907,"outputTokens":142,"cacheReadTokens":0,"reasoningTokens":67}}},{"type":"chunk","time":1788500553302,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_00_gVbLWC12Qu8JheZpVRRz8749","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\":false}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500553308,"mode":"one-shot","label":"Reply with CHILD_OK"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_gVbLWC12Qu8JheZpVRRz8749"},"content":[{"type":"tool-result","toolCallId":"call_00_gVbLWC12Qu8JheZpVRRz8749","content":[{"type":"text","text":"CHILD_OK"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The subagent returned \"CHILD_OK\" as expected. Now I need to reply with the single word \"PARENT_DONE\" and stop."},{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":120,"outputTokens":35,"cacheReadTokens":2944,"reasoningTokens":30},"stream":[{"type":"chunk","time":1788233528286,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788233528286,"index":0,"dt":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," sub","agent"," returned"," \"","CH","ILD","_OK","\""," as"," expected","."," Now"," I"," need"," to"," reply"," with"," the"," single"," word"," \"","PAR","ENT","_D","ONE","\""," and"," stop","."]},{"type":"chunk","time":1788233528287,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788233528287,"index":1,"dt":[0,0,0],"texts":["PAR","ENT","_D","ONE"]},{"type":"chunk","time":1788233528287,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The subagent returned \"CHILD_OK\" as expected. Now I need to reply with the single word \"PARENT_DONE\" and stop."}}},{"type":"chunk","time":1788233528287,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788233528287,"chunk":{"type":"usage","usage":{"inputTokens":120,"outputTokens":35,"cacheReadTokens":2944,"reasoningTokens":30}}},{"type":"chunk","time":1788233528287,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The subagent returned \"CHILD_OK\" as expected. Now I need to reply with the single word \"PARENT_DONE\" and stop."},{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":120,"outputTokens":35,"cacheReadTokens":2944,"reasoningTokens":30},"stream":[{"type":"chunk","time":1788500553369,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788500553369,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," sub","agent"," returned"," \"","CH","ILD","_OK","\""," as"," expected","."," Now"," I"," need"," to"," reply"," with"," the"," single"," word"," \"","PAR","ENT","_D","ONE","\""," and"," stop","."]},{"type":"chunk","time":1788500553369,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788500553369,"index":1,"dt":[0,0,0],"texts":["PAR","ENT","_D","ONE"]},{"type":"chunk","time":1788500553369,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The subagent returned \"CHILD_OK\" as expected. Now I need to reply with the single word \"PARENT_DONE\" and stop."}}},{"type":"chunk","time":1788500553369,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788500553369,"chunk":{"type":"usage","usage":{"inputTokens":120,"outputTokens":35,"cacheReadTokens":2944,"reasoningTokens":30}}},{"type":"chunk","time":1788500553369,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/workflow-run/session.v2.jsonl b/snapshots/session/workflow-run/session.v2.jsonl index f9285f97f6..46bd43686a 100644 --- a/snapshots/session/workflow-run/session.v2.jsonl +++ b/snapshots/session/workflow-run/session.v2.jsonl @@ -11,15 +11,16 @@ {"type":"session/title","data":{"title":"Use the workflow tool exactly","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to use the workflow tool exactly once with specific parameters. Let me carefully follow the instructions:\n\n1. args omitted (so I don't include it)\n2. meta = { \"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\" }\n3. script = as given verbatim\n4. After it returns, reply with \"WORKFLOW_DONE\"\n\nLet me do exactly that."},{"type":"tool-call","id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","arguments":"{\"meta\": {\"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\"}, \"script\": \"\\nphase('Run')\\nconst reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.')\\nreturn { reply }\\n\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":3174,"outputTokens":191,"cacheReadTokens":0,"reasoningTokens":88},"stream":[{"type":"chunk","time":1788233532122,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788233532122,"index":0,"dt":[0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," use"," the"," workflow"," tool"," exactly"," once"," with"," specific"," parameters","."," Let"," me"," carefully"," follow"," the"," instructions",":\n\n","1","."," args"," omitted"," (","so"," I"," don","'t"," include"," it",")\n","2","."," meta"," ="," {"," \"","name","\":"," \"","sn","apshot","-flow","\","," \"","description","\":"," \"","one"," child"," for"," the"," snapshot","\""," }\n","3","."," script"," ="," as"," given"," verb","atim","\n","4","."," After"," it"," returns",","," reply"," with"," \"","WORK","FL","OW","_D","ONE","\"\n\n","Let"," me"," do"," exactly"," that","."]},{"type":"chunk","time":1788233532123,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233532123,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","args":["","{","\"","meta","\"",": ","{\"","name","\":"," \"","sn","apshot","-flow","\","," \"","description","\":"," \"","one"," child"," for"," the"," snapshot","\"","}",", ","\"","script","\"",": ","\"","\\n","phase","('","Run","')\\n","const"," reply"," ="," await"," agent","('","Reply"," with"," exactly"," the"," word"," WF","_CH","ILD","_OK"," and"," nothing"," else",".')\\n","return"," {"," reply"," }\\n","\"","}"]},{"type":"chunk","time":1788233532123,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to use the workflow tool exactly once with specific parameters. Let me carefully follow the instructions:\n\n1. args omitted (so I don't include it)\n2. meta = { \"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\" }\n3. script = as given verbatim\n4. After it returns, reply with \"WORKFLOW_DONE\"\n\nLet me do exactly that."}}},{"type":"chunk","time":1788233532124,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","arguments":"{\"meta\": {\"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\"}, \"script\": \"\\nphase('Run')\\nconst reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.')\\nreturn { reply }\\n\"}"}}},{"type":"chunk","time":1788233532124,"chunk":{"type":"usage","usage":{"inputTokens":3174,"outputTokens":191,"cacheReadTokens":0,"reasoningTokens":88}}},{"type":"chunk","time":1788233532124,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to use the workflow tool exactly once with specific parameters. Let me carefully follow the instructions:\n\n1. args omitted (so I don't include it)\n2. meta = { \"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\" }\n3. script = as given verbatim\n4. After it returns, reply with \"WORKFLOW_DONE\"\n\nLet me do exactly that."},{"type":"tool-call","id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","arguments":"{\"meta\": {\"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\"}, \"script\": \"\\nphase('Run')\\nconst reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.')\\nreturn { reply }\\n\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":3174,"outputTokens":191,"cacheReadTokens":0,"reasoningTokens":88},"stream":[{"type":"chunk","time":1788500554310,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788500554310,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," use"," the"," workflow"," tool"," exactly"," once"," with"," specific"," parameters","."," Let"," me"," carefully"," follow"," the"," instructions",":\n\n","1","."," args"," omitted"," (","so"," I"," don","'t"," include"," it",")\n","2","."," meta"," ="," {"," \"","name","\":"," \"","sn","apshot","-flow","\","," \"","description","\":"," \"","one"," child"," for"," the"," snapshot","\""," }\n","3","."," script"," ="," as"," given"," verb","atim","\n","4","."," After"," it"," returns",","," reply"," with"," \"","WORK","FL","OW","_D","ONE","\"\n\n","Let"," me"," do"," exactly"," that","."]},{"type":"chunk","time":1788500554311,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500554311,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","args":["","{","\"","meta","\"",": ","{\"","name","\":"," \"","sn","apshot","-flow","\","," \"","description","\":"," \"","one"," child"," for"," the"," snapshot","\"","}",", ","\"","script","\"",": ","\"","\\n","phase","('","Run","')\\n","const"," reply"," ="," await"," agent","('","Reply"," with"," exactly"," the"," word"," WF","_CH","ILD","_OK"," and"," nothing"," else",".')\\n","return"," {"," reply"," }\\n","\"","}"]},{"type":"chunk","time":1788500554311,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to use the workflow tool exactly once with specific parameters. Let me carefully follow the instructions:\n\n1. args omitted (so I don't include it)\n2. meta = { \"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\" }\n3. script = as given verbatim\n4. After it returns, reply with \"WORKFLOW_DONE\"\n\nLet me do exactly that."}}},{"type":"chunk","time":1788500554311,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","arguments":"{\"meta\": {\"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\"}, \"script\": \"\\nphase('Run')\\nconst reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.')\\nreturn { reply }\\n\"}"}}},{"type":"chunk","time":1788500554311,"chunk":{"type":"usage","usage":{"inputTokens":3174,"outputTokens":191,"cacheReadTokens":0,"reasoningTokens":88}}},{"type":"chunk","time":1788500554311,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","arguments":"{\"meta\": {\"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\"}, \"script\": \"\\nphase('Run')\\nconst reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.')\\nreturn { reply }\\n\"}"}} {"type":"tool-workflow/run-start","data":{"runId":"{{workflow:1}}","name":"snapshot-flow"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500554400,"mode":"one-shot"}} {"type":"tool-workflow/agent-start","data":{"runId":"{{workflow:1}}","seq":1,"label":"Reply with exactly the word WF_CHILD_OK and not…","phase":"Run","childId":"{{session:2}}"}} {"type":"tool-workflow/agent-end","data":{"runId":"{{workflow:1}}","seq":1,"outcome":"completed"}} {"type":"tool-workflow/run-end","data":{"runId":"{{workflow:1}}","stopReason":"completed"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_dD2BLuNeJCTh2iiYC1QR3449"},"content":[{"type":"tool-result","toolCallId":"call_00_dD2BLuNeJCTh2iiYC1QR3449","content":[{"type":"text","text":"workflow \"snapshot-flow\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WF_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The workflow returned successfully with the reply \"WF_CHILD_OK\". Now I need to reply with exactly \"WORKFLOW_DONE\" and stop."},{"type":"text","text":"WORKFLOW_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":328,"outputTokens":36,"cacheReadTokens":3072,"reasoningTokens":30},"stream":[{"type":"chunk","time":1788233532207,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788233532207,"index":0,"dt":[0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," workflow"," returned"," successfully"," with"," the"," reply"," \"","WF","_CH","ILD","_OK","\"."," Now"," I"," need"," to"," reply"," with"," exactly"," \"","WORK","FL","OW","_D","ONE","\""," and"," stop","."]},{"type":"chunk","time":1788233532208,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788233532208,"index":1,"dt":[0,0,0,0],"texts":["WORK","FL","OW","_D","ONE"]},{"type":"chunk","time":1788233532208,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The workflow returned successfully with the reply \"WF_CHILD_OK\". Now I need to reply with exactly \"WORKFLOW_DONE\" and stop."}}},{"type":"chunk","time":1788233532208,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"WORKFLOW_DONE"}}},{"type":"chunk","time":1788233532208,"chunk":{"type":"usage","usage":{"inputTokens":328,"outputTokens":36,"cacheReadTokens":3072,"reasoningTokens":30}}},{"type":"chunk","time":1788233532208,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The workflow returned successfully with the reply \"WF_CHILD_OK\". Now I need to reply with exactly \"WORKFLOW_DONE\" and stop."},{"type":"text","text":"WORKFLOW_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":328,"outputTokens":36,"cacheReadTokens":3072,"reasoningTokens":30},"stream":[{"type":"chunk","time":1788500554460,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788500554460,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," workflow"," returned"," successfully"," with"," the"," reply"," \"","WF","_CH","ILD","_OK","\"."," Now"," I"," need"," to"," reply"," with"," exactly"," \"","WORK","FL","OW","_D","ONE","\""," and"," stop","."]},{"type":"chunk","time":1788500554460,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788500554460,"index":1,"dt":[0,0,0,0],"texts":["WORK","FL","OW","_D","ONE"]},{"type":"chunk","time":1788500554460,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The workflow returned successfully with the reply \"WF_CHILD_OK\". Now I need to reply with exactly \"WORKFLOW_DONE\" and stop."}}},{"type":"chunk","time":1788500554460,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"WORKFLOW_DONE"}}},{"type":"chunk","time":1788500554460,"chunk":{"type":"usage","usage":{"inputTokens":328,"outputTokens":36,"cacheReadTokens":3072,"reasoningTokens":30}}},{"type":"chunk","time":1788500554460,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} From 399d7313c4f3084ae52e94d33105a57159586d79 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Mon, 7 Sep 2026 16:14:58 +0800 Subject: [PATCH 15/58] test(session): cover historical catalog comparison failures --- .../session-snapshot/tests/normalize.spec.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/packages/test-support/session-snapshot/tests/normalize.spec.ts b/packages/test-support/session-snapshot/tests/normalize.spec.ts index cfc02aaeab..e6594ea603 100644 --- a/packages/test-support/session-snapshot/tests/normalize.spec.ts +++ b/packages/test-support/session-snapshot/tests/normalize.spec.ts @@ -684,6 +684,43 @@ describe('normalizeSessionSnapshot', () => { ].join('\n')) }) + it('rejects an empty historical comparison input', () => { + expect(() => omitSubagentCatalogForHistoricalComparison('\n')) + .toThrow('session snapshot must start with a session header') + }) + + it('preserves snapshots and source references when no catalog fact is present', () => { + const normalized = [ + { type: 'session', id: '{{session:1}}' }, + { type: 'tool/call', data: { callId: 'result' } }, + { type: 'tool/result', data: { callId: 'result' }, sourceEventSeqs: [0] }, + ].map(record => JSON.stringify(record)).join('\n') + '\n' + + expect(omitSubagentCatalogForHistoricalComparison(normalized)).toBe(normalized) + }) + + it.each(['0', 0.5, -1])('refuses malformed source references during historical comparison: %s', (source) => { + const normalized = [ + { type: 'session', id: '{{session:1}}' }, + { type: 'subagent/catalog', data: { childId: '{{session:2}}' } }, + { type: 'tool/result', data: { callId: 'result' }, sourceEventSeqs: [source] }, + ].map(record => JSON.stringify(record)).join('\n') + + expect(() => omitSubagentCatalogForHistoricalComparison(normalized)) + .toThrow('normalized sourceEventSeqs must contain non-negative safe integers') + }) + + it('refuses to remove a catalog fact cited by a retained event', () => { + const normalized = [ + { type: 'session', id: '{{session:1}}' }, + { type: 'subagent/catalog', data: { childId: '{{session:2}}' } }, + { type: 'tool/result', data: { callId: 'result' }, sourceEventSeqs: [0] }, + ].map(record => JSON.stringify(record)).join('\n') + + expect(() => omitSubagentCatalogForHistoricalComparison(normalized)) + .toThrow('historical comparison cannot omit cited catalog event 0') + }) + it('migrates and re-packs multi-session fixtures after relationship-preserving id redaction', () => { const raw = [ JSON.stringify({ type: 'session', version: 0, id: '{{session:1}}', createdAt: 0, delegationDepth: 0 }), From 99462aef9823cf07f95b8e96634b3ab64b99a742 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Mon, 7 Sep 2026 16:52:02 +0800 Subject: [PATCH 16/58] fix(subagent): preserve catalog citations and skip unrequested views --- ...01-parent-owned-subagent-catalog.i18n.yaml | 4 +- ...026-09-01-parent-owned-subagent-catalog.md | 4 +- ...-09-01-parent-owned-subagent-catalog.zh.md | 4 +- docs/subsystems/session-projection.i18n.yaml | 4 +- docs/subsystems/session-projection.md | 13 +++--- docs/subsystems/session-projection.zh.md | 13 +++--- .../extensions/tool-cordis/src/api-catalog.ts | 16 ++++---- .../session-query/README.i18n.yaml | 4 +- .../session-query/session-query/README.md | 2 +- .../session-query/session-query/README.zh.md | 2 +- .../session-query/src/observation.ts | 16 +++++--- .../session-query/tests/observation.spec.ts | 37 +++++++++++++++-- .../session-projection-cache/README.i18n.yaml | 4 +- .../session-projection-cache/README.md | 2 + .../session-projection-cache/README.zh.md | 2 + .../session-projection-cache/src/index.ts | 9 +++-- .../tests/cache.spec.ts | 40 +++++++++++++++++++ .../session-projection/README.i18n.yaml | 4 +- packages/session/session-projection/README.md | 2 + .../session/session-projection/README.zh.md | 2 + .../session/session-projection/src/index.ts | 14 ++++++- .../session-snapshot/src/normalize.ts | 10 ++++- .../session-snapshot/tests/normalize.spec.ts | 22 ++++++++++ 23 files changed, 180 insertions(+), 50 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml index d85dec23a0..b6fb66d7e5 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md -2026-09-01-parent-owned-subagent-catalog.md: 7b2082bb6c8e046d3083ab95abffb739895d13b8 -2026-09-01-parent-owned-subagent-catalog.zh.md: 5f8413a3b88a747de6308b1220bb3139543ba83c +2026-09-01-parent-owned-subagent-catalog.md: 6e0547c95db3ad5fc22219163785ae29afe3d707 +2026-09-01-parent-owned-subagent-catalog.zh.md: 0d99f8f7e513953b824da78c7200cdafda0e209e diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md index 7b2082bb6c..6e0547c95d 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md @@ -22,7 +22,7 @@ The registered host-only `subagentCatalog` projection materializes the parent fa Fork isolation uses the exact `Session.inheritedEventCount` supplied to projection initialization. The fold ignores `subagent/catalog` events below that offset. The state stores the inherited offset but not each event seq because acceptance is decided during folding. -Snapshot normalizers zero `childCreatedAt` because it originates from the process clock. The TypeScript normalizer also sorts adjacent catalog facts by distinct child id because parallel successful creations may append in either order. Non-catalog events remain ordering barriers. +Snapshot normalizers zero `childCreatedAt` because it originates from the process clock. The TypeScript normalizer also sorts adjacent catalog facts by distinct child id because parallel successful creations may append in either order. Non-catalog events remain ordering barriers. Source-event references are remapped to the sorted positions so each citation retains its original fact. ## Alternatives considered @@ -38,6 +38,6 @@ Snapshot normalizers zero `childCreatedAt` because it originates from the proces ## Consequences -A caller can request `projectionStateKeys: ['subagentCatalog']` from `observeSession`. Live observations clone the maintained registry state; cold observations hydrate their prepared Session and detach the same state at the observation cursor. Direct-child and descendant listing still use the Session corpus and child identity projection. +A caller can request `projectionStateKeys: ['subagentCatalog']` from `observeSession`. Live observations clone the maintained registry state; cold observations hydrate their prepared Session and detach the same state at the observation cursor. A host-state-only read passes an empty view selection through checkpoint hydration, including cache reuse and malformed-row recovery; unrelated wire views are neither computed nor validated. Direct-child and descendant listing still use the Session corpus and child identity projection. Backends that do not know the required event refuse the log under the existing Session event mechanism. Pre-release format policy requires no fallback scan for old logs. diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md index 5f8413a3b8..0d99f8f7e5 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md @@ -22,7 +22,7 @@ child header 与 `subagent/descriptor` 继续拥有恢复与 composition 权威 fork 隔离使用 projection 初始化时提供的精确 `Session.inheritedEventCount`。fold 忽略该 offset 之前的 `subagent/catalog` 事件。state 保存 inherited offset,但不保存每条 event seq,因为接受判定已在 fold 时完成。 -snapshot normalizer 会把 `childCreatedAt` 归零,因为它来自 process clock。TypeScript normalizer 还会按不同 child id 排序相邻 catalog fact,因为并行成功创建的 append 顺序可能不同。非 catalog event 继续作为顺序 barrier。 +snapshot normalizer 会把 `childCreatedAt` 归零,因为它来自 process clock。TypeScript normalizer 还会按不同 child id 排序相邻 catalog fact,因为并行成功创建的 append 顺序可能不同。非 catalog event 继续作为顺序 barrier。来源事件引用会重映射到排序后的位置,使每个引用仍指向原来的事实。 ## 考虑过的替代方案 @@ -38,6 +38,6 @@ snapshot normalizer 会把 `childCreatedAt` 归零,因为它来自 process clo ## 后果 -调用方可以向 `observeSession` 请求 `projectionStateKeys: ['subagentCatalog']`。实时观察克隆 registry 维护中的 state;冷观察 hydrate 已准备的 Session,并在观察 cursor 处分离出同一 state。直接子级和后代列表仍使用 Session 语料库与子级身份 projection。 +调用方可以向 `observeSession` 请求 `projectionStateKeys: ['subagentCatalog']`。实时观察克隆 registry 维护中的 state;冷观察 hydrate 已准备的 Session,并在观察 cursor 处分离出同一 state。仅读取 host state 时,空视图选择会传递到检查点 hydration,包括缓存复用和损坏 row 的恢复;无关 wire view 不会被计算或校验。直接子级和后代列表仍使用 Session 语料库与子级身份 projection。 不认识该 required event 的 backend 会按既有 Session event 机制拒绝日志。pre-release format policy 不要求为旧日志保留 fallback scan。 diff --git a/docs/subsystems/session-projection.i18n.yaml b/docs/subsystems/session-projection.i18n.yaml index 39bbf0016a..03a6f38bbd 100644 --- a/docs/subsystems/session-projection.i18n.yaml +++ b/docs/subsystems/session-projection.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/session-projection.md -session-projection.md: 8568b6df94b1427a341568777c64c82d74176a17 -session-projection.zh.md: 5e50341a6443efde2a3b49b7f5185bc44896efc6 +session-projection.md: 52e99245609872c61853792250a4e0fcf631f279 +session-projection.zh.md: 372c530ba36e83240c431c2c07edc2d39fb7d21b diff --git a/docs/subsystems/session-projection.md b/docs/subsystems/session-projection.md index 8568b6df94..52e9924560 100644 --- a/docs/subsystems/session-projection.md +++ b/docs/subsystems/session-projection.md @@ -163,9 +163,10 @@ cachedPredecessorTitle( meta: SessionHeader, inheritedEventCount: SessionLogOffs * because the logical observation may contain recovery events not yet durable. * @param session - exact unpublished Session retained by persistence. * @param events - exact logical event prefix represented by the observation. - * @returns all projection values at the event cut. + * @param keys - optional client-visible outputs; an empty list hydrates states without computing or validating views. + * @returns selected projection values at the event cut, defaulting to all views. */ -hydratePrepared( session: Session, events: readonly SessionEvent[], ): ProjectionSnapshot +hydratePrepared( session: Session, events: readonly SessionEvent[], keys?: readonly Extract[], ): ProjectionSnapshot /** * Durably checkpoint one live session NOW (all mandatory points call @@ -329,11 +330,12 @@ viewCheckpoint( checkpoint: ProjectionCheckpoint, keys?: readonly Extract[], ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint } /** * Restore an exact cut and install its states on the supplied prepared Session. @@ -343,9 +345,10 @@ restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], base * @param checkpoint - persisted rows for this Session lifecycle. * @param events - exact events at the observation cut. * @param baseSeq - first supplied event sequence. - * @returns all projection values at the supplied cut. + * @param keys - optional client-visible outputs; an empty list installs every state without computing or validating views. + * @returns selected projection values at the supplied cut, defaulting to all views. */ -hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, ): ProjectionSnapshot +hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, keys?: readonly Extract[], ): ProjectionSnapshot ``` Types: [Session](session.md) · [SessionEvent](session.md) · [SessionHeader](persistence.md) · [SessionLogOffset](session.md) diff --git a/docs/subsystems/session-projection.zh.md b/docs/subsystems/session-projection.zh.md index 5e50341a64..372c530ba3 100644 --- a/docs/subsystems/session-projection.zh.md +++ b/docs/subsystems/session-projection.zh.md @@ -163,9 +163,10 @@ cachedPredecessorTitle( meta: SessionHeader, inheritedEventCount: SessionLogOffs * because the logical observation may contain recovery events not yet durable. * @param session - exact unpublished Session retained by persistence. * @param events - exact logical event prefix represented by the observation. - * @returns all projection values at the event cut. + * @param keys - optional client-visible outputs; an empty list hydrates states without computing or validating views. + * @returns selected projection values at the event cut, defaulting to all views. */ -hydratePrepared( session: Session, events: readonly SessionEvent[], ): ProjectionSnapshot +hydratePrepared( session: Session, events: readonly SessionEvent[], keys?: readonly Extract[], ): ProjectionSnapshot /** * Durably checkpoint one live session NOW (all mandatory points call @@ -329,11 +330,12 @@ viewCheckpoint( checkpoint: ProjectionCheckpoint, keys?: readonly Extract[], ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint } /** * Restore an exact cut and install its states on the supplied prepared Session. @@ -343,9 +345,10 @@ restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], base * @param checkpoint - persisted rows for this Session lifecycle. * @param events - exact events at the observation cut. * @param baseSeq - first supplied event sequence. - * @returns all projection values at the supplied cut. + * @param keys - optional client-visible outputs; an empty list installs every state without computing or validating views. + * @returns selected projection values at the supplied cut, defaulting to all views. */ -hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, ): ProjectionSnapshot +hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, keys?: readonly Extract[], ): ProjectionSnapshot ``` Types: [Session](session.zh.md) · [SessionEvent](session.zh.md) · [SessionHeader](persistence.zh.md) · [SessionLogOffset](session.zh.md) diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index de254c190b..dba22a4046 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -1627,10 +1627,10 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ returns: 'a title-only checkpoint view with `asOfSeq: -1`, or `undefined` when the record is current, newer, unrelated, missing, or incompatible with the title unit. The sentinel avoids reusing a sequence that a cardinality-changing Session migration may have remapped.', }, { - signature: 'hydratePrepared( session: Session, events: readonly SessionEvent[], ): ProjectionSnapshot', + signature: 'hydratePrepared( session: Session, events: readonly SessionEvent[], keys?: readonly Extract[], ): ProjectionSnapshot', description: 'Hydrate projection cells for an already-prepared Session without another persistence read. The cache seeds matching rows; the supplied exact log advances every unit to the observation cut. No checkpoint is written because the logical observation may contain recovery events not yet durable.', - parameters: [{ name: 'session', description: 'exact unpublished Session retained by persistence.' }, { name: 'events', description: 'exact logical event prefix represented by the observation.' }], - returns: 'all projection values at the event cut.', + parameters: [{ name: 'session', description: 'exact unpublished Session retained by persistence.' }, { name: 'events', description: 'exact logical event prefix represented by the observation.' }, { name: 'keys', description: 'optional client-visible outputs; an empty list hydrates states without computing or validating views.' }], + returns: 'selected projection values at the event cut, defaulting to all views.', }, { signature: 'async write(session: Session): Promise', @@ -1706,16 +1706,16 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ returns: 'whole values per key with a usable row; empty when none.', }, { - signature: 'restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, header: SessionHeader, inheritedEventCount: SessionLogOffset, ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }', + signature: 'restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, header: SessionHeader, inheritedEventCount: SessionLogOffset, keys?: readonly Extract[], ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }', description: 'Cold read: fold every persisted unit over a stored log suffix, seeding each from its checkpoint row when usable — the one read recipe (cached state + forward tail replay + `view`) applied without a live `Session`. Call with the stored events at or past `restoreFloor(checkpoint)` (a `SessionHandle.read` slice) and that same floor as `baseSeq`; the floor\'s one-below anchor makes the supplied end honest, so a shrunk log is detected here. A row is usable iff its `ver` matches the live unit\'s `stateVersion`, it does not predate `baseSeq` (`seq >= baseSeq - 1`), and it does not claim events past the supplied end (`seq <= endSeq`); an unusable row is discarded and its key refolds from `init` — which is only sound over the full log, so a discarded row with `baseSeq > 0` throws (the caller re-reads from seq 0, e.g. after a crash-repair truncation shrank the log below a row\'s watermark).', - parameters: [{ name: 'checkpoint', description: 'persisted rows for one session (possibly stale or empty).' }, { name: 'events', description: 'the stored events with `seq >= baseSeq`, in seq order.' }, { name: 'baseSeq', description: 'the seq `events` starts at (its first event\'s seq when non-empty).' }, { name: 'header', description: 'immutable metadata for the Session being restored.' }, { name: 'inheritedEventCount', description: 'exact fork-inherited prefix length supplied to unit initialization.' }], + parameters: [{ name: 'checkpoint', description: 'persisted rows for one session (possibly stale or empty).' }, { name: 'events', description: 'the stored events with `seq >= baseSeq`, in seq order.' }, { name: 'baseSeq', description: 'the seq `events` starts at (its first event\'s seq when non-empty).' }, { name: 'header', description: 'immutable metadata for the Session being restored.' }, { name: 'inheritedEventCount', description: 'exact fork-inherited prefix length supplied to unit initialization.' }, { name: 'keys', description: 'optional client-visible outputs; an empty list restores every state without computing or validating views.' }], returns: 'the snapshot cut at the supplied log end (`asOfSeq` is the last supplied event\'s seq, `baseSeq - 1` for an empty tail) plus the refreshed checkpoint rows at that cut, ready for a durable write-back.', }, { - signature: 'hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, ): ProjectionSnapshot', + signature: 'hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, keys?: readonly Extract[], ): ProjectionSnapshot', description: 'Restore an exact cut and install its states on the supplied prepared Session. A later publication reuses these cells; ordinary live reads and event drive advance any constructor-owned suffix exactly once.', - parameters: [{ name: 'session', description: 'exact prepared Session that owns the restored log prefix.' }, { name: 'checkpoint', description: 'persisted rows for this Session lifecycle.' }, { name: 'events', description: 'exact events at the observation cut.' }, { name: 'baseSeq', description: 'first supplied event sequence.' }], - returns: 'all projection values at the supplied cut.', + parameters: [{ name: 'session', description: 'exact prepared Session that owns the restored log prefix.' }, { name: 'checkpoint', description: 'persisted rows for this Session lifecycle.' }, { name: 'events', description: 'exact events at the observation cut.' }, { name: 'baseSeq', description: 'first supplied event sequence.' }, { name: 'keys', description: 'optional client-visible outputs; an empty list installs every state without computing or validating views.' }], + returns: 'selected projection values at the supplied cut, defaulting to all views.', }, ], }, diff --git a/packages/session-query/session-query/README.i18n.yaml b/packages/session-query/session-query/README.i18n.yaml index e5ac8d1d9b..7857b37bc6 100644 --- a/packages/session-query/session-query/README.i18n.yaml +++ b/packages/session-query/session-query/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session-query/session-query/README.md -README.md: 52c6870fed7f39c2e2c5a49c1aa43c7e498488ea -README.zh.md: e062d0a2a37252378ae32431d8ae8d1a264ec200 +README.md: b754f342e4614137b92aa749c3b6822a23e2e4bf +README.zh.md: 9c41cc0ce97a99d8b64c206af26799527e97d102 diff --git a/packages/session-query/session-query/README.md b/packages/session-query/session-query/README.md index 52c6870fed..b754f342e4 100644 --- a/packages/session-query/session-query/README.md +++ b/packages/session-query/session-query/README.md @@ -108,7 +108,7 @@ The decision history lives in the [unified service decision](../../../.agents/no ### Observation cache -`observeSession` builds point observations without a listing preflight. A live observation fixes its cut as the current log length and materializes `events` on first read, so header-, cursor-, or projection-only consumers never copy the log; the log only appends, so a late first read still yields exactly that prefix. The cold path stats the stored session first and consults an own bounded cache keyed by the persistence instance and the `stat` revision: an unchanged revision reuses the restored unpublished Session without re-reading the log; a changed revision, or a replaced persistence instance, reloads through the handle seam and replaces the entry. The cache holds `preparedSessionCacheSize` entries with least-recently-used eviction, entries pinned by active observation leases are never evicted, and a session that goes live mid-read retries the live path. `projectionStateKeys` detaches named host projection states at the same cut independently of `projectionMode`: a live observation clones the registry's maintained cells, and a cold observation hydrates its prepared Session's cells once so states and any views share one cut; unregistered keys are omitted and an absent registry leaves `projectionStates` undefined. +`observeSession` builds point observations without a listing preflight. A live observation fixes its cut as the current log length and materializes `events` on first read, so header-, cursor-, or projection-only consumers never copy the log; the log only appends, so a late first read still yields exactly that prefix. The cold path stats the stored session first and consults an own bounded cache keyed by the persistence instance and the `stat` revision: an unchanged revision reuses the restored unpublished Session without re-reading the log; a changed revision, or a replaced persistence instance, reloads through the handle seam and replaces the entry. The cache holds `preparedSessionCacheSize` entries with least-recently-used eviction, entries pinned by active observation leases are never evicted, and a session that goes live mid-read retries the live path. `projectionStateKeys` detaches named host projection states at the same cut independently of `projectionMode`: a live observation clones the registry's maintained cells, and a cold observation hydrates its prepared Session's cells once so states and any views share one cut; unregistered keys are omitted and an absent registry leaves `projectionStates` undefined. With `projectionMode: 'none'`, both live and cold observations omit client view computation and validation even when host states are requested. ### Reads and traces diff --git a/packages/session-query/session-query/README.zh.md b/packages/session-query/session-query/README.zh.md index e062d0a2a3..9c41cc0ce9 100644 --- a/packages/session-query/session-query/README.zh.md +++ b/packages/session-query/session-query/README.zh.md @@ -108,7 +108,7 @@ kind: "package-reference" ### 观察缓存 -`observeSession` 不经过列表预检直接构建定点观察。实时观察以当前日志长度固定 cut,并在首次读取时才物化 `events`,因此只需要 header、cursor 或 projection 的消费者永远不会复制日志;日志只会追加,所以延后的首次读取得到的仍然正好是该前缀。冷路径先对存储会话执行 `stat`,再查询自有的有界缓存,缓存键为持久化实例加 `stat` 修订:修订未变则复用已恢复的未发布 Session,不再重读日志;修订变化或持久化实例被替换则经 handle 缝重新加载并替换条目。缓存保留 `preparedSessionCacheSize` 个条目并按最久未用淘汰,被活跃观察租约钉住的条目从不被淘汰;读取中途转为实时的会话会重试实时路径。 `projectionStateKeys` 在同一 cut 上分离出指定的 host projection state,与 `projectionMode` 无关:实时观察克隆 registry 维护中的 cell,冷观察则一次性 hydrate 其已准备 Session 的 cell,使 state 与任何 view 共享同一 cut;未注册的 key 被省略,registry 缺失时 `projectionStates` 为 undefined。 +`observeSession` 不经过列表预检直接构建定点观察。实时观察以当前日志长度固定 cut,并在首次读取时才物化 `events`,因此只需要 header、cursor 或 projection 的消费者永远不会复制日志;日志只会追加,所以延后的首次读取得到的仍然正好是该前缀。冷路径先对存储会话执行 `stat`,再查询自有的有界缓存,缓存键为持久化实例加 `stat` 修订:修订未变则复用已恢复的未发布 Session,不再重读日志;修订变化或持久化实例被替换则经 handle 缝重新加载并替换条目。缓存保留 `preparedSessionCacheSize` 个条目并按最久未用淘汰,被活跃观察租约钉住的条目从不被淘汰;读取中途转为实时的会话会重试实时路径。 `projectionStateKeys` 在同一 cut 上分离出指定的 host projection state,与 `projectionMode` 无关:实时观察克隆 registry 维护中的 cell,冷观察则一次性 hydrate 其已准备 Session 的 cell,使 state 与任何 view 共享同一 cut;未注册的 key 被省略,registry 缺失时 `projectionStates` 为 undefined。使用 `projectionMode: 'none'` 时,即使请求了 host state,实时和冷观察也都不会计算或校验客户端视图。 ### 读取与追踪 diff --git a/packages/session-query/session-query/src/observation.ts b/packages/session-query/session-query/src/observation.ts index 7ba5951710..eb1a886243 100644 --- a/packages/session-query/session-query/src/observation.ts +++ b/packages/session-query/session-query/src/observation.ts @@ -50,7 +50,7 @@ export interface SessionObservation extends Disposable { export interface SessionObservationOptions { /** Optional cancellation while resolving a cold source. */ readonly signal?: AbortSignal - /** Whether to compute every projection or leave projection state untouched. */ + /** Whether to compute client views; requested host states are still materialized. */ readonly projectionMode?: 'all' | 'none' /** * Host projection states to detach from the same exact cut, independent of @@ -103,7 +103,7 @@ export class SessionObservationReader { /** * Observe one live-preferred Session and retain a cold preparation until disposal. * @param sessionId - logical Session identity. - * @param options - cancellation and all-or-none projection computation for this read. + * @param options - cancellation, client views, and requested host states for this read. * @returns one exact immutable observation. */ async read( @@ -167,7 +167,7 @@ export class SessionObservationReader { if (projectionMode === 'all' || projectionStateKeys !== undefined) { // Hydration installs every unit's cell on the prepared Session, so a // requested host state reads the same cut the views were taken from. - const snapshot = this.preparedProjections(entry) + const snapshot = this.preparedProjections(entry, projectionMode) projections = projectionMode === 'all' ? snapshot : undefined projectionStates = this.projectionStates(entry.session, projectionStateKeys) } @@ -330,13 +330,17 @@ export class SessionObservationReader { return lease() } - private preparedProjections(entry: PreparedEntry): ProjectionSnapshot | undefined { + private preparedProjections( + entry: PreparedEntry, + projectionMode: NonNullable, + ): ProjectionSnapshot | undefined { const registry = this.ctx.get('sessionProjections') if (registry === undefined) return undefined const cache = this.ctx.get('sessionProjectionCache') + const keys = projectionMode === 'none' ? [] : undefined return cache === undefined - ? registry.hydrate(entry.session, {}, entry.events, SessionLogOffset(0)) - : cache.hydratePrepared(entry.session, entry.events) + ? registry.hydrate(entry.session, {}, entry.events, SessionLogOffset(0), keys) + : cache.hydratePrepared(entry.session, entry.events, keys) } /** diff --git a/packages/session-query/session-query/tests/observation.spec.ts b/packages/session-query/session-query/tests/observation.spec.ts index 106f3453e1..93a0e4c191 100644 --- a/packages/session-query/session-query/tests/observation.spec.ts +++ b/packages/session-query/session-query/tests/observation.spec.ts @@ -762,7 +762,7 @@ describe('SessionObservationReader cold projections', () => { await ctx.fiber.dispose() }) - it('hydrates prepared projections through a mounted projection cache', async () => { + it.each(['all', 'none'] as const)('hydrates prepared projections through a mounted projection cache in %s mode', async (projectionMode) => { const ctx = await readerContext() await ctx.plugin(SessionProjectionRegistry) const meta = header('cold-cache') @@ -772,10 +772,14 @@ describe('SessionObservationReader cold projections', () => { const hydratePrepared = vi.fn().mockReturnValue(snapshot) ctx.provide('sessionProjectionCache', { hydratePrepared } as never) - using observed = await new SessionObservationReader(ctx).read(meta.id) + using observed = await new SessionObservationReader(ctx).read(meta.id, { + projectionMode, + projectionStateKeys: ['title'], + }) - expect(observed.projections).toBe(snapshot) + expect(observed.projections).toBe(projectionMode === 'all' ? snapshot : undefined) expect(hydratePrepared).toHaveBeenCalledOnce() + expect(hydratePrepared.mock.calls[0]?.[2]).toEqual(projectionMode === 'all' ? undefined : []) await ctx.fiber.dispose() }) @@ -837,6 +841,33 @@ describe('SessionObservationReader host projection states', () => { await ctx.fiber.dispose() }) + it('omits wire views on fresh and reused cold host-state reads', async () => { + const ctx = await readerContext() + try { + await ctx.plugin(SessionProjectionRegistry) + const view = vi.fn(titleProjectionDefinition.wire.view) + ctx.sessionProjections.register({ + ...titleProjectionDefinition, + wire: { ...titleProjectionDefinition.wire, view }, + }) + const meta = header('cold-host-only') + const store = new Map([[meta.id, { header: meta, events: [titleEvent(0, 'Cold title')], revision: 'r1' }]]) + ctx.provide('sessionPersistence', stubPersistence(store, { stat: 0, open: 0, read: 0 })) + const reader = new SessionObservationReader(ctx) + for (let read = 0; read < 2; read++) { + using observed = await reader.read(meta.id, { projectionMode: 'none', projectionStateKeys: ['title'] }) + expect(observed.projections).toBeUndefined() + expect(observed.projectionStates).toEqual({ title: 'Cold title' }) + expect(view).not.toHaveBeenCalled() + } + using full = await reader.read(meta.id) + expect(full.projections?.values).toEqual({ title: 'Cold title' }) + expect(view).toHaveBeenCalledOnce() + } finally { + await ctx.fiber.dispose() + } + }) + it('omits unregistered keys and the whole map when no registry is mounted', async () => { const withRegistry = await readerContext() await withRegistry.plugin(SessionProjectionRegistry) diff --git a/packages/session/session-projection-cache/README.i18n.yaml b/packages/session/session-projection-cache/README.i18n.yaml index c738058bea..549dd6c0d8 100644 --- a/packages/session/session-projection-cache/README.i18n.yaml +++ b/packages/session/session-projection-cache/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session/session-projection-cache/README.md -README.md: dcbe57af1f9c13f1f43572ff188dfdaaf9287160 -README.zh.md: 367d893c56a23e0965eb1e10f42fb6705284af6b +README.md: ab9c4f0ebb8f68d53ebdb952997805f2b52f59af +README.zh.md: c6ac28788ea4000327e0232d45fb0e1344e500da diff --git a/packages/session/session-projection-cache/README.md b/packages/session/session-projection-cache/README.md index dcbe57af1f..ab9c4f0ebb 100644 --- a/packages/session/session-projection-cache/README.md +++ b/packages/session/session-projection-cache/README.md @@ -52,6 +52,8 @@ The cache opens its domain through the storage stack, so base mounts `storage`, The plugin injects `storageDomain`, `sessionProjections`, and `sessions`. The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-session-projection-cache) is the exhaustive source for every accepted field and its JSDoc. +`hydratePrepared` accepts optional view keys and restores every state from the checkpoint and event tail. An empty key list suppresses client view computation and validation, including when a malformed checkpoint requires a full refold. + ### How checkpoints are written Three mandatory points always write: session creation persists the seed-derived cut, `turn/end` persists the value that listing reads want, and session disposal persists the final live cut. Between them, the configured count and interval throttles write as events accumulate. Every write atomically replaces the session's complete record through the domain write chain; a failure logs a warning and keeps the cache stale, and the next write self-heals. diff --git a/packages/session/session-projection-cache/README.zh.md b/packages/session/session-projection-cache/README.zh.md index 367d893c56..c6ac28788e 100644 --- a/packages/session/session-projection-cache/README.zh.md +++ b/packages/session/session-projection-cache/README.zh.md @@ -52,6 +52,8 @@ kind: "package-reference" 本插件注入 `storageDomain`、`sessionProjections` 与 `sessions`。生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-session-projection-cache)是每个受支持字段及其 JSDoc 的穷尽式真源。 +`hydratePrepared` 接受可选的视图 key,并从检查点与事件尾部恢复所有状态。空 key 列表会跳过客户端视图的计算与校验,即使检查点损坏、需要重新折叠完整日志也如此。 + ### 检查点如何写入 三个必写点总是写入:会话创建保存由种子派生的切面,`turn/end` 保存列表读取所需的轮次终值,会话释放保存最终实时切面。其间,配置的条数与间隔节流随事件累积写入。每次写入通过领域写入链以原子方式替换该会话的完整记录;失败会记录警告并让缓存保持陈旧,后续写入会自行修复。 diff --git a/packages/session/session-projection-cache/src/index.ts b/packages/session/session-projection-cache/src/index.ts index 639dfcc978..af5636c68c 100644 --- a/packages/session/session-projection-cache/src/index.ts +++ b/packages/session/session-projection-cache/src/index.ts @@ -207,18 +207,20 @@ export class SessionProjectionCache extends Service { * because the logical observation may contain recovery events not yet durable. * @param session - exact unpublished Session retained by persistence. * @param events - exact logical event prefix represented by the observation. - * @returns all projection values at the event cut. + * @param keys - optional client-visible outputs; an empty list hydrates states without computing or validating views. + * @returns selected projection values at the event cut, defaulting to all views. */ hydratePrepared( session: Session, events: readonly SessionEvent[], + keys?: readonly Extract[], ): ProjectionSnapshot { const record = this.recordFor( session.id, identityOf(session.header, session.inheritedEventCount), ) if (record === undefined) { - return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0)) + return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0), keys) } try { return this.ctx.sessionProjections.hydrate( @@ -226,11 +228,12 @@ export class SessionProjectionCache extends Service { record.rows, events, SessionLogOffset(0), + keys, ) } catch { // Cached rows are disposable derived data. Retry from the exact log so a // stale schema cannot make a valid Session unreadable. - return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0)) + return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0), keys) } } diff --git a/packages/session/session-projection-cache/tests/cache.spec.ts b/packages/session/session-projection-cache/tests/cache.spec.ts index b38f49eec1..1774adb6ad 100644 --- a/packages/session/session-projection-cache/tests/cache.spec.ts +++ b/packages/session/session-projection-cache/tests/cache.spec.ts @@ -651,6 +651,46 @@ describe('SessionProjectionCache cold-read seeding', () => { }) }) + it.each(['matching', 'malformed', 'absent'])('hydrates every state with only selected views from a %s checkpoint', async (record) => { + const root = await mkdtemp(join(tmpdir(), 'dsh-projcache-')) + roots.push(root) + const id = SessionId('selected-views') + if (record !== 'absent') { + await seedRecord(root, id, { + 'cache-test/marks': { ver: 1, seq: SessionSeq(1), val: { marks: record === 'matching' ? ['cached'] : 'malformed' } }, + }) + } + const { cache, ctx } = await harness({ root }) + const view = vi.fn(secondaryMarksUnit.wire.view) + const schema = secondaryMarksUnit.wire.viewSchema.clone() + const parseView = vi.spyOn(schema, 'parse') + ctx.sessionProjections.register({ + ...secondaryMarksUnit, + wire: { viewSchema: schema, view }, + }) + const events = storedLog([['fresh']]) + const session = Session.create(id, events, headerOf(id)) + const marks = { marks: [record === 'matching' ? 'cached' : 'fresh'] } + + for (let read = 0; read < 2; read++) { + expect(cache.hydratePrepared(session, events, [])).toEqual({ asOfSeq: 2, values: {} }) + expect(ctx.sessionProjections.stateOf(session, 'cache-test/marks')).toEqual(marks) + expect(ctx.sessionProjections.stateOf(session, 'cache-test/secondary-marks')).toEqual({ marks: ['fresh'] }) + expect(view).not.toHaveBeenCalled() + expect(parseView).not.toHaveBeenCalled() + } + expect(cache.hydratePrepared(session, events, ['cache-test/marks'])).toEqual({ + asOfSeq: 2, + values: { 'cache-test/marks': marks }, + }) + expect(view).not.toHaveBeenCalled() + expect(cache.hydratePrepared(session, events).values).toEqual({ + 'cache-test/marks': marks, + 'cache-test/secondary-marks': { marks: ['fresh'] }, + }) + expect(view).toHaveBeenCalledOnce() + }) + it('coldSnapshot traverses the full log but applies only the events after each cached watermark', async () => { const root = await mkdtemp(join(tmpdir(), 'dsh-projcache-')) roots.push(root) diff --git a/packages/session/session-projection/README.i18n.yaml b/packages/session/session-projection/README.i18n.yaml index 1b5c91d061..2d8e377de7 100644 --- a/packages/session/session-projection/README.i18n.yaml +++ b/packages/session/session-projection/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session/session-projection/README.md -README.md: d1aee8aed891fe04585781772757a8a1ea307590 -README.zh.md: e97617a2006f9a944e7fce3aed26cd0e922cce0b +README.md: 9cbe4ea8f065787ee8ba568d4598a2a1cf69812a +README.zh.md: 16fa8202c4fe13823062bc3396f63e19c26713cc diff --git a/packages/session/session-projection/README.md b/packages/session/session-projection/README.md index d1aee8aed8..9cbe4ea8f0 100644 --- a/packages/session/session-projection/README.md +++ b/packages/session/session-projection/README.md @@ -64,6 +64,8 @@ const { asOfSeq, values } = ctx.sessionProjections.snapshot(session) A domain that requires projected state declares `sessionProjections` as a Cordis service dependency; optional contributors may register under `ctx.inject(['sessionProjections'], …)`. Carriers use `ctx.get('sessionProjections')` and omit their block or frames when the registry is absent. +`restore` and `hydrate` accept optional view keys. Omitting them returns all client views; an empty list restores every host state without computing or validating any client view. + ### Persisted checkpoints Every unit's state is checkpointed — client-visible and host-only alike — through `checkpoint(session)`, and the sibling [session-projection-cache](../session-projection-cache/README.md) persists those checkpoints so cold reads skip full log loads. Checkpoint watermarks use `SessionSeqCursor` (`-1` for an empty log), while replay starts use `SessionLogOffset`; `restoreFloor` and `restore` implement the read recipe without conflating an existing event with a log gap. diff --git a/packages/session/session-projection/README.zh.md b/packages/session/session-projection/README.zh.md index e97617a200..16fa8202c4 100644 --- a/packages/session/session-projection/README.zh.md +++ b/packages/session/session-projection/README.zh.md @@ -64,6 +64,8 @@ const { asOfSeq, values } = ctx.sessionProjections.snapshot(session) 必须使用投影状态的领域把 `sessionProjections` 声明为 Cordis 服务依赖;可选贡献方可以在 `ctx.inject(['sessionProjections'], …)` 下注册。载体使用 `ctx.get('sessionProjections')`,注册表缺席时省略自己的块或帧。 +`restore` 和 `hydrate` 接受可选的视图 key。省略时返回全部客户端视图;传入空列表时恢复所有 host state,但不计算或校验任何客户端视图。 + ### 持久检查点 每个单元的状态都会被检查点化——client-visible 与 host-only 一视同仁——通过 `checkpoint(session)`,同级包 [session-projection-cache](../session-projection-cache/README.zh.md) 持久化这些检查点,使冷读跳过全量日志加载。检查点水位使用 `SessionSeqCursor`(空日志为 `-1`),回放起点使用 `SessionLogOffset`;`restoreFloor` 与 `restore` 在无活动会话的情况下实现读取配方,且不会混淆已有事件与日志间隙。 diff --git a/packages/session/session-projection/src/index.ts b/packages/session/session-projection/src/index.ts index d29c5bb021..8488d12e62 100644 --- a/packages/session/session-projection/src/index.ts +++ b/packages/session/session-projection/src/index.ts @@ -488,6 +488,7 @@ export class SessionProjectionRegistry extends Service { * @param baseSeq - the seq `events` starts at (its first event's seq when non-empty). * @param header - immutable metadata for the Session being restored. * @param inheritedEventCount - exact fork-inherited prefix length supplied to unit initialization. + * @param keys - optional client-visible outputs; an empty list restores every state without computing or validating views. * @returns the snapshot cut at the supplied log end (`asOfSeq` is the last * supplied event's seq, `baseSeq - 1` for an empty tail) plus the * refreshed checkpoint rows at that cut, ready for a durable write-back. @@ -498,12 +499,14 @@ export class SessionProjectionRegistry extends Service { baseSeq: SessionLogOffset, header: SessionHeader, inheritedEventCount: SessionLogOffset, + keys?: readonly Extract[], ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint } { const endSeq: SessionSeqCursor = events.at(-1)?.seq ?? cursorBefore(baseSeq) const beforeBase = cursorBefore(baseSeq) const values: Record = {} const refreshed: ProjectionCheckpoint = {} + const selected = keys === undefined ? undefined : new Set(keys) for (const registration of this.registrations.values()) { const def = registration.def const row = checkpoint[def.key] @@ -530,7 +533,9 @@ export class SessionProjectionRegistry extends Service { } state = def.apply(state, event) } - if (def.wire !== undefined) values[def.key] = def.wire.viewSchema.parse(def.wire.view(state)) + if (def.wire !== undefined && (selected === undefined || selected.has(def.key))) { + values[def.key] = def.wire.viewSchema.parse(def.wire.view(state)) + } refreshed[def.key] = { ver: def.stateVersion, seq: endSeq, val: state } } return { @@ -547,13 +552,15 @@ export class SessionProjectionRegistry extends Service { * @param checkpoint - persisted rows for this Session lifecycle. * @param events - exact events at the observation cut. * @param baseSeq - first supplied event sequence. - * @returns all projection values at the supplied cut. + * @param keys - optional client-visible outputs; an empty list installs every state without computing or validating views. + * @returns selected projection values at the supplied cut, defaulting to all views. */ hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, + keys?: readonly Extract[], ): ProjectionSnapshot { const endSeq: SessionSeqCursor = events.at(-1)?.seq ?? cursorBefore(baseSeq) let complete = true @@ -566,8 +573,10 @@ export class SessionProjectionRegistry extends Service { } if (complete) { const values: Record = {} + const selected = keys === undefined ? undefined : new Set(keys) for (const registration of this.registrations.values()) { if (registration.def.wire === undefined) continue + if (selected !== undefined && !selected.has(registration.def.key)) continue const current = registration.cells.get(session) as UnitCell values[registration.def.key] = this.viewCell(registration, current) } @@ -579,6 +588,7 @@ export class SessionProjectionRegistry extends Service { baseSeq, session.header, session.inheritedEventCount, + keys, ) for (const registration of this.registrations.values()) { const row = restored.checkpoint[registration.def.key] diff --git a/packages/test-support/session-snapshot/src/normalize.ts b/packages/test-support/session-snapshot/src/normalize.ts index 2a4145fe80..fe4469e1c9 100644 --- a/packages/test-support/session-snapshot/src/normalize.ts +++ b/packages/test-support/session-snapshot/src/normalize.ts @@ -427,11 +427,17 @@ function projectSessionSnapshot(rawLog: string): string { const lines = rawLog.split('\n').filter(line => line.trim().length > 0) const header = lines.shift() as string - const records = canonicalizeCatalogRuns(lines.map((line) => { + const original = lines.map((line) => { return JSON.parse(line) as Record & { type: string } - })) + }) + const records = canonicalizeCatalogRuns(original) + const positions = new Map(records.map((record, seq) => [record, seq])) + const sourcePositions = original.map(record => positions.get(record) as number) const body = records.map((record) => { const projected = { ...record } + if (Array.isArray(record.sourceEventSeqs)) { + projected.sourceEventSeqs = record.sourceEventSeqs.map((seq: number) => sourcePositions[seq] ?? seq) + } omitFixtureEnvelope(projected) return JSON.stringify(projected) }) diff --git a/packages/test-support/session-snapshot/tests/normalize.spec.ts b/packages/test-support/session-snapshot/tests/normalize.spec.ts index e6594ea603..339e190411 100644 --- a/packages/test-support/session-snapshot/tests/normalize.spec.ts +++ b/packages/test-support/session-snapshot/tests/normalize.spec.ts @@ -657,6 +657,28 @@ describe('normalizeSessionSnapshot', () => { expect(normalized).toContain('"childId":3') }) + it.each([ + { sources: [0, 1], remapped: [0, 2] }, + { sources: [0, 2], remapped: [0, 1] }, + ])('preserves cited children when adjacent catalog facts change position: $sources', ({ sources, remapped }) => { + const records = [ + { type: 'session', version: 2 }, + { type: 'tool/call', data: { callId: 'parallel' } }, + { type: 'subagent/catalog', data: { childId: 'child-z', childCreatedAt: 1, version: 0, mode: 'one-shot' } }, + { type: 'subagent/catalog', data: { childId: 'child-a', childCreatedAt: 2, version: 0, mode: 'one-shot' } }, + { type: 'tool/result', data: { callId: 'parallel' }, sourceEventSeqs: sources, surfaceOp: 'append' }, + ] + const normalized = normalizeSessionSnapshot(records.map(record => JSON.stringify(record)).join('\n'), ctx) + expect(normalized).toBe([ + records[0], + records[1], + { ...records[3], data: { ...records[3]?.data, childCreatedAt: 0 } }, + { ...records[2], data: { ...records[2]?.data, childCreatedAt: 0 } }, + { ...records[4], sourceEventSeqs: remapped }, + ].map(record => JSON.stringify(record)).join('\n') + '\n') + expect(normalizeSessionSnapshot(normalized, ctx)).toBe(normalized) + }) + it('omits current catalog facts and rebases source references for historical comparison', () => { const normalized = [ JSON.stringify({ type: 'session', id: '{{session:1}}' }), From 80e34ce7095c4b7721a32d4587c69adba828b56e Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Mon, 7 Sep 2026 17:00:33 +0800 Subject: [PATCH 17/58] perf(subagent): list parent catalog in event order --- .../2026-09-01-parent-owned-subagent-catalog.i18n.yaml | 4 ++-- .../2026-09-01-parent-owned-subagent-catalog.md | 2 +- .../2026-09-01-parent-owned-subagent-catalog.zh.md | 2 +- packages/subagent/subagent/README.i18n.yaml | 4 ++-- packages/subagent/subagent/README.md | 2 +- packages/subagent/subagent/README.zh.md | 2 +- packages/subagent/subagent/src/catalog.ts | 7 ++++--- packages/subagent/subagent/tests/catalog.spec.ts | 10 +++++----- 8 files changed, 17 insertions(+), 16 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml index b6fb66d7e5..3d9e0eed02 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md -2026-09-01-parent-owned-subagent-catalog.md: 6e0547c95db3ad5fc22219163785ae29afe3d707 -2026-09-01-parent-owned-subagent-catalog.zh.md: 0d99f8f7e513953b824da78c7200cdafda0e209e +2026-09-01-parent-owned-subagent-catalog.md: 9c7742cb537050d0edbf7985c1ed624ec2bcacfc +2026-09-01-parent-owned-subagent-catalog.zh.md: 3836e01df49396fef65dd7bb8fb469582078f060 diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md index 6e0547c95d..9c7742cb53 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md @@ -18,7 +18,7 @@ Creation publishes only successful facts. A one-shot run appends the catalog eve The child header and `subagent/descriptor` remain authoritative for recovery and composition. An Activation and the exact parent relationship remain authoritative for authorization and delivery. Mode and label are snapshotted once and the same detached values reach the parent catalog fact and child descriptor. -The registered host-only `subagentCatalog` projection materializes the parent facts. It stores facts in a persistent stack of 64-entry chunks, so an append copies at most the head chunk in bounded O(1) work. Materialization visits D facts and sorts them by `childCreatedAt`, then child id, in O(D log D). A projection checkpoint clones the state once in O(D); projection-cache writes remain asynchronous and use the existing mandatory creation, turn-end, and disposal points. +The registered host-only `subagentCatalog` projection materializes the parent facts. It stores facts in a persistent stack of 64-entry chunks, so an append copies at most the head chunk in bounded O(1) work. Materialization visits chunks from oldest to newest and preserves parent catalog event order in O(D) time for D facts. Concurrent creation is ordered by successful catalog append, independent of child timestamps and ids. A projection checkpoint clones the state once in O(D); projection-cache writes remain asynchronous and use the existing mandatory creation, turn-end, and disposal points. Fork isolation uses the exact `Session.inheritedEventCount` supplied to projection initialization. The fold ignores `subagent/catalog` events below that offset. The state stores the inherited offset but not each event seq because acceptance is decided during folding. diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md index 0d99f8f7e5..3836e01df4 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md @@ -18,7 +18,7 @@ parent Session 的 required `subagent/catalog` 事件是直接 child discovery child header 与 `subagent/descriptor` 继续拥有恢复与 composition 权威。Activation 与精确 parent 关系继续拥有授权与投递权威。mode 与 label 只快照一次,同一份分离值写入 parent catalog fact 与 child descriptor。 -注册的 host-only `subagentCatalog` projection 物化 parent fact。它以每块 64 项的持久 stack 保存事实,因此 append 最多复制 head chunk,以有界 O(1) 工作完成。materialization 访问 D 条事实,再按 `childCreatedAt` 与 child id 以 O(D log D) 排序。projection checkpoint 以 O(D) 克隆 state;projection-cache 继续异步写入,并使用既有创建、turn-end 与 disposal 强制点。 +注册的 host-only `subagentCatalog` projection 物化 parent fact。它以每块 64 项的持久 stack 保存事实,因此 append 最多复制 head chunk,以有界 O(1) 工作完成。materialization 从旧到新访问 chunk,对 D 条事实以 O(D) 时间保留父目录事件顺序。并发创建按目录成功追加的顺序排列,与 child 时间戳和 id 无关。projection checkpoint 以 O(D) 克隆 state;projection-cache 继续异步写入,并使用既有创建、turn-end 与 disposal 强制点。 fork 隔离使用 projection 初始化时提供的精确 `Session.inheritedEventCount`。fold 忽略该 offset 之前的 `subagent/catalog` 事件。state 保存 inherited offset,但不保存每条 event seq,因为接受判定已在 fold 时完成。 diff --git a/packages/subagent/subagent/README.i18n.yaml b/packages/subagent/subagent/README.i18n.yaml index 7ff5a6a616..9d53d1de77 100644 --- a/packages/subagent/subagent/README.i18n.yaml +++ b/packages/subagent/subagent/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/subagent/subagent/README.md -README.md: 1b57a43bbd565e46bd738fe94e0d324fd1310684 -README.zh.md: f3e49584f3b40984d616ddc6f269d0adacc424de +README.md: 4c2f8254663214bdd1400cb9859f3e54e33a41dc +README.zh.md: a65f0322972c473a7c7945c1d2cf479864c18e8d diff --git a/packages/subagent/subagent/README.md b/packages/subagent/subagent/README.md index 1b57a43bbd..4c2f825466 100644 --- a/packages/subagent/subagent/README.md +++ b/packages/subagent/subagent/README.md @@ -96,7 +96,7 @@ A request is validated against the provider's advertised capabilities, a durable The manager reserves a child identity, resolves the durable descriptor, creates (or cold-resumes) the child Agent, installs it in an Activation, and submits the prompt. Model-authored messages cross one parent/child edge through fixed Steer scheduling; browser human prompts choose Queue or best-effort Steer through an internal adapter, while other host protocols may retain Queue for distinct turns. A Session queue command admits a live subagent-owned Agent only from its own continuable descriptor. Settlement waits for Agent activity to finish, an empty Inbox, and no owned children, then flushes final Session state with admission open. Under the child lock, the manager revalidates the wake generation, Session sequence, Inbox, and owned children; the synchronous task entry of `Agent.runMaintenance()` claims the idle phase and closes the private subagent Inbox in the same JavaScript turn before handle disposal. An absent direct-child Activation cold-resumes from the persisted session. When a resident Activation settles, the manager tells the child's direct parent in the parent's own turn stream. -Successful local child creation appends a `subagent/catalog` fact to the parent Session. One-shot creation records it after the provider returns; continuable creation records it after initial inbox admission and before returning the child id. Failure releases the child without publishing a compensating catalog event. The host-only `subagentCatalog` projection excludes fork-inherited facts and can be read through an observation's requested projection state. [The parent-catalog decision](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md) owns ordering, persistence costs, and alternatives. +Successful local child creation appends a `subagent/catalog` fact to the parent Session. One-shot creation records it after the provider returns; continuable creation records it after initial inbox admission and before returning the child id. Failure releases the child without publishing a compensating catalog event. The host-only `subagentCatalog` projection excludes fork-inherited facts and returns entries in parent catalog event order in O(D) time for D facts; it can be read through an observation's requested projection state. [The parent-catalog decision](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md) owns ordering, persistence costs, and alternatives. ### Ownership and invariants diff --git a/packages/subagent/subagent/README.zh.md b/packages/subagent/subagent/README.zh.md index f3e49584f3..a65f032297 100644 --- a/packages/subagent/subagent/README.zh.md +++ b/packages/subagent/subagent/README.zh.md @@ -96,7 +96,7 @@ kind: "package-reference" 管理器预留 child 身份、解析持久化描述符、创建(或冷恢复)child、把它安装进 Activation 并提交提示词。模型编写的消息通过固定 Steer 调度跨一条 parent/child 边;浏览器人类 prompt 通过内部适配器选择 Queue 或 best-effort Steer,其他 host 协议仍可保留 Queue 以创建独立轮次。Session queue command 仅根据 child 自身的 continuable descriptor 准入在线 subagent-owned Agent。Settlement 会等待 Agent 活动结束、Inbox 为空且没有所拥有子级,再在准入开放时 flush 最终 Session 状态。管理器随后在 child lock 内重新验证 wake generation、Session 序号、Inbox 与所拥有子级;`Agent.runMaintenance()` 的同步 task 入口会占用 idle 阶段,并在同一个 JavaScript turn 内关闭私有 subagent Inbox,然后才 dispose handle。直接 child 不存在 Activation 时会从持久化会话冷恢复。当驻留 Activation 结算时,管理器会在 parent 自身的轮次流中告知该 child 的直接 parent。 -本地子级创建成功时,父 Session 追加一条 `subagent/catalog` 事实。一次性创建在 provider 返回后记录;可继续创建在初始 inbox 准入后、返回子级 id 前记录。失败会释放子级,不发布补偿性目录事件。仅 host 使用的 `subagentCatalog` projection 排除 fork 继承的事实,并可通过观察请求的 projection state 读取。[父目录决策](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md) 说明排序、持久化成本和替代方案。 +本地子级创建成功时,父 Session 追加一条 `subagent/catalog` 事实。一次性创建在 provider 返回后记录;可继续创建在初始 inbox 准入后、返回子级 id 前记录。失败会释放子级,不发布补偿性目录事件。仅 host 使用的 `subagentCatalog` projection 排除 fork 继承的事实,对 D 条事实以 O(D) 时间按父目录事件顺序返回条目,并可通过观察请求的 projection state 读取。[父目录决策](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md) 说明排序、持久化成本和替代方案。 ### 所有权与不变式 diff --git a/packages/subagent/subagent/src/catalog.ts b/packages/subagent/subagent/src/catalog.ts index 1ea5f86d9c..890b04f6fd 100644 --- a/packages/subagent/subagent/src/catalog.ts +++ b/packages/subagent/subagent/src/catalog.ts @@ -116,11 +116,13 @@ function appendFact(state: SubagentCatalogState, fact: SubagentCatalogEvent): Su /** * Materialize direct children from their parent's successful creation facts. * @param state - parent catalog fold state. - * @returns current direct-child rows in creation order. + * @returns current direct-child rows in parent catalog event order. */ export function subagentCatalogEntries(state: SubagentCatalogState): SubagentCatalogEntry[] { + const chunks: CatalogChunk[] = [] + for (let chunk = state.head; chunk !== undefined; chunk = chunk.previous) chunks.push(chunk) const entries: SubagentCatalogEntry[] = [] - for (let chunk = state.head; chunk !== undefined; chunk = chunk.previous) { + for (const chunk of chunks.reverse()) { for (const data of chunk.facts) { entries.push(data.mode === 'one-shot' ? { @@ -137,7 +139,6 @@ export function subagentCatalogEntries(state: SubagentCatalogState): SubagentCat }) } } - entries.sort((left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id)) return entries } diff --git a/packages/subagent/subagent/tests/catalog.spec.ts b/packages/subagent/subagent/tests/catalog.spec.ts index 9fb134f4b8..8983679574 100644 --- a/packages/subagent/subagent/tests/catalog.spec.ts +++ b/packages/subagent/subagent/tests/catalog.spec.ts @@ -48,14 +48,14 @@ function fact( } describe('subagent catalog projection', () => { - it('preserves earlier checkpoints across chunk rollover and restores every fact', () => { + it('preserves event order across chunk rollover and checkpoint restoration', () => { const first = fold(Array.from({ length: 64 }, (_, i) => - fact(i, `child-${i}`, i, { mode: 'one-shot' }))) + fact(i, `child-${i}`, 130 - i, { mode: 'one-shot' }))) const checkpoint = JSON.stringify(first) let state = first for (let i = 64; i < 130; i += 1) { state = subagentCatalogProjectionDefinition.apply( - state, fact(i, `child-${i}`, i, { mode: 'one-shot' }), + state, fact(i, `child-${i}`, 130 - i, { mode: 'one-shot' }), ) } const restored = subagentCatalogProjectionDefinition.stateSchema.parse( @@ -68,7 +68,7 @@ describe('subagent catalog projection', () => { .toEqual(Array.from({ length: 130 }, (_, i) => SessionId(`child-${i}`))) }) - it('validates recursive chunk state and materializes every label variant in stable order', () => { + it('validates recursive chunk state and materializes every label variant in event order', () => { const events: SessionEvent[] = [ fact(0, 'child-b', 1, { mode: 'one-shot' }), fact(1, 'child-a', 1, { mode: 'one-shot', label: 'once' }), @@ -90,8 +90,8 @@ describe('subagent catalog projection', () => { expect(subagentCatalogProjectionDefinition.stateSchema.parse(JSON.parse(JSON.stringify(state)))) .toEqual(state) expect(subagentCatalogEntries(state)).toEqual([ - { id: SessionId('child-a'), createdAt: 1, mode: 'one-shot', label: 'once' }, { id: SessionId('child-b'), createdAt: 1, mode: 'one-shot' }, + { id: SessionId('child-a'), createdAt: 1, mode: 'one-shot', label: 'once' }, { id: SessionId('child-d'), createdAt: 3, mode: 'continuable', label: 'later' }, ]) }) From e3cb6dd352da219b814989b034d9570ca1aeab50 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Tue, 8 Sep 2026 11:30:11 +0800 Subject: [PATCH 18/58] refactor(session): limit hydration views to all or none --- ...01-parent-owned-subagent-catalog.i18n.yaml | 4 +-- ...026-09-01-parent-owned-subagent-catalog.md | 2 +- ...-09-01-parent-owned-subagent-catalog.zh.md | 2 +- docs/subsystems/session-projection.i18n.yaml | 4 +-- docs/subsystems/session-projection.md | 16 +++++------ docs/subsystems/session-projection.zh.md | 16 +++++------ .../extensions/tool-cordis/src/api-catalog.ts | 16 +++++------ .../session-query/src/observation.ts | 5 ++-- .../session-query/tests/observation.spec.ts | 2 +- .../session-projection-cache/README.i18n.yaml | 4 +-- .../session-projection-cache/README.md | 2 +- .../session-projection-cache/README.zh.md | 2 +- .../session-projection-cache/src/index.ts | 12 ++++----- .../tests/cache.spec.ts | 9 ++----- .../session-projection/README.i18n.yaml | 4 +-- packages/session/session-projection/README.md | 2 +- .../session/session-projection/README.zh.md | 2 +- .../session/session-projection/src/index.ts | 27 +++++++++---------- 18 files changed, 62 insertions(+), 69 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml index 3d9e0eed02..55dad331e2 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md -2026-09-01-parent-owned-subagent-catalog.md: 9c7742cb537050d0edbf7985c1ed624ec2bcacfc -2026-09-01-parent-owned-subagent-catalog.zh.md: 3836e01df49396fef65dd7bb8fb469582078f060 +2026-09-01-parent-owned-subagent-catalog.md: fced05dab48d281a2a89a60da03163f15e2e7b74 +2026-09-01-parent-owned-subagent-catalog.zh.md: 527583813ca0fa8cf06fd721f68ce5d2c8fa81eb diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md index 9c7742cb53..fced05dab4 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md @@ -38,6 +38,6 @@ Snapshot normalizers zero `childCreatedAt` because it originates from the proces ## Consequences -A caller can request `projectionStateKeys: ['subagentCatalog']` from `observeSession`. Live observations clone the maintained registry state; cold observations hydrate their prepared Session and detach the same state at the observation cursor. A host-state-only read passes an empty view selection through checkpoint hydration, including cache reuse and malformed-row recovery; unrelated wire views are neither computed nor validated. Direct-child and descendant listing still use the Session corpus and child identity projection. +A caller can request `projectionStateKeys: ['subagentCatalog']` from `observeSession`. Live observations clone the maintained registry state; cold observations hydrate their prepared Session and detach the same state at the observation cursor. A host-state-only read passes `projectionMode: 'none'` through checkpoint hydration, including cache reuse and malformed-row recovery; unrelated wire views are neither computed nor validated. Hydration accepts only `all` or `none` because observations do not request individual client view keys. Direct-child and descendant listing still use the Session corpus and child identity projection. Backends that do not know the required event refuse the log under the existing Session event mechanism. Pre-release format policy requires no fallback scan for old logs. diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md index 3836e01df4..527583813c 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md @@ -38,6 +38,6 @@ snapshot normalizer 会把 `childCreatedAt` 归零,因为它来自 process clo ## 后果 -调用方可以向 `observeSession` 请求 `projectionStateKeys: ['subagentCatalog']`。实时观察克隆 registry 维护中的 state;冷观察 hydrate 已准备的 Session,并在观察 cursor 处分离出同一 state。仅读取 host state 时,空视图选择会传递到检查点 hydration,包括缓存复用和损坏 row 的恢复;无关 wire view 不会被计算或校验。直接子级和后代列表仍使用 Session 语料库与子级身份 projection。 +调用方可以向 `observeSession` 请求 `projectionStateKeys: ['subagentCatalog']`。实时观察克隆 registry 维护中的 state;冷观察 hydrate 已准备的 Session,并在观察 cursor 处分离出同一 state。仅读取 host state 时,`projectionMode: 'none'` 会传递到检查点 hydration,包括缓存复用和损坏 row 的恢复;无关 wire view 不会被计算或校验。Hydration 只接受 `all` 或 `none`,因为观察不会请求单独的客户端视图 key。直接子级和后代列表仍使用 Session 语料库与子级身份 projection。 不认识该 required event 的 backend 会按既有 Session event 机制拒绝日志。pre-release format policy 不要求为旧日志保留 fallback scan。 diff --git a/docs/subsystems/session-projection.i18n.yaml b/docs/subsystems/session-projection.i18n.yaml index 03a6f38bbd..6d8dea9d69 100644 --- a/docs/subsystems/session-projection.i18n.yaml +++ b/docs/subsystems/session-projection.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/session-projection.md -session-projection.md: 52e99245609872c61853792250a4e0fcf631f279 -session-projection.zh.md: 372c530ba36e83240c431c2c07edc2d39fb7d21b +session-projection.md: 7552885c947b268831a9d3109244fea738e9ef2b +session-projection.zh.md: eeb3bef9b52b5b8ab88e48a3bff61ccaacebfaaf diff --git a/docs/subsystems/session-projection.md b/docs/subsystems/session-projection.md index 52e9924560..7552885c94 100644 --- a/docs/subsystems/session-projection.md +++ b/docs/subsystems/session-projection.md @@ -163,10 +163,10 @@ cachedPredecessorTitle( meta: SessionHeader, inheritedEventCount: SessionLogOffs * because the logical observation may contain recovery events not yet durable. * @param session - exact unpublished Session retained by persistence. * @param events - exact logical event prefix represented by the observation. - * @param keys - optional client-visible outputs; an empty list hydrates states without computing or validating views. - * @returns selected projection values at the event cut, defaulting to all views. + * @param projectionMode - `none` hydrates every state without computing or validating client views; defaults to `all`. + * @returns the event cut with all client views, or empty values in `none` mode. */ -hydratePrepared( session: Session, events: readonly SessionEvent[], keys?: readonly Extract[], ): ProjectionSnapshot +hydratePrepared( session: Session, events: readonly SessionEvent[], projectionMode: 'all' | 'none' = 'all', ): ProjectionSnapshot /** * Durably checkpoint one live session NOW (all mandatory points call @@ -330,12 +330,12 @@ viewCheckpoint( checkpoint: ProjectionCheckpoint, keys?: readonly Extract[], ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint } +restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, header: SessionHeader, inheritedEventCount: SessionLogOffset, projectionMode: 'all' | 'none' = 'all', ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint } /** * Restore an exact cut and install its states on the supplied prepared Session. @@ -345,10 +345,10 @@ restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], base * @param checkpoint - persisted rows for this Session lifecycle. * @param events - exact events at the observation cut. * @param baseSeq - first supplied event sequence. - * @param keys - optional client-visible outputs; an empty list installs every state without computing or validating views. - * @returns selected projection values at the supplied cut, defaulting to all views. + * @param projectionMode - `none` installs every state without computing or validating client views; defaults to `all`. + * @returns the supplied cut with all client views, or empty values in `none` mode. */ -hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, keys?: readonly Extract[], ): ProjectionSnapshot +hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, projectionMode: 'all' | 'none' = 'all', ): ProjectionSnapshot ``` Types: [Session](session.md) · [SessionEvent](session.md) · [SessionHeader](persistence.md) · [SessionLogOffset](session.md) diff --git a/docs/subsystems/session-projection.zh.md b/docs/subsystems/session-projection.zh.md index 372c530ba3..eeb3bef9b5 100644 --- a/docs/subsystems/session-projection.zh.md +++ b/docs/subsystems/session-projection.zh.md @@ -163,10 +163,10 @@ cachedPredecessorTitle( meta: SessionHeader, inheritedEventCount: SessionLogOffs * because the logical observation may contain recovery events not yet durable. * @param session - exact unpublished Session retained by persistence. * @param events - exact logical event prefix represented by the observation. - * @param keys - optional client-visible outputs; an empty list hydrates states without computing or validating views. - * @returns selected projection values at the event cut, defaulting to all views. + * @param projectionMode - `none` hydrates every state without computing or validating client views; defaults to `all`. + * @returns the event cut with all client views, or empty values in `none` mode. */ -hydratePrepared( session: Session, events: readonly SessionEvent[], keys?: readonly Extract[], ): ProjectionSnapshot +hydratePrepared( session: Session, events: readonly SessionEvent[], projectionMode: 'all' | 'none' = 'all', ): ProjectionSnapshot /** * Durably checkpoint one live session NOW (all mandatory points call @@ -330,12 +330,12 @@ viewCheckpoint( checkpoint: ProjectionCheckpoint, keys?: readonly Extract[], ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint } +restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, header: SessionHeader, inheritedEventCount: SessionLogOffset, projectionMode: 'all' | 'none' = 'all', ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint } /** * Restore an exact cut and install its states on the supplied prepared Session. @@ -345,10 +345,10 @@ restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], base * @param checkpoint - persisted rows for this Session lifecycle. * @param events - exact events at the observation cut. * @param baseSeq - first supplied event sequence. - * @param keys - optional client-visible outputs; an empty list installs every state without computing or validating views. - * @returns selected projection values at the supplied cut, defaulting to all views. + * @param projectionMode - `none` installs every state without computing or validating client views; defaults to `all`. + * @returns the supplied cut with all client views, or empty values in `none` mode. */ -hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, keys?: readonly Extract[], ): ProjectionSnapshot +hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, projectionMode: 'all' | 'none' = 'all', ): ProjectionSnapshot ``` Types: [Session](session.zh.md) · [SessionEvent](session.zh.md) · [SessionHeader](persistence.zh.md) · [SessionLogOffset](session.zh.md) diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index dba22a4046..e1a265aa0a 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -1627,10 +1627,10 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ returns: 'a title-only checkpoint view with `asOfSeq: -1`, or `undefined` when the record is current, newer, unrelated, missing, or incompatible with the title unit. The sentinel avoids reusing a sequence that a cardinality-changing Session migration may have remapped.', }, { - signature: 'hydratePrepared( session: Session, events: readonly SessionEvent[], keys?: readonly Extract[], ): ProjectionSnapshot', + signature: 'hydratePrepared( session: Session, events: readonly SessionEvent[], projectionMode: \'all\' | \'none\' = \'all\', ): ProjectionSnapshot', description: 'Hydrate projection cells for an already-prepared Session without another persistence read. The cache seeds matching rows; the supplied exact log advances every unit to the observation cut. No checkpoint is written because the logical observation may contain recovery events not yet durable.', - parameters: [{ name: 'session', description: 'exact unpublished Session retained by persistence.' }, { name: 'events', description: 'exact logical event prefix represented by the observation.' }, { name: 'keys', description: 'optional client-visible outputs; an empty list hydrates states without computing or validating views.' }], - returns: 'selected projection values at the event cut, defaulting to all views.', + parameters: [{ name: 'session', description: 'exact unpublished Session retained by persistence.' }, { name: 'events', description: 'exact logical event prefix represented by the observation.' }, { name: 'projectionMode', description: '`none` hydrates every state without computing or validating client views; defaults to `all`.' }], + returns: 'the event cut with all client views, or empty values in `none` mode.', }, { signature: 'async write(session: Session): Promise', @@ -1706,16 +1706,16 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ returns: 'whole values per key with a usable row; empty when none.', }, { - signature: 'restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, header: SessionHeader, inheritedEventCount: SessionLogOffset, keys?: readonly Extract[], ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }', + signature: 'restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, header: SessionHeader, inheritedEventCount: SessionLogOffset, projectionMode: \'all\' | \'none\' = \'all\', ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }', description: 'Cold read: fold every persisted unit over a stored log suffix, seeding each from its checkpoint row when usable — the one read recipe (cached state + forward tail replay + `view`) applied without a live `Session`. Call with the stored events at or past `restoreFloor(checkpoint)` (a `SessionHandle.read` slice) and that same floor as `baseSeq`; the floor\'s one-below anchor makes the supplied end honest, so a shrunk log is detected here. A row is usable iff its `ver` matches the live unit\'s `stateVersion`, it does not predate `baseSeq` (`seq >= baseSeq - 1`), and it does not claim events past the supplied end (`seq <= endSeq`); an unusable row is discarded and its key refolds from `init` — which is only sound over the full log, so a discarded row with `baseSeq > 0` throws (the caller re-reads from seq 0, e.g. after a crash-repair truncation shrank the log below a row\'s watermark).', - parameters: [{ name: 'checkpoint', description: 'persisted rows for one session (possibly stale or empty).' }, { name: 'events', description: 'the stored events with `seq >= baseSeq`, in seq order.' }, { name: 'baseSeq', description: 'the seq `events` starts at (its first event\'s seq when non-empty).' }, { name: 'header', description: 'immutable metadata for the Session being restored.' }, { name: 'inheritedEventCount', description: 'exact fork-inherited prefix length supplied to unit initialization.' }, { name: 'keys', description: 'optional client-visible outputs; an empty list restores every state without computing or validating views.' }], + parameters: [{ name: 'checkpoint', description: 'persisted rows for one session (possibly stale or empty).' }, { name: 'events', description: 'the stored events with `seq >= baseSeq`, in seq order.' }, { name: 'baseSeq', description: 'the seq `events` starts at (its first event\'s seq when non-empty).' }, { name: 'header', description: 'immutable metadata for the Session being restored.' }, { name: 'inheritedEventCount', description: 'exact fork-inherited prefix length supplied to unit initialization.' }, { name: 'projectionMode', description: '`none` restores every state without computing or validating client views; defaults to `all`.' }], returns: 'the snapshot cut at the supplied log end (`asOfSeq` is the last supplied event\'s seq, `baseSeq - 1` for an empty tail) plus the refreshed checkpoint rows at that cut, ready for a durable write-back.', }, { - signature: 'hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, keys?: readonly Extract[], ): ProjectionSnapshot', + signature: 'hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, projectionMode: \'all\' | \'none\' = \'all\', ): ProjectionSnapshot', description: 'Restore an exact cut and install its states on the supplied prepared Session. A later publication reuses these cells; ordinary live reads and event drive advance any constructor-owned suffix exactly once.', - parameters: [{ name: 'session', description: 'exact prepared Session that owns the restored log prefix.' }, { name: 'checkpoint', description: 'persisted rows for this Session lifecycle.' }, { name: 'events', description: 'exact events at the observation cut.' }, { name: 'baseSeq', description: 'first supplied event sequence.' }, { name: 'keys', description: 'optional client-visible outputs; an empty list installs every state without computing or validating views.' }], - returns: 'selected projection values at the supplied cut, defaulting to all views.', + parameters: [{ name: 'session', description: 'exact prepared Session that owns the restored log prefix.' }, { name: 'checkpoint', description: 'persisted rows for this Session lifecycle.' }, { name: 'events', description: 'exact events at the observation cut.' }, { name: 'baseSeq', description: 'first supplied event sequence.' }, { name: 'projectionMode', description: '`none` installs every state without computing or validating client views; defaults to `all`.' }], + returns: 'the supplied cut with all client views, or empty values in `none` mode.', }, ], }, diff --git a/packages/session-query/session-query/src/observation.ts b/packages/session-query/session-query/src/observation.ts index eb1a886243..e5492d038a 100644 --- a/packages/session-query/session-query/src/observation.ts +++ b/packages/session-query/session-query/src/observation.ts @@ -337,10 +337,9 @@ export class SessionObservationReader { const registry = this.ctx.get('sessionProjections') if (registry === undefined) return undefined const cache = this.ctx.get('sessionProjectionCache') - const keys = projectionMode === 'none' ? [] : undefined return cache === undefined - ? registry.hydrate(entry.session, {}, entry.events, SessionLogOffset(0), keys) - : cache.hydratePrepared(entry.session, entry.events, keys) + ? registry.hydrate(entry.session, {}, entry.events, SessionLogOffset(0), projectionMode) + : cache.hydratePrepared(entry.session, entry.events, projectionMode) } /** diff --git a/packages/session-query/session-query/tests/observation.spec.ts b/packages/session-query/session-query/tests/observation.spec.ts index 93a0e4c191..101669ebed 100644 --- a/packages/session-query/session-query/tests/observation.spec.ts +++ b/packages/session-query/session-query/tests/observation.spec.ts @@ -779,7 +779,7 @@ describe('SessionObservationReader cold projections', () => { expect(observed.projections).toBe(projectionMode === 'all' ? snapshot : undefined) expect(hydratePrepared).toHaveBeenCalledOnce() - expect(hydratePrepared.mock.calls[0]?.[2]).toEqual(projectionMode === 'all' ? undefined : []) + expect(hydratePrepared.mock.calls[0]?.[2]).toBe(projectionMode) await ctx.fiber.dispose() }) diff --git a/packages/session/session-projection-cache/README.i18n.yaml b/packages/session/session-projection-cache/README.i18n.yaml index 549dd6c0d8..92b3540cd1 100644 --- a/packages/session/session-projection-cache/README.i18n.yaml +++ b/packages/session/session-projection-cache/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session/session-projection-cache/README.md -README.md: ab9c4f0ebb8f68d53ebdb952997805f2b52f59af -README.zh.md: c6ac28788ea4000327e0232d45fb0e1344e500da +README.md: 9ef25b737252d051b16b375442b8edefbd92a939 +README.zh.md: 650d6e4fe7fcd3365c99975c72e140d26f9592bf diff --git a/packages/session/session-projection-cache/README.md b/packages/session/session-projection-cache/README.md index ab9c4f0ebb..9ef25b7372 100644 --- a/packages/session/session-projection-cache/README.md +++ b/packages/session/session-projection-cache/README.md @@ -52,7 +52,7 @@ The cache opens its domain through the storage stack, so base mounts `storage`, The plugin injects `storageDomain`, `sessionProjections`, and `sessions`. The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-session-projection-cache) is the exhaustive source for every accepted field and its JSDoc. -`hydratePrepared` accepts optional view keys and restores every state from the checkpoint and event tail. An empty key list suppresses client view computation and validation, including when a malformed checkpoint requires a full refold. +`hydratePrepared` restores every state from the checkpoint and event tail. Its `projectionMode` defaults to `all`; `none` suppresses client view computation and validation, including when a malformed checkpoint requires a full refold. ### How checkpoints are written diff --git a/packages/session/session-projection-cache/README.zh.md b/packages/session/session-projection-cache/README.zh.md index c6ac28788e..650d6e4fe7 100644 --- a/packages/session/session-projection-cache/README.zh.md +++ b/packages/session/session-projection-cache/README.zh.md @@ -52,7 +52,7 @@ kind: "package-reference" 本插件注入 `storageDomain`、`sessionProjections` 与 `sessions`。生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-session-projection-cache)是每个受支持字段及其 JSDoc 的穷尽式真源。 -`hydratePrepared` 接受可选的视图 key,并从检查点与事件尾部恢复所有状态。空 key 列表会跳过客户端视图的计算与校验,即使检查点损坏、需要重新折叠完整日志也如此。 +`hydratePrepared` 从检查点与事件尾部恢复所有状态。其 `projectionMode` 默认为 `all`;`none` 会跳过客户端视图的计算与校验,即使检查点损坏、需要重新折叠完整日志也如此。 ### 检查点如何写入 diff --git a/packages/session/session-projection-cache/src/index.ts b/packages/session/session-projection-cache/src/index.ts index af5636c68c..079cc388f7 100644 --- a/packages/session/session-projection-cache/src/index.ts +++ b/packages/session/session-projection-cache/src/index.ts @@ -207,20 +207,20 @@ export class SessionProjectionCache extends Service { * because the logical observation may contain recovery events not yet durable. * @param session - exact unpublished Session retained by persistence. * @param events - exact logical event prefix represented by the observation. - * @param keys - optional client-visible outputs; an empty list hydrates states without computing or validating views. - * @returns selected projection values at the event cut, defaulting to all views. + * @param projectionMode - `none` hydrates every state without computing or validating client views; defaults to `all`. + * @returns the event cut with all client views, or empty values in `none` mode. */ hydratePrepared( session: Session, events: readonly SessionEvent[], - keys?: readonly Extract[], + projectionMode: 'all' | 'none' = 'all', ): ProjectionSnapshot { const record = this.recordFor( session.id, identityOf(session.header, session.inheritedEventCount), ) if (record === undefined) { - return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0), keys) + return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0), projectionMode) } try { return this.ctx.sessionProjections.hydrate( @@ -228,12 +228,12 @@ export class SessionProjectionCache extends Service { record.rows, events, SessionLogOffset(0), - keys, + projectionMode, ) } catch { // Cached rows are disposable derived data. Retry from the exact log so a // stale schema cannot make a valid Session unreadable. - return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0), keys) + return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0), projectionMode) } } diff --git a/packages/session/session-projection-cache/tests/cache.spec.ts b/packages/session/session-projection-cache/tests/cache.spec.ts index 1774adb6ad..9bcd2a5d2c 100644 --- a/packages/session/session-projection-cache/tests/cache.spec.ts +++ b/packages/session/session-projection-cache/tests/cache.spec.ts @@ -651,7 +651,7 @@ describe('SessionProjectionCache cold-read seeding', () => { }) }) - it.each(['matching', 'malformed', 'absent'])('hydrates every state with only selected views from a %s checkpoint', async (record) => { + it.each(['matching', 'malformed', 'absent'])('hydrates host states without client views from a %s checkpoint', async (record) => { const root = await mkdtemp(join(tmpdir(), 'dsh-projcache-')) roots.push(root) const id = SessionId('selected-views') @@ -673,17 +673,12 @@ describe('SessionProjectionCache cold-read seeding', () => { const marks = { marks: [record === 'matching' ? 'cached' : 'fresh'] } for (let read = 0; read < 2; read++) { - expect(cache.hydratePrepared(session, events, [])).toEqual({ asOfSeq: 2, values: {} }) + expect(cache.hydratePrepared(session, events, 'none')).toEqual({ asOfSeq: 2, values: {} }) expect(ctx.sessionProjections.stateOf(session, 'cache-test/marks')).toEqual(marks) expect(ctx.sessionProjections.stateOf(session, 'cache-test/secondary-marks')).toEqual({ marks: ['fresh'] }) expect(view).not.toHaveBeenCalled() expect(parseView).not.toHaveBeenCalled() } - expect(cache.hydratePrepared(session, events, ['cache-test/marks'])).toEqual({ - asOfSeq: 2, - values: { 'cache-test/marks': marks }, - }) - expect(view).not.toHaveBeenCalled() expect(cache.hydratePrepared(session, events).values).toEqual({ 'cache-test/marks': marks, 'cache-test/secondary-marks': { marks: ['fresh'] }, diff --git a/packages/session/session-projection/README.i18n.yaml b/packages/session/session-projection/README.i18n.yaml index 2d8e377de7..b92fb0f575 100644 --- a/packages/session/session-projection/README.i18n.yaml +++ b/packages/session/session-projection/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session/session-projection/README.md -README.md: 9cbe4ea8f065787ee8ba568d4598a2a1cf69812a -README.zh.md: 16fa8202c4fe13823062bc3396f63e19c26713cc +README.md: 9bd08d119cf1961a47595bec29efabdc8d7fb276 +README.zh.md: e7982626f6e0c734ef426ec1a2851562ca01525a diff --git a/packages/session/session-projection/README.md b/packages/session/session-projection/README.md index 9cbe4ea8f0..9bd08d119c 100644 --- a/packages/session/session-projection/README.md +++ b/packages/session/session-projection/README.md @@ -64,7 +64,7 @@ const { asOfSeq, values } = ctx.sessionProjections.snapshot(session) A domain that requires projected state declares `sessionProjections` as a Cordis service dependency; optional contributors may register under `ctx.inject(['sessionProjections'], …)`. Carriers use `ctx.get('sessionProjections')` and omit their block or frames when the registry is absent. -`restore` and `hydrate` accept optional view keys. Omitting them returns all client views; an empty list restores every host state without computing or validating any client view. +`restore` and `hydrate` accept `projectionMode`, which defaults to `all`. Mode `none` restores every host state without computing or validating client views. ### Persisted checkpoints diff --git a/packages/session/session-projection/README.zh.md b/packages/session/session-projection/README.zh.md index 16fa8202c4..e7982626f6 100644 --- a/packages/session/session-projection/README.zh.md +++ b/packages/session/session-projection/README.zh.md @@ -64,7 +64,7 @@ const { asOfSeq, values } = ctx.sessionProjections.snapshot(session) 必须使用投影状态的领域把 `sessionProjections` 声明为 Cordis 服务依赖;可选贡献方可以在 `ctx.inject(['sessionProjections'], …)` 下注册。载体使用 `ctx.get('sessionProjections')`,注册表缺席时省略自己的块或帧。 -`restore` 和 `hydrate` 接受可选的视图 key。省略时返回全部客户端视图;传入空列表时恢复所有 host state,但不计算或校验任何客户端视图。 +`restore` 和 `hydrate` 接受 `projectionMode`,默认值为 `all`。模式 `none` 恢复所有 host state,但不计算或校验客户端视图。 ### 持久检查点 diff --git a/packages/session/session-projection/src/index.ts b/packages/session/session-projection/src/index.ts index 8488d12e62..d7cff2e765 100644 --- a/packages/session/session-projection/src/index.ts +++ b/packages/session/session-projection/src/index.ts @@ -488,7 +488,7 @@ export class SessionProjectionRegistry extends Service { * @param baseSeq - the seq `events` starts at (its first event's seq when non-empty). * @param header - immutable metadata for the Session being restored. * @param inheritedEventCount - exact fork-inherited prefix length supplied to unit initialization. - * @param keys - optional client-visible outputs; an empty list restores every state without computing or validating views. + * @param projectionMode - `none` restores every state without computing or validating client views; defaults to `all`. * @returns the snapshot cut at the supplied log end (`asOfSeq` is the last * supplied event's seq, `baseSeq - 1` for an empty tail) plus the * refreshed checkpoint rows at that cut, ready for a durable write-back. @@ -499,14 +499,13 @@ export class SessionProjectionRegistry extends Service { baseSeq: SessionLogOffset, header: SessionHeader, inheritedEventCount: SessionLogOffset, - keys?: readonly Extract[], + projectionMode: 'all' | 'none' = 'all', ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint } { const endSeq: SessionSeqCursor = events.at(-1)?.seq ?? cursorBefore(baseSeq) const beforeBase = cursorBefore(baseSeq) const values: Record = {} const refreshed: ProjectionCheckpoint = {} - const selected = keys === undefined ? undefined : new Set(keys) for (const registration of this.registrations.values()) { const def = registration.def const row = checkpoint[def.key] @@ -533,7 +532,7 @@ export class SessionProjectionRegistry extends Service { } state = def.apply(state, event) } - if (def.wire !== undefined && (selected === undefined || selected.has(def.key))) { + if (projectionMode === 'all' && def.wire !== undefined) { values[def.key] = def.wire.viewSchema.parse(def.wire.view(state)) } refreshed[def.key] = { ver: def.stateVersion, seq: endSeq, val: state } @@ -552,15 +551,15 @@ export class SessionProjectionRegistry extends Service { * @param checkpoint - persisted rows for this Session lifecycle. * @param events - exact events at the observation cut. * @param baseSeq - first supplied event sequence. - * @param keys - optional client-visible outputs; an empty list installs every state without computing or validating views. - * @returns selected projection values at the supplied cut, defaulting to all views. + * @param projectionMode - `none` installs every state without computing or validating client views; defaults to `all`. + * @returns the supplied cut with all client views, or empty values in `none` mode. */ hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, - keys?: readonly Extract[], + projectionMode: 'all' | 'none' = 'all', ): ProjectionSnapshot { const endSeq: SessionSeqCursor = events.at(-1)?.seq ?? cursorBefore(baseSeq) let complete = true @@ -573,12 +572,12 @@ export class SessionProjectionRegistry extends Service { } if (complete) { const values: Record = {} - const selected = keys === undefined ? undefined : new Set(keys) - for (const registration of this.registrations.values()) { - if (registration.def.wire === undefined) continue - if (selected !== undefined && !selected.has(registration.def.key)) continue - const current = registration.cells.get(session) as UnitCell - values[registration.def.key] = this.viewCell(registration, current) + if (projectionMode === 'all') { + for (const registration of this.registrations.values()) { + if (registration.def.wire === undefined) continue + const current = registration.cells.get(session) as UnitCell + values[registration.def.key] = this.viewCell(registration, current) + } } return { asOfSeq: endSeq, values } } @@ -588,7 +587,7 @@ export class SessionProjectionRegistry extends Service { baseSeq, session.header, session.inheritedEventCount, - keys, + projectionMode, ) for (const registration of this.registrations.values()) { const row = restored.checkpoint[registration.def.key] From 3dcabbba4411f15b9b08f1eb56da6b2e8e2e0e0d Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Tue, 8 Sep 2026 15:32:59 +0800 Subject: [PATCH 19/58] refactor(session-query): return all projection states by default --- ...01-parent-owned-subagent-catalog.i18n.yaml | 4 +- ...026-09-01-parent-owned-subagent-catalog.md | 2 +- ...-09-01-parent-owned-subagent-catalog.zh.md | 2 +- docs/subsystems/session-projection.i18n.yaml | 4 +- docs/subsystems/session-projection.md | 13 ++- docs/subsystems/session-projection.zh.md | 13 ++- .../extensions/tool-cordis/src/api-catalog.ts | 18 ++-- .../session-query/README.i18n.yaml | 4 +- .../session-query/session-query/README.md | 2 +- .../session-query/session-query/README.zh.md | 2 +- .../session-query/src/observation.ts | 62 ++++--------- .../session-query/tests/observation.spec.ts | 90 ++++++++++--------- .../session-projection-cache/README.i18n.yaml | 4 +- .../session-projection-cache/README.md | 3 - .../session-projection-cache/README.zh.md | 3 - .../session-projection-cache/src/index.ts | 9 +- .../tests/cache.spec.ts | 35 -------- .../session-projection/README.i18n.yaml | 4 +- packages/session/session-projection/README.md | 1 - .../session/session-projection/README.zh.md | 1 - .../session/session-projection/src/index.ts | 21 ++--- 21 files changed, 108 insertions(+), 189 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml index 55dad331e2..2a82fc2336 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md -2026-09-01-parent-owned-subagent-catalog.md: fced05dab48d281a2a89a60da03163f15e2e7b74 -2026-09-01-parent-owned-subagent-catalog.zh.md: 527583813ca0fa8cf06fd721f68ce5d2c8fa81eb +2026-09-01-parent-owned-subagent-catalog.md: f424943c523c1143884116fd5aeccdf6e20423f2 +2026-09-01-parent-owned-subagent-catalog.zh.md: b4c524ab81e4c2df9a666d0119e1de9c8b771e8b diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md index fced05dab4..f424943c52 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md @@ -38,6 +38,6 @@ Snapshot normalizers zero `childCreatedAt` because it originates from the proces ## Consequences -A caller can request `projectionStateKeys: ['subagentCatalog']` from `observeSession`. Live observations clone the maintained registry state; cold observations hydrate their prepared Session and detach the same state at the observation cursor. A host-state-only read passes `projectionMode: 'none'` through checkpoint hydration, including cache reuse and malformed-row recovery; unrelated wire views are neither computed nor validated. Hydration accepts only `all` or `none` because observations do not request individual client view keys. Direct-child and descendant listing still use the Session corpus and child identity projection. +Default Session observations expose every registered host state in `projectionStates` alongside client views. The registry’s existing `checkpoint` operation supplies detached state values; no key-selection parameter or additional hydration mode is required. `projectionMode: 'none'` retains its projection-free behavior. Direct-child and descendant listing still use the Session corpus and child identity projection. Backends that do not know the required event refuse the log under the existing Session event mechanism. Pre-release format policy requires no fallback scan for old logs. diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md index 527583813c..b4c524ab81 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md @@ -38,6 +38,6 @@ snapshot normalizer 会把 `childCreatedAt` 归零,因为它来自 process clo ## 后果 -调用方可以向 `observeSession` 请求 `projectionStateKeys: ['subagentCatalog']`。实时观察克隆 registry 维护中的 state;冷观察 hydrate 已准备的 Session,并在观察 cursor 处分离出同一 state。仅读取 host state 时,`projectionMode: 'none'` 会传递到检查点 hydration,包括缓存复用和损坏 row 的恢复;无关 wire view 不会被计算或校验。Hydration 只接受 `all` 或 `none`,因为观察不会请求单独的客户端视图 key。直接子级和后代列表仍使用 Session 语料库与子级身份 projection。 +默认 Session 观察在返回客户端视图的同时,通过 `projectionStates` 暴露全部已注册 host state。注册表现有的 `checkpoint` 操作提供独立状态值,无需 key 选择参数或额外 hydration 模式。`projectionMode: 'none'` 保留不执行投影的行为。直接子级和后代列表仍使用 Session 语料库与子级身份 projection。 不认识该 required event 的 backend 会按既有 Session event 机制拒绝日志。pre-release format policy 不要求为旧日志保留 fallback scan。 diff --git a/docs/subsystems/session-projection.i18n.yaml b/docs/subsystems/session-projection.i18n.yaml index 6d8dea9d69..39bbf0016a 100644 --- a/docs/subsystems/session-projection.i18n.yaml +++ b/docs/subsystems/session-projection.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/session-projection.md -session-projection.md: 7552885c947b268831a9d3109244fea738e9ef2b -session-projection.zh.md: eeb3bef9b52b5b8ab88e48a3bff61ccaacebfaaf +session-projection.md: 8568b6df94b1427a341568777c64c82d74176a17 +session-projection.zh.md: 5e50341a6443efde2a3b49b7f5185bc44896efc6 diff --git a/docs/subsystems/session-projection.md b/docs/subsystems/session-projection.md index 7552885c94..8568b6df94 100644 --- a/docs/subsystems/session-projection.md +++ b/docs/subsystems/session-projection.md @@ -163,10 +163,9 @@ cachedPredecessorTitle( meta: SessionHeader, inheritedEventCount: SessionLogOffs * because the logical observation may contain recovery events not yet durable. * @param session - exact unpublished Session retained by persistence. * @param events - exact logical event prefix represented by the observation. - * @param projectionMode - `none` hydrates every state without computing or validating client views; defaults to `all`. - * @returns the event cut with all client views, or empty values in `none` mode. + * @returns all projection values at the event cut. */ -hydratePrepared( session: Session, events: readonly SessionEvent[], projectionMode: 'all' | 'none' = 'all', ): ProjectionSnapshot +hydratePrepared( session: Session, events: readonly SessionEvent[], ): ProjectionSnapshot /** * Durably checkpoint one live session NOW (all mandatory points call @@ -330,12 +329,11 @@ viewCheckpoint( checkpoint: ProjectionCheckpoint, keys?: readonly Extract', @@ -1706,16 +1706,16 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ returns: 'whole values per key with a usable row; empty when none.', }, { - signature: 'restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, header: SessionHeader, inheritedEventCount: SessionLogOffset, projectionMode: \'all\' | \'none\' = \'all\', ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }', + signature: 'restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, header: SessionHeader, inheritedEventCount: SessionLogOffset, ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }', description: 'Cold read: fold every persisted unit over a stored log suffix, seeding each from its checkpoint row when usable — the one read recipe (cached state + forward tail replay + `view`) applied without a live `Session`. Call with the stored events at or past `restoreFloor(checkpoint)` (a `SessionHandle.read` slice) and that same floor as `baseSeq`; the floor\'s one-below anchor makes the supplied end honest, so a shrunk log is detected here. A row is usable iff its `ver` matches the live unit\'s `stateVersion`, it does not predate `baseSeq` (`seq >= baseSeq - 1`), and it does not claim events past the supplied end (`seq <= endSeq`); an unusable row is discarded and its key refolds from `init` — which is only sound over the full log, so a discarded row with `baseSeq > 0` throws (the caller re-reads from seq 0, e.g. after a crash-repair truncation shrank the log below a row\'s watermark).', - parameters: [{ name: 'checkpoint', description: 'persisted rows for one session (possibly stale or empty).' }, { name: 'events', description: 'the stored events with `seq >= baseSeq`, in seq order.' }, { name: 'baseSeq', description: 'the seq `events` starts at (its first event\'s seq when non-empty).' }, { name: 'header', description: 'immutable metadata for the Session being restored.' }, { name: 'inheritedEventCount', description: 'exact fork-inherited prefix length supplied to unit initialization.' }, { name: 'projectionMode', description: '`none` restores every state without computing or validating client views; defaults to `all`.' }], + parameters: [{ name: 'checkpoint', description: 'persisted rows for one session (possibly stale or empty).' }, { name: 'events', description: 'the stored events with `seq >= baseSeq`, in seq order.' }, { name: 'baseSeq', description: 'the seq `events` starts at (its first event\'s seq when non-empty).' }, { name: 'header', description: 'immutable metadata for the Session being restored.' }, { name: 'inheritedEventCount', description: 'exact fork-inherited prefix length supplied to unit initialization.' }], returns: 'the snapshot cut at the supplied log end (`asOfSeq` is the last supplied event\'s seq, `baseSeq - 1` for an empty tail) plus the refreshed checkpoint rows at that cut, ready for a durable write-back.', }, { - signature: 'hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, projectionMode: \'all\' | \'none\' = \'all\', ): ProjectionSnapshot', + signature: 'hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, ): ProjectionSnapshot', description: 'Restore an exact cut and install its states on the supplied prepared Session. A later publication reuses these cells; ordinary live reads and event drive advance any constructor-owned suffix exactly once.', - parameters: [{ name: 'session', description: 'exact prepared Session that owns the restored log prefix.' }, { name: 'checkpoint', description: 'persisted rows for this Session lifecycle.' }, { name: 'events', description: 'exact events at the observation cut.' }, { name: 'baseSeq', description: 'first supplied event sequence.' }, { name: 'projectionMode', description: '`none` installs every state without computing or validating client views; defaults to `all`.' }], - returns: 'the supplied cut with all client views, or empty values in `none` mode.', + parameters: [{ name: 'session', description: 'exact prepared Session that owns the restored log prefix.' }, { name: 'checkpoint', description: 'persisted rows for this Session lifecycle.' }, { name: 'events', description: 'exact events at the observation cut.' }, { name: 'baseSeq', description: 'first supplied event sequence.' }], + returns: 'all projection values at the supplied cut.', }, ], }, @@ -5217,7 +5217,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SessionObservationOptions', - declaration: 'export interface SessionObservationOptions {\n readonly signal?: AbortSignal;\n readonly projectionMode?: \'all\' | \'none\';\n readonly projectionStateKeys?: readonly Extract[];\n}', + declaration: 'export interface SessionObservationOptions {\n readonly signal?: AbortSignal;\n readonly projectionMode?: \'all\' | \'none\';\n}', }, { name: 'SessionOpenWorkspacePathRequest', diff --git a/packages/session-query/session-query/README.i18n.yaml b/packages/session-query/session-query/README.i18n.yaml index 7857b37bc6..2ab8f8e1b7 100644 --- a/packages/session-query/session-query/README.i18n.yaml +++ b/packages/session-query/session-query/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session-query/session-query/README.md -README.md: b754f342e4614137b92aa749c3b6822a23e2e4bf -README.zh.md: 9c41cc0ce97a99d8b64c206af26799527e97d102 +README.md: 9b7f7b09e91dea8dc6c853eb342c5ca434614f99 +README.zh.md: 7f098a082393cd78fb477b2cf9c07eb74d831d1b diff --git a/packages/session-query/session-query/README.md b/packages/session-query/session-query/README.md index b754f342e4..9b7f7b09e9 100644 --- a/packages/session-query/session-query/README.md +++ b/packages/session-query/session-query/README.md @@ -108,7 +108,7 @@ The decision history lives in the [unified service decision](../../../.agents/no ### Observation cache -`observeSession` builds point observations without a listing preflight. A live observation fixes its cut as the current log length and materializes `events` on first read, so header-, cursor-, or projection-only consumers never copy the log; the log only appends, so a late first read still yields exactly that prefix. The cold path stats the stored session first and consults an own bounded cache keyed by the persistence instance and the `stat` revision: an unchanged revision reuses the restored unpublished Session without re-reading the log; a changed revision, or a replaced persistence instance, reloads through the handle seam and replaces the entry. The cache holds `preparedSessionCacheSize` entries with least-recently-used eviction, entries pinned by active observation leases are never evicted, and a session that goes live mid-read retries the live path. `projectionStateKeys` detaches named host projection states at the same cut independently of `projectionMode`: a live observation clones the registry's maintained cells, and a cold observation hydrates its prepared Session's cells once so states and any views share one cut; unregistered keys are omitted and an absent registry leaves `projectionStates` undefined. With `projectionMode: 'none'`, both live and cold observations omit client view computation and validation even when host states are requested. +`observeSession` builds point observations without a listing preflight. A live observation fixes its cut as the current log length and materializes `events` on first read, so header-, cursor-, or projection-only consumers never copy the log; the log only appends, so a late first read still yields exactly that prefix. The cold path stats the stored session first and consults an own bounded cache keyed by the persistence instance and the `stat` revision: an unchanged revision reuses the restored unpublished Session without re-reading the log; a changed revision, or a replaced persistence instance, reloads through the handle seam and replaces the entry. The cache holds `preparedSessionCacheSize` entries with least-recently-used eviction, entries pinned by active observation leases are never evicted, and a session that goes live mid-read retries the live path. Default observations return every registered host state in `projectionStates` alongside client views. State values come from the registry’s detached checkpoint rows at the observation cursor. `projectionMode: 'none'` omits both views and states and performs no projection hydration or checkpointing. ### Reads and traces diff --git a/packages/session-query/session-query/README.zh.md b/packages/session-query/session-query/README.zh.md index 9c41cc0ce9..7f098a0823 100644 --- a/packages/session-query/session-query/README.zh.md +++ b/packages/session-query/session-query/README.zh.md @@ -108,7 +108,7 @@ kind: "package-reference" ### 观察缓存 -`observeSession` 不经过列表预检直接构建定点观察。实时观察以当前日志长度固定 cut,并在首次读取时才物化 `events`,因此只需要 header、cursor 或 projection 的消费者永远不会复制日志;日志只会追加,所以延后的首次读取得到的仍然正好是该前缀。冷路径先对存储会话执行 `stat`,再查询自有的有界缓存,缓存键为持久化实例加 `stat` 修订:修订未变则复用已恢复的未发布 Session,不再重读日志;修订变化或持久化实例被替换则经 handle 缝重新加载并替换条目。缓存保留 `preparedSessionCacheSize` 个条目并按最久未用淘汰,被活跃观察租约钉住的条目从不被淘汰;读取中途转为实时的会话会重试实时路径。 `projectionStateKeys` 在同一 cut 上分离出指定的 host projection state,与 `projectionMode` 无关:实时观察克隆 registry 维护中的 cell,冷观察则一次性 hydrate 其已准备 Session 的 cell,使 state 与任何 view 共享同一 cut;未注册的 key 被省略,registry 缺失时 `projectionStates` 为 undefined。使用 `projectionMode: 'none'` 时,即使请求了 host state,实时和冷观察也都不会计算或校验客户端视图。 +`observeSession` 不经过列表预检直接构建定点观察。实时观察以当前日志长度固定 cut,并在首次读取时才物化 `events`,因此只需要 header、cursor 或 projection 的消费者永远不会复制日志;日志只会追加,所以延后的首次读取得到的仍然正好是该前缀。冷路径先对存储会话执行 `stat`,再查询自有的有界缓存,缓存键为持久化实例加 `stat` 修订:修订未变则复用已恢复的未发布 Session,不再重读日志;修订变化或持久化实例被替换则经 handle 缝重新加载并替换条目。缓存保留 `preparedSessionCacheSize` 个条目并按最久未用淘汰,被活跃观察租约钉住的条目从不被淘汰;读取中途转为实时的会话会重试实时路径。 默认观察在返回客户端视图的同时,通过 `projectionStates` 返回全部已注册 host state。状态值来自注册表在观察 cursor 处生成的独立检查点行。`projectionMode: 'none'` 同时省略视图和状态,不执行投影 hydration 或检查点生成。 ### 读取与追踪 diff --git a/packages/session-query/session-query/src/observation.ts b/packages/session-query/session-query/src/observation.ts index e5492d038a..78eb767d1b 100644 --- a/packages/session-query/session-query/src/observation.ts +++ b/packages/session-query/session-query/src/observation.ts @@ -35,8 +35,8 @@ export interface SessionObservation extends Disposable { /** Exact projection baseline at {@link cursor}, when the registry is mounted. */ readonly projections?: ProjectionSnapshot /** - * Requested host projection states detached at {@link cursor}; present only - * when the registry is mounted, and contains only the registered keys. + * All registered host projection states detached at {@link cursor}; omitted + * when projections are disabled or the registry is absent. */ readonly projectionStates?: Partial /** @@ -50,14 +50,8 @@ export interface SessionObservation extends Disposable { export interface SessionObservationOptions { /** Optional cancellation while resolving a cold source. */ readonly signal?: AbortSignal - /** Whether to compute client views; requested host states are still materialized. */ + /** Whether to return projection views and states or leave projections untouched. */ readonly projectionMode?: 'all' | 'none' - /** - * Host projection states to detach from the same exact cut, independent of - * {@link projectionMode}. A cold observation hydrates its registry cells to - * serve them. - */ - readonly projectionStateKeys?: readonly Extract[] } /** @@ -103,32 +97,32 @@ export class SessionObservationReader { /** * Observe one live-preferred Session and retain a cold preparation until disposal. * @param sessionId - logical Session identity. - * @param options - cancellation, client views, and requested host states for this read. + * @param options - cancellation and projection views and states for this read. * @returns one exact immutable observation. */ async read( sessionId: SessionId, options: SessionObservationOptions = {}, ): Promise { - const { signal, projectionMode = 'all', projectionStateKeys } = options + const { signal, projectionMode = 'all' } = options for (;;) { throwIfObservationAborted(signal) const live = this.ctx.sessions.get(sessionId) - if (live !== undefined) return this.live(live, projectionMode, projectionStateKeys) + if (live !== undefined) return this.live(live, projectionMode) const persistence = this.ctx.get('sessionPersistence') if (persistence === undefined) throw notFound(sessionId) const snapshot = await this.statSource(persistence, sessionId, signal) const attachedDuringStat = this.ctx.sessions.get(sessionId) if (attachedDuringStat !== undefined) { - return this.live(attachedDuringStat, projectionMode, projectionStateKeys) + return this.live(attachedDuringStat, projectionMode) } let entry = this.cachedEntry(persistence, sessionId, snapshot.revision) if (entry === undefined) { const loaded = await this.loadSource(persistence, sessionId, signal) throwIfObservationAborted(signal) const attached = this.ctx.sessions.get(sessionId) - if (attached !== undefined) return this.live(attached, projectionMode, projectionStateKeys) + if (attached !== undefined) return this.live(attached, projectionMode) // The handle marks persisted events as adoptable; synthetic closers // are owned by this read, so the combined seed needs no copy. const seed = loaded.events @@ -164,12 +158,9 @@ export class SessionObservationReader { let projections: ProjectionSnapshot | undefined let projectionStates: Partial | undefined try { - if (projectionMode === 'all' || projectionStateKeys !== undefined) { - // Hydration installs every unit's cell on the prepared Session, so a - // requested host state reads the same cut the views were taken from. - const snapshot = this.preparedProjections(entry, projectionMode) - projections = projectionMode === 'all' ? snapshot : undefined - projectionStates = this.projectionStates(entry.session, projectionStateKeys) + if (projectionMode === 'all') { + projections = this.preparedProjections(entry) + projectionStates = this.projectionStates(entry.session) } } catch (error: unknown) { throw new SessionQueryError( @@ -297,7 +288,6 @@ export class SessionObservationReader { private live( session: Session, projectionMode: NonNullable, - projectionStateKeys: SessionObservationOptions['projectionStateKeys'], ): SessionObservation { // The cut is the log length now. The log only appends, so the prefix // below `seq` is the same array whenever a consumer first reads `events`. @@ -306,7 +296,7 @@ export class SessionObservationReader { const projections = projectionMode === 'none' ? undefined : this.ctx.get('sessionProjections')?.snapshot(session) - const projectionStates = this.projectionStates(session, projectionStateKeys) + const projectionStates = projectionMode === 'none' ? undefined : this.projectionStates(session) const lease = (): SessionObservation => { let disposed = false return { @@ -330,36 +320,20 @@ export class SessionObservationReader { return lease() } - private preparedProjections( - entry: PreparedEntry, - projectionMode: NonNullable, - ): ProjectionSnapshot | undefined { + private preparedProjections(entry: PreparedEntry): ProjectionSnapshot | undefined { const registry = this.ctx.get('sessionProjections') if (registry === undefined) return undefined const cache = this.ctx.get('sessionProjectionCache') return cache === undefined - ? registry.hydrate(entry.session, {}, entry.events, SessionLogOffset(0), projectionMode) - : cache.hydratePrepared(entry.session, entry.events, projectionMode) + ? registry.hydrate(entry.session, {}, entry.events, SessionLogOffset(0)) + : cache.hydratePrepared(entry.session, entry.events) } - /** - * Detach the requested host states from the registry cells already at this - * Session's cut. Registry cells are live references that later drives - * advance, so each state is cloned into the immutable observation. - */ - private projectionStates( - session: Session, - keys: SessionObservationOptions['projectionStateKeys'], - ): Partial | undefined { - if (keys === undefined) return undefined + /** Checkpoint rows already own detached state values at the Session cut. */ + private projectionStates(session: Session): Partial | undefined { const registry = this.ctx.get('sessionProjections') if (registry === undefined) return undefined - const states: Record = {} - for (const key of keys) { - const state = registry.stateOf(session, key) - if (state !== undefined) states[key] = structuredClone(state) - } - return states + return Object.fromEntries(Object.entries(registry.checkpoint(session)).map(([key, row]) => [key, row.val])) } } diff --git a/packages/session-query/session-query/tests/observation.spec.ts b/packages/session-query/session-query/tests/observation.spec.ts index 101669ebed..6521269c40 100644 --- a/packages/session-query/session-query/tests/observation.spec.ts +++ b/packages/session-query/session-query/tests/observation.spec.ts @@ -21,6 +21,12 @@ import { titleProjectionDefinition } from '@deepseek-ai/dsh-session-title' import { describe, expect, it, vi } from 'vitest' import { SessionObservationReader } from '../src/observation.ts' +declare module '@deepseek-ai/dsh-session-projection/types' { + interface SessionProjectionStateMap { + 'observation-test/titles': (string | null)[] + } +} + function header(id: string): SessionHeader { return { version: SESSION_FORMAT_VERSION, id: SessionId(id), createdAt: 1, isSeeded: false, cwd: '/workspace' } } @@ -762,7 +768,7 @@ describe('SessionObservationReader cold projections', () => { await ctx.fiber.dispose() }) - it.each(['all', 'none'] as const)('hydrates prepared projections through a mounted projection cache in %s mode', async (projectionMode) => { + it('hydrates prepared projections through a mounted projection cache', async () => { const ctx = await readerContext() await ctx.plugin(SessionProjectionRegistry) const meta = header('cold-cache') @@ -772,14 +778,10 @@ describe('SessionObservationReader cold projections', () => { const hydratePrepared = vi.fn().mockReturnValue(snapshot) ctx.provide('sessionProjectionCache', { hydratePrepared } as never) - using observed = await new SessionObservationReader(ctx).read(meta.id, { - projectionMode, - projectionStateKeys: ['title'], - }) + using observed = await new SessionObservationReader(ctx).read(meta.id) - expect(observed.projections).toBe(projectionMode === 'all' ? snapshot : undefined) + expect(observed.projections).toBe(snapshot) expect(hydratePrepared).toHaveBeenCalledOnce() - expect(hydratePrepared.mock.calls[0]?.[2]).toBe(projectionMode) await ctx.fiber.dispose() }) @@ -802,7 +804,7 @@ describe('SessionObservationReader cold projections', () => { }) describe('SessionObservationReader host projection states', () => { - it('detaches a live registry state at the observed cut without computing views', async () => { + it('returns detached live states and views at the observed cut', async () => { const ctx = await readerContext() await ctx.plugin(SessionProjectionRegistry) ctx.sessionProjections.register(titleProjectionDefinition) @@ -810,17 +812,17 @@ describe('SessionObservationReader host projection states', () => { session.append('session/title', { title: 'Live title', messageSeqs: [], source: { kind: 'user' } }) const reader = new SessionObservationReader(ctx) - using observed = await reader.read(session.id, { projectionMode: 'none', projectionStateKeys: ['title'] }) + using observed = await reader.read(session.id) session.append('session/title', { title: 'Later title', messageSeqs: [], source: { kind: 'user' } }) - using later = await reader.read(session.id, { projectionMode: 'none', projectionStateKeys: ['title'] }) + using later = await reader.read(session.id) - expect(observed.projections).toBeUndefined() + expect(observed.projections?.values.title).toBeDefined() expect(observed.projectionStates).toEqual({ title: 'Live title' }) expect(later.projectionStates).toEqual({ title: 'Later title' }) await ctx.fiber.dispose() }) - it('hydrates a prepared Session once and serves the requested state from its cells', async () => { + it('returns cold states and views from the prepared Session', async () => { const ctx = await readerContext() await ctx.plugin(SessionProjectionRegistry) ctx.sessionProjections.register(titleProjectionDefinition) @@ -829,53 +831,60 @@ describe('SessionObservationReader host projection states', () => { ctx.provide('sessionPersistence', stubPersistence(store, { stat: 0, open: 0, read: 0 })) const hydrate = vi.spyOn(ctx.sessionProjections, 'hydrate') - using observed = await new SessionObservationReader(ctx).read(meta.id, { - projectionMode: 'none', - projectionStateKeys: ['title'], - }) + using observed = await new SessionObservationReader(ctx).read(meta.id) expect(observed.source).toBe('prepared') - expect(observed.projections).toBeUndefined() + expect(observed.projections?.values.title).toBeDefined() expect(observed.projectionStates).toEqual({ title: 'Cold title' }) expect(hydrate).toHaveBeenCalledOnce() await ctx.fiber.dispose() }) - it('omits wire views on fresh and reused cold host-state reads', async () => { + it.each(['live', 'cold'] as const)('returns all detached states on repeated %s reads and omits them in none mode', async (source) => { const ctx = await readerContext() try { await ctx.plugin(SessionProjectionRegistry) - const view = vi.fn(titleProjectionDefinition.wire.view) + ctx.sessionProjections.register(titleProjectionDefinition) ctx.sessionProjections.register({ - ...titleProjectionDefinition, - wire: { ...titleProjectionDefinition.wire, view }, + key: 'observation-test/titles', + stateVersion: 1, + stateSchema: titleProjectionDefinition.stateSchema.array(), + init: () => [] as (string | null)[], + apply: (state, event) => event.type === 'session/title' + ? [...state, event.data.title] : state, }) - const meta = header('cold-host-only') - const store = new Map([[meta.id, { header: meta, events: [titleEvent(0, 'Cold title')], revision: 'r1' }]]) - ctx.provide('sessionPersistence', stubPersistence(store, { stat: 0, open: 0, read: 0 })) - const reader = new SessionObservationReader(ctx) - for (let read = 0; read < 2; read++) { - using observed = await reader.read(meta.id, { projectionMode: 'none', projectionStateKeys: ['title'] }) - expect(observed.projections).toBeUndefined() - expect(observed.projectionStates).toEqual({ title: 'Cold title' }) - expect(view).not.toHaveBeenCalled() + const meta = header(`all-states-${source}`) + if (source === 'live') { + const session = ctx.sessions.create(meta.id) + session.append('session/title', { title: 'First', messageSeqs: [], source: { kind: 'user' } }) + } else { + const store = new Map([[meta.id, { header: meta, events: [titleEvent(0, 'First')], revision: 'r1' }]]) + ctx.provide('sessionPersistence', stubPersistence(store, { stat: 0, open: 0, read: 0 })) } - using full = await reader.read(meta.id) - expect(full.projections?.values).toEqual({ title: 'Cold title' }) - expect(view).toHaveBeenCalledOnce() + const reader = new SessionObservationReader(ctx) + using first = await reader.read(meta.id) + expect(first.projectionStates).toEqual({ title: 'First', 'observation-test/titles': ['First'] }) + first.projectionStates?.['observation-test/titles']?.push('caller edit') + using second = await reader.read(meta.id) + expect(second.projectionStates).toEqual({ title: 'First', 'observation-test/titles': ['First'] }) + expect(second.projections?.values).toEqual({ title: 'First' }) + const checkpoint = vi.spyOn(ctx.sessionProjections, 'checkpoint') + const hydrate = vi.spyOn(ctx.sessionProjections, 'hydrate') + using none = await reader.read(meta.id, { projectionMode: 'none' }) + expect(none.projections).toBeUndefined() + expect(none.projectionStates).toBeUndefined() + expect(checkpoint).not.toHaveBeenCalled() + expect(hydrate).not.toHaveBeenCalled() } finally { await ctx.fiber.dispose() } }) - it('omits unregistered keys and the whole map when no registry is mounted', async () => { + it('returns an empty state map for an empty registry and omits it without a registry', async () => { const withRegistry = await readerContext() await withRegistry.plugin(SessionProjectionRegistry) const registered = withRegistry.sessions.create(SessionId('unregistered-key')) - using partial = await new SessionObservationReader(withRegistry).read(registered.id, { - projectionMode: 'none', - projectionStateKeys: ['title'], - }) + using partial = await new SessionObservationReader(withRegistry).read(registered.id) expect(partial.projectionStates).toEqual({}) await withRegistry.fiber.dispose() @@ -883,10 +892,7 @@ describe('SessionObservationReader host projection states', () => { const meta = header('no-registry') const store = new Map([[meta.id, { header: meta, events: [titleEvent(0, 'Cold title')], revision: 'r1' }]]) withoutRegistry.provide('sessionPersistence', stubPersistence(store, { stat: 0, open: 0, read: 0 })) - using absent = await new SessionObservationReader(withoutRegistry).read(meta.id, { - projectionMode: 'none', - projectionStateKeys: ['title'], - }) + using absent = await new SessionObservationReader(withoutRegistry).read(meta.id) expect(absent.projectionStates).toBeUndefined() await withoutRegistry.fiber.dispose() }) diff --git a/packages/session/session-projection-cache/README.i18n.yaml b/packages/session/session-projection-cache/README.i18n.yaml index 92b3540cd1..e58dce3a5a 100644 --- a/packages/session/session-projection-cache/README.i18n.yaml +++ b/packages/session/session-projection-cache/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session/session-projection-cache/README.md -README.md: 9ef25b737252d051b16b375442b8edefbd92a939 -README.zh.md: 650d6e4fe7fcd3365c99975c72e140d26f9592bf +README.md: 4ac2ddabe878202a3b324d5b2aaecbea8da99e5f +README.zh.md: ffed860eb59621d82672c25989d603b79634a498 diff --git a/packages/session/session-projection-cache/README.md b/packages/session/session-projection-cache/README.md index 9ef25b7372..4ac2ddabe8 100644 --- a/packages/session/session-projection-cache/README.md +++ b/packages/session/session-projection-cache/README.md @@ -52,8 +52,6 @@ The cache opens its domain through the storage stack, so base mounts `storage`, The plugin injects `storageDomain`, `sessionProjections`, and `sessions`. The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-session-projection-cache) is the exhaustive source for every accepted field and its JSDoc. -`hydratePrepared` restores every state from the checkpoint and event tail. Its `projectionMode` defaults to `all`; `none` suppresses client view computation and validation, including when a malformed checkpoint requires a full refold. - ### How checkpoints are written Three mandatory points always write: session creation persists the seed-derived cut, `turn/end` persists the value that listing reads want, and session disposal persists the final live cut. Between them, the configured count and interval throttles write as events accumulate. Every write atomically replaces the session's complete record through the domain write chain; a failure logs a warning and keeps the cache stale, and the next write self-heals. @@ -124,7 +122,6 @@ None; the cache never assembles or sends provider requests. - These limits define where the cache needs operational care. They are current package constraints, not a task backlog. - **No eviction or retention surface** — records accumulate per session; pruning stored checkpoints is out-of-band maintenance, same stance as session persistence itself. diff --git a/packages/session/session-projection-cache/README.zh.md b/packages/session/session-projection-cache/README.zh.md index 650d6e4fe7..ffed860eb5 100644 --- a/packages/session/session-projection-cache/README.zh.md +++ b/packages/session/session-projection-cache/README.zh.md @@ -52,8 +52,6 @@ kind: "package-reference" 本插件注入 `storageDomain`、`sessionProjections` 与 `sessions`。生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-session-projection-cache)是每个受支持字段及其 JSDoc 的穷尽式真源。 -`hydratePrepared` 从检查点与事件尾部恢复所有状态。其 `projectionMode` 默认为 `all`;`none` 会跳过客户端视图的计算与校验,即使检查点损坏、需要重新折叠完整日志也如此。 - ### 检查点如何写入 三个必写点总是写入:会话创建保存由种子派生的切面,`turn/end` 保存列表读取所需的轮次终值,会话释放保存最终实时切面。其间,配置的条数与间隔节流随事件累积写入。每次写入通过领域写入链以原子方式替换该会话的完整记录;失败会记录警告并让缓存保持陈旧,后续写入会自行修复。 @@ -124,7 +122,6 @@ kind: "package-reference" - 这些限制说明缓存何时需要运维注意。它们是当前包约束,不是任务积压。 - **无淘汰或保留接口**——记录按会话持续累积;清理已存储检查点属于带外维护,与会话持久化采用相同策略。 diff --git a/packages/session/session-projection-cache/src/index.ts b/packages/session/session-projection-cache/src/index.ts index 079cc388f7..639dfcc978 100644 --- a/packages/session/session-projection-cache/src/index.ts +++ b/packages/session/session-projection-cache/src/index.ts @@ -207,20 +207,18 @@ export class SessionProjectionCache extends Service { * because the logical observation may contain recovery events not yet durable. * @param session - exact unpublished Session retained by persistence. * @param events - exact logical event prefix represented by the observation. - * @param projectionMode - `none` hydrates every state without computing or validating client views; defaults to `all`. - * @returns the event cut with all client views, or empty values in `none` mode. + * @returns all projection values at the event cut. */ hydratePrepared( session: Session, events: readonly SessionEvent[], - projectionMode: 'all' | 'none' = 'all', ): ProjectionSnapshot { const record = this.recordFor( session.id, identityOf(session.header, session.inheritedEventCount), ) if (record === undefined) { - return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0), projectionMode) + return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0)) } try { return this.ctx.sessionProjections.hydrate( @@ -228,12 +226,11 @@ export class SessionProjectionCache extends Service { record.rows, events, SessionLogOffset(0), - projectionMode, ) } catch { // Cached rows are disposable derived data. Retry from the exact log so a // stale schema cannot make a valid Session unreadable. - return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0), projectionMode) + return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0)) } } diff --git a/packages/session/session-projection-cache/tests/cache.spec.ts b/packages/session/session-projection-cache/tests/cache.spec.ts index 9bcd2a5d2c..b38f49eec1 100644 --- a/packages/session/session-projection-cache/tests/cache.spec.ts +++ b/packages/session/session-projection-cache/tests/cache.spec.ts @@ -651,41 +651,6 @@ describe('SessionProjectionCache cold-read seeding', () => { }) }) - it.each(['matching', 'malformed', 'absent'])('hydrates host states without client views from a %s checkpoint', async (record) => { - const root = await mkdtemp(join(tmpdir(), 'dsh-projcache-')) - roots.push(root) - const id = SessionId('selected-views') - if (record !== 'absent') { - await seedRecord(root, id, { - 'cache-test/marks': { ver: 1, seq: SessionSeq(1), val: { marks: record === 'matching' ? ['cached'] : 'malformed' } }, - }) - } - const { cache, ctx } = await harness({ root }) - const view = vi.fn(secondaryMarksUnit.wire.view) - const schema = secondaryMarksUnit.wire.viewSchema.clone() - const parseView = vi.spyOn(schema, 'parse') - ctx.sessionProjections.register({ - ...secondaryMarksUnit, - wire: { viewSchema: schema, view }, - }) - const events = storedLog([['fresh']]) - const session = Session.create(id, events, headerOf(id)) - const marks = { marks: [record === 'matching' ? 'cached' : 'fresh'] } - - for (let read = 0; read < 2; read++) { - expect(cache.hydratePrepared(session, events, 'none')).toEqual({ asOfSeq: 2, values: {} }) - expect(ctx.sessionProjections.stateOf(session, 'cache-test/marks')).toEqual(marks) - expect(ctx.sessionProjections.stateOf(session, 'cache-test/secondary-marks')).toEqual({ marks: ['fresh'] }) - expect(view).not.toHaveBeenCalled() - expect(parseView).not.toHaveBeenCalled() - } - expect(cache.hydratePrepared(session, events).values).toEqual({ - 'cache-test/marks': marks, - 'cache-test/secondary-marks': { marks: ['fresh'] }, - }) - expect(view).toHaveBeenCalledOnce() - }) - it('coldSnapshot traverses the full log but applies only the events after each cached watermark', async () => { const root = await mkdtemp(join(tmpdir(), 'dsh-projcache-')) roots.push(root) diff --git a/packages/session/session-projection/README.i18n.yaml b/packages/session/session-projection/README.i18n.yaml index b92fb0f575..b1f80c30a6 100644 --- a/packages/session/session-projection/README.i18n.yaml +++ b/packages/session/session-projection/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session/session-projection/README.md -README.md: 9bd08d119cf1961a47595bec29efabdc8d7fb276 -README.zh.md: e7982626f6e0c734ef426ec1a2851562ca01525a +README.md: ced9837dbf69a0fc168da7f574342e803d83bfea +README.zh.md: 2af7d33d617414baac24575ad425db0db8300cd8 diff --git a/packages/session/session-projection/README.md b/packages/session/session-projection/README.md index 9bd08d119c..ced9837dbf 100644 --- a/packages/session/session-projection/README.md +++ b/packages/session/session-projection/README.md @@ -126,7 +126,6 @@ None; projections never assemble or send provider requests. - These limits define where the projection registry needs care at scale. They are current package constraints, not a task backlog. - **Every tail page carries every client-visible key** — there is no per-key opt-out or lazy-key request shape yet; acceptable while values are UI-scale whole states, revisit if a domain's value grows large. diff --git a/packages/session/session-projection/README.zh.md b/packages/session/session-projection/README.zh.md index e7982626f6..2af7d33d61 100644 --- a/packages/session/session-projection/README.zh.md +++ b/packages/session/session-projection/README.zh.md @@ -126,7 +126,6 @@ const { asOfSeq, values } = ctx.sessionProjections.snapshot(session) - 这些限制说明投影注册表在大规模下何时需要特别处理。它们是当前包约束,不是任务积压。 - **每个尾页携带每个 client-visible key**——尚无逐 key 的 opt-out 或惰性 key 请求形状;在值都是 UI 量级的全量状态时可以接受,若某领域的值变大再重议。 diff --git a/packages/session/session-projection/src/index.ts b/packages/session/session-projection/src/index.ts index d7cff2e765..d29c5bb021 100644 --- a/packages/session/session-projection/src/index.ts +++ b/packages/session/session-projection/src/index.ts @@ -488,7 +488,6 @@ export class SessionProjectionRegistry extends Service { * @param baseSeq - the seq `events` starts at (its first event's seq when non-empty). * @param header - immutable metadata for the Session being restored. * @param inheritedEventCount - exact fork-inherited prefix length supplied to unit initialization. - * @param projectionMode - `none` restores every state without computing or validating client views; defaults to `all`. * @returns the snapshot cut at the supplied log end (`asOfSeq` is the last * supplied event's seq, `baseSeq - 1` for an empty tail) plus the * refreshed checkpoint rows at that cut, ready for a durable write-back. @@ -499,7 +498,6 @@ export class SessionProjectionRegistry extends Service { baseSeq: SessionLogOffset, header: SessionHeader, inheritedEventCount: SessionLogOffset, - projectionMode: 'all' | 'none' = 'all', ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint } { const endSeq: SessionSeqCursor = events.at(-1)?.seq ?? cursorBefore(baseSeq) @@ -532,9 +530,7 @@ export class SessionProjectionRegistry extends Service { } state = def.apply(state, event) } - if (projectionMode === 'all' && def.wire !== undefined) { - values[def.key] = def.wire.viewSchema.parse(def.wire.view(state)) - } + if (def.wire !== undefined) values[def.key] = def.wire.viewSchema.parse(def.wire.view(state)) refreshed[def.key] = { ver: def.stateVersion, seq: endSeq, val: state } } return { @@ -551,15 +547,13 @@ export class SessionProjectionRegistry extends Service { * @param checkpoint - persisted rows for this Session lifecycle. * @param events - exact events at the observation cut. * @param baseSeq - first supplied event sequence. - * @param projectionMode - `none` installs every state without computing or validating client views; defaults to `all`. - * @returns the supplied cut with all client views, or empty values in `none` mode. + * @returns all projection values at the supplied cut. */ hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, - projectionMode: 'all' | 'none' = 'all', ): ProjectionSnapshot { const endSeq: SessionSeqCursor = events.at(-1)?.seq ?? cursorBefore(baseSeq) let complete = true @@ -572,12 +566,10 @@ export class SessionProjectionRegistry extends Service { } if (complete) { const values: Record = {} - if (projectionMode === 'all') { - for (const registration of this.registrations.values()) { - if (registration.def.wire === undefined) continue - const current = registration.cells.get(session) as UnitCell - values[registration.def.key] = this.viewCell(registration, current) - } + for (const registration of this.registrations.values()) { + if (registration.def.wire === undefined) continue + const current = registration.cells.get(session) as UnitCell + values[registration.def.key] = this.viewCell(registration, current) } return { asOfSeq: endSeq, values } } @@ -587,7 +579,6 @@ export class SessionProjectionRegistry extends Service { baseSeq, session.header, session.inheritedEventCount, - projectionMode, ) for (const registration of this.registrations.values()) { const row = restored.checkpoint[registration.def.key] From 532e712c7beab9cbe2d16a76c89e9a5a8128d67a Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Tue, 8 Sep 2026 15:57:26 +0800 Subject: [PATCH 20/58] refactor(subagent): expose catalog through existing projection views --- ...01-parent-owned-subagent-catalog.i18n.yaml | 4 +- ...026-09-01-parent-owned-subagent-catalog.md | 6 +- ...-09-01-parent-owned-subagent-catalog.zh.md | 6 +- docs/persistence-catalog.i18n.yaml | 4 +- docs/persistence-catalog.md | 2 +- docs/persistence-catalog.zh.md | 2 +- docs/subsystems/subagent.i18n.yaml | 4 +- docs/subsystems/subagent.md | 2 + docs/subsystems/subagent.zh.md | 2 + .../extensions/tool-cordis/src/api-catalog.ts | 2 +- .../session-query/README.i18n.yaml | 4 +- .../session-query/session-query/README.md | 2 +- .../session-query/session-query/README.zh.md | 2 +- .../session-query/src/observation.ts | 33 +----- .../session-query/tests/observation.spec.ts | 111 ------------------ packages/subagent/subagent/README.i18n.yaml | 4 +- packages/subagent/subagent/README.md | 2 +- packages/subagent/subagent/README.zh.md | 2 +- packages/subagent/subagent/src/catalog.ts | 30 +++-- packages/subagent/subagent/src/client.ts | 2 +- .../subagent/subagent/src/projection-types.ts | 15 ++- .../subagent/subagent/tests/catalog.spec.ts | 48 +++++++- 22 files changed, 104 insertions(+), 185 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml index 2a82fc2336..284744c942 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md -2026-09-01-parent-owned-subagent-catalog.md: f424943c523c1143884116fd5aeccdf6e20423f2 -2026-09-01-parent-owned-subagent-catalog.zh.md: b4c524ab81e4c2df9a666d0119e1de9c8b771e8b +2026-09-01-parent-owned-subagent-catalog.md: d4b6a20dfbceb30e3f7e5dc827895a25f823b59a +2026-09-01-parent-owned-subagent-catalog.zh.md: d4a394aee3ee1b9f86aff5872657c37acca93852 diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md index f424943c52..d4b6a20dfb 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md @@ -18,7 +18,7 @@ Creation publishes only successful facts. A one-shot run appends the catalog eve The child header and `subagent/descriptor` remain authoritative for recovery and composition. An Activation and the exact parent relationship remain authoritative for authorization and delivery. Mode and label are snapshotted once and the same detached values reach the parent catalog fact and child descriptor. -The registered host-only `subagentCatalog` projection materializes the parent facts. It stores facts in a persistent stack of 64-entry chunks, so an append copies at most the head chunk in bounded O(1) work. Materialization visits chunks from oldest to newest and preserves parent catalog event order in O(D) time for D facts. Concurrent creation is ordered by successful catalog append, independent of child timestamps and ids. A projection checkpoint clones the state once in O(D); projection-cache writes remain asynchronous and use the existing mandatory creation, turn-end, and disposal points. +The registered `subagentCatalog` projection materializes the parent facts. It stores facts in a persistent stack of 64-entry chunks, so an append copies at most the head chunk in bounded O(1) work. Materialization visits chunks from oldest to newest and preserves parent catalog event order in O(D) time for D facts. Concurrent creation is ordered by successful catalog append, independent of child timestamps and ids. A projection checkpoint clones the state once in O(D); projection-cache writes remain asynchronous and use the existing mandatory creation, turn-end, and disposal points. Fork isolation uses the exact `Session.inheritedEventCount` supplied to projection initialization. The fold ignores `subagent/catalog` events below that offset. The state stores the inherited offset but not each event seq because acceptance is decided during folding. @@ -30,7 +30,7 @@ Snapshot normalizers zero `childCreatedAt` because it originates from the proces **A node-per-fact linked list.** It provides O(1) append and O(D) read, but persisted projection checkpoints form JSON nested D levels deep. Sixty-four-entry chunks preserve the asymptotic costs while reducing nesting. -**A client-visible projection.** Publishing the complete catalog after every creation would turn constant-time folding into O(D²) cumulative materialization. The Session Controller already owns the observation needed for a cold read. +**Separate host-state observation output.** Returning internal projection states duplicates the existing observation result mechanism and copies states unrelated to child discovery. A catalog view supplies the direct-child list through the existing typed projection map. **A durable SQLite child index.** An index would create another write path, reconciliation protocol, schema, and corruption surface for a fact already ordered in the parent Session log. @@ -38,6 +38,6 @@ Snapshot normalizers zero `childCreatedAt` because it originates from the proces ## Consequences -Default Session observations expose every registered host state in `projectionStates` alongside client views. The registry’s existing `checkpoint` operation supplies detached state values; no key-selection parameter or additional hydration mode is required. `projectionMode: 'none'` retains its projection-free behavior. Direct-child and descendant listing still use the Session corpus and child identity projection. +Session observations and client snapshots expose the direct-child list through `projections.values.subagentCatalog`. The projection change feed publishes a complete list when catalog state changes. Each view costs O(D), so D creations can incur O(D²) cumulative view work; this follows the existing projection mechanism. Direct-child and descendant listing still use the Session corpus and child identity projection. Backends that do not know the required event refuse the log under the existing Session event mechanism. Pre-release format policy requires no fallback scan for old logs. diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md index b4c524ab81..d4a394aee3 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md @@ -18,7 +18,7 @@ parent Session 的 required `subagent/catalog` 事件是直接 child discovery child header 与 `subagent/descriptor` 继续拥有恢复与 composition 权威。Activation 与精确 parent 关系继续拥有授权与投递权威。mode 与 label 只快照一次,同一份分离值写入 parent catalog fact 与 child descriptor。 -注册的 host-only `subagentCatalog` projection 物化 parent fact。它以每块 64 项的持久 stack 保存事实,因此 append 最多复制 head chunk,以有界 O(1) 工作完成。materialization 从旧到新访问 chunk,对 D 条事实以 O(D) 时间保留父目录事件顺序。并发创建按目录成功追加的顺序排列,与 child 时间戳和 id 无关。projection checkpoint 以 O(D) 克隆 state;projection-cache 继续异步写入,并使用既有创建、turn-end 与 disposal 强制点。 +注册的 `subagentCatalog` projection 物化 parent fact。它以每块 64 项的持久 stack 保存事实,因此 append 最多复制 head chunk,以有界 O(1) 工作完成。materialization 从旧到新访问 chunk,对 D 条事实以 O(D) 时间保留父目录事件顺序。并发创建按目录成功追加的顺序排列,与 child 时间戳和 id 无关。projection checkpoint 以 O(D) 克隆 state;projection-cache 继续异步写入,并使用既有创建、turn-end 与 disposal 强制点。 fork 隔离使用 projection 初始化时提供的精确 `Session.inheritedEventCount`。fold 忽略该 offset 之前的 `subagent/catalog` 事件。state 保存 inherited offset,但不保存每条 event seq,因为接受判定已在 fold 时完成。 @@ -30,7 +30,7 @@ snapshot normalizer 会把 `childCreatedAt` 归零,因为它来自 process clo **每 fact 一个 node 的 linked list。** 它提供 O(1) append 与 O(D) read,但持久 projection checkpoint 会形成 D 层 JSON 嵌套。每块 64 项保留渐进复杂度,同时降低嵌套深度。 -**client-visible projection。** 每次创建后发布完整 catalog 会把常数时间 fold 变成 O(D²) 累积 materialization。Session Controller 已经拥有 cold read 所需的 observation。 +**独立的 host state 观察输出。** 返回内部 projection state 会重复已有观察结果机制,并复制与子级发现无关的状态。目录视图通过既有的类型化 projection map 提供直接子级列表。 **持久 SQLite child index。** index 会为 parent Session 日志中已有顺序的 fact 增加另一套写路径、reconciliation protocol、schema 与 corruption surface。 @@ -38,6 +38,6 @@ snapshot normalizer 会把 `childCreatedAt` 归零,因为它来自 process clo ## 后果 -默认 Session 观察在返回客户端视图的同时,通过 `projectionStates` 暴露全部已注册 host state。注册表现有的 `checkpoint` 操作提供独立状态值,无需 key 选择参数或额外 hydration 模式。`projectionMode: 'none'` 保留不执行投影的行为。直接子级和后代列表仍使用 Session 语料库与子级身份 projection。 +Session 观察和客户端快照通过 `projections.values.subagentCatalog` 暴露直接子级列表。目录状态变化时,projection 变更通知发布完整列表。每次视图计算成本为 O(D),因此 D 次创建的累计视图工作量可能为 O(D²);这沿用既有 projection 机制。直接子级和后代列表仍使用 Session 语料库与子级身份 projection。 不认识该 required event 的 backend 会按既有 Session event 机制拒绝日志。pre-release format policy 不要求为旧日志保留 fallback scan。 diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index ab3b610505..dc6e192b50 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-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/persistence-catalog.md -persistence-catalog.md: 1859ca37bec3ba06033a209103d4471eaa3fa3c7 -persistence-catalog.zh.md: d967b46c9e60df10a60d4599640b58f5e8b97bf5 +persistence-catalog.md: d9a9449a4369988d4031d921c4124d8cb74f8791 +persistence-catalog.zh.md: 538b20e430c8d061e7f443d9a412443e81851415 diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 1859ca37be..d9a9449a43 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -773,7 +773,7 @@ Source: [`packages/core/session/src/types.ts:287`](../packages/core/session/src/ 'subagent/catalog': SubagentCatalogEvent ``` -Source: [`packages/subagent/subagent/src/catalog.ts:37`](../packages/subagent/subagent/src/catalog.ts) +Source: [`packages/subagent/subagent/src/catalog.ts:38`](../packages/subagent/subagent/src/catalog.ts) diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index d967b46c9e..538b20e430 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -775,7 +775,7 @@ export type SessionEvent = { 'subagent/catalog': SubagentCatalogEvent ``` -来源:[`packages/subagent/subagent/src/catalog.ts:37`](../packages/subagent/subagent/src/catalog.ts) +来源:[`packages/subagent/subagent/src/catalog.ts:38`](../packages/subagent/subagent/src/catalog.ts) diff --git a/docs/subsystems/subagent.i18n.yaml b/docs/subsystems/subagent.i18n.yaml index f64a9d5c91..9c6a2880e4 100644 --- a/docs/subsystems/subagent.i18n.yaml +++ b/docs/subsystems/subagent.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/subagent.md -subagent.md: 55e8f8af23cb71c1103c017c09e9dfd2ef365b75 -subagent.zh.md: 71a71b33771ff83ccaa6802358b8534c11930181 +subagent.md: 24a10bb4a47c58e063797509abc4863f6cfe93a7 +subagent.zh.md: 66a9ac5eab7ecf1a93f60fad29f89da135f68c4b diff --git a/docs/subsystems/subagent.md b/docs/subsystems/subagent.md index 55e8f8af23..24a10bb4a4 100644 --- a/docs/subsystems/subagent.md +++ b/docs/subsystems/subagent.md @@ -8,6 +8,8 @@ Service Definition: [dsh-subagent](../../packages/subagent/subagent) (`ctx.subag Sources: [`packages/subagent/subagent/src/types.ts`](../../packages/subagent/subagent/src/types.ts), [`packages/subagent/subagent/src/index.ts`](../../packages/subagent/subagent/src/index.ts), and [`packages/subagent/subagent/src/continuation.ts`](../../packages/subagent/subagent/src/continuation.ts) +The `subagentCatalog` projection exposes `SubagentCatalogEntry[]` in parent event order through Session observations and client snapshots. Each entry contains the child id, creation time, mode, and mode-dependent label; fork-inherited catalog facts are excluded. [The subagent package](../../packages/subagent/subagent/README.md) owns catalog creation and persistence semantics. + ## Two kinds of capability, discovered two ways A provider advertises its **start-time** features on a static descriptor the service checks BEFORE a one-shot run exists; a request that needs one the provider lacks is rejected loud (`SubagentError('UNSUPPORTED_CAPABILITY')`), never accepted-then-ignored. Those flags describe only the one-shot [`start()`](#the-provider-contract-subagentprovider) path, where the provider composes the child. **Continuable** children are composed by the continuation manager itself, so they are gated by one optional method whose presence IS the capability, with TS narrowing as the discovery mechanism: [`SubagentProvider.prepareContinuable`](#the-provider-contract-subagentprovider). diff --git a/docs/subsystems/subagent.zh.md b/docs/subsystems/subagent.zh.md index 71a71b3377..66a9ac5eab 100644 --- a/docs/subsystems/subagent.zh.md +++ b/docs/subsystems/subagent.zh.md @@ -8,6 +8,8 @@ Service Definition:[dsh-subagent](../../packages/subagent/subagent)(`ctx.sub 源码:[`packages/subagent/subagent/src/types.ts`](../../packages/subagent/subagent/src/types.ts)、[`packages/subagent/subagent/src/index.ts`](../../packages/subagent/subagent/src/index.ts)和 [`packages/subagent/subagent/src/continuation.ts`](../../packages/subagent/subagent/src/continuation.ts) +`subagentCatalog` projection 通过 Session 观察和客户端快照暴露按父会话事件排序的 `SubagentCatalogEntry[]`。每个条目包含子级 id、创建时间、模式和依模式确定的标签;fork 继承的目录事实不在其中。[subagent 包](../../packages/subagent/subagent/README.zh.md) 定义目录创建和持久化语义。 + ## 两类能力,两种发现方式 提供方通过一个静态描述符公布其**启动时**功能,服务会在单次 run 存在之前即行检查;如果请求依赖提供方不具备的功能,会被明确拒绝(`SubagentError('UNSUPPORTED_CAPABILITY')`),绝不会被接受后静默忽略。这些 flag 仅描述单次 [`start()`](#the-provider-contract-subagentprovider) 路径,即由提供方组合子 agent 的路径。**可继续**子 agent 由继续执行管理器自行组合,因此它们由唯一一个可选方法把关,方法存在即为能力,并以 TypeScript 的类型收窄作为发现机制:[`SubagentProvider.prepareContinuable`](#the-provider-contract-subagentprovider)。 diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 41d4394066..f74e1d9e51 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -5213,7 +5213,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SessionObservation', - declaration: 'export interface SessionObservation extends Disposable {\n readonly source: \'live\' | \'prepared\';\n readonly header: SessionHeader;\n readonly inheritedEventCount: SessionLogOffsetType;\n readonly events: readonly SessionEvent[];\n readonly cursor: SessionSeqCursor;\n readonly revision?: SessionPersistenceRevision;\n readonly projections?: ProjectionSnapshot;\n readonly projectionStates?: Partial;\n retain(): SessionObservation;\n}', + declaration: 'export interface SessionObservation extends Disposable {\n readonly source: \'live\' | \'prepared\';\n readonly header: SessionHeader;\n readonly inheritedEventCount: SessionLogOffsetType;\n readonly events: readonly SessionEvent[];\n readonly cursor: SessionSeqCursor;\n readonly revision?: SessionPersistenceRevision;\n readonly projections?: ProjectionSnapshot;\n retain(): SessionObservation;\n}', }, { name: 'SessionObservationOptions', diff --git a/packages/session-query/session-query/README.i18n.yaml b/packages/session-query/session-query/README.i18n.yaml index 2ab8f8e1b7..122ec31432 100644 --- a/packages/session-query/session-query/README.i18n.yaml +++ b/packages/session-query/session-query/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session-query/session-query/README.md -README.md: 9b7f7b09e91dea8dc6c853eb342c5ca434614f99 -README.zh.md: 7f098a082393cd78fb477b2cf9c07eb74d831d1b +README.md: cbd7379b290397101ab3178e9d742c87a4afab08 +README.zh.md: 02bf708e4e8dc0104f5f031c8b963f29a5675b75 diff --git a/packages/session-query/session-query/README.md b/packages/session-query/session-query/README.md index 9b7f7b09e9..cbd7379b29 100644 --- a/packages/session-query/session-query/README.md +++ b/packages/session-query/session-query/README.md @@ -108,7 +108,7 @@ The decision history lives in the [unified service decision](../../../.agents/no ### Observation cache -`observeSession` builds point observations without a listing preflight. A live observation fixes its cut as the current log length and materializes `events` on first read, so header-, cursor-, or projection-only consumers never copy the log; the log only appends, so a late first read still yields exactly that prefix. The cold path stats the stored session first and consults an own bounded cache keyed by the persistence instance and the `stat` revision: an unchanged revision reuses the restored unpublished Session without re-reading the log; a changed revision, or a replaced persistence instance, reloads through the handle seam and replaces the entry. The cache holds `preparedSessionCacheSize` entries with least-recently-used eviction, entries pinned by active observation leases are never evicted, and a session that goes live mid-read retries the live path. Default observations return every registered host state in `projectionStates` alongside client views. State values come from the registry’s detached checkpoint rows at the observation cursor. `projectionMode: 'none'` omits both views and states and performs no projection hydration or checkpointing. +`observeSession` builds point observations without a listing preflight. A live observation fixes its cut as the current log length and materializes `events` on first read, so header-, cursor-, or projection-only consumers never copy the log; the log only appends, so a late first read still yields exactly that prefix. The cold path stats the stored session first and consults an own bounded cache keyed by the persistence instance and the `stat` revision: an unchanged revision reuses the restored unpublished Session without re-reading the log; a changed revision, or a replaced persistence instance, reloads through the handle seam and replaces the entry. The cache holds `preparedSessionCacheSize` entries with least-recently-used eviction, entries pinned by active observation leases are never evicted, and a session that goes live mid-read retries the live path. ### Reads and traces diff --git a/packages/session-query/session-query/README.zh.md b/packages/session-query/session-query/README.zh.md index 7f098a0823..02bf708e4e 100644 --- a/packages/session-query/session-query/README.zh.md +++ b/packages/session-query/session-query/README.zh.md @@ -108,7 +108,7 @@ kind: "package-reference" ### 观察缓存 -`observeSession` 不经过列表预检直接构建定点观察。实时观察以当前日志长度固定 cut,并在首次读取时才物化 `events`,因此只需要 header、cursor 或 projection 的消费者永远不会复制日志;日志只会追加,所以延后的首次读取得到的仍然正好是该前缀。冷路径先对存储会话执行 `stat`,再查询自有的有界缓存,缓存键为持久化实例加 `stat` 修订:修订未变则复用已恢复的未发布 Session,不再重读日志;修订变化或持久化实例被替换则经 handle 缝重新加载并替换条目。缓存保留 `preparedSessionCacheSize` 个条目并按最久未用淘汰,被活跃观察租约钉住的条目从不被淘汰;读取中途转为实时的会话会重试实时路径。 默认观察在返回客户端视图的同时,通过 `projectionStates` 返回全部已注册 host state。状态值来自注册表在观察 cursor 处生成的独立检查点行。`projectionMode: 'none'` 同时省略视图和状态,不执行投影 hydration 或检查点生成。 +`observeSession` 不经过列表预检直接构建定点观察。实时观察以当前日志长度固定 cut,并在首次读取时才物化 `events`,因此只需要 header、cursor 或 projection 的消费者永远不会复制日志;日志只会追加,所以延后的首次读取得到的仍然正好是该前缀。冷路径先对存储会话执行 `stat`,再查询自有的有界缓存,缓存键为持久化实例加 `stat` 修订:修订未变则复用已恢复的未发布 Session,不再重读日志;修订变化或持久化实例被替换则经 handle 缝重新加载并替换条目。缓存保留 `preparedSessionCacheSize` 个条目并按最久未用淘汰,被活跃观察租约钉住的条目从不被淘汰;读取中途转为实时的会话会重试实时路径。 ### 读取与追踪 diff --git a/packages/session-query/session-query/src/observation.ts b/packages/session-query/session-query/src/observation.ts index 78eb767d1b..7ef86cebd4 100644 --- a/packages/session-query/session-query/src/observation.ts +++ b/packages/session-query/session-query/src/observation.ts @@ -9,7 +9,6 @@ import type { SessionPersistenceSnapshot, } from '@deepseek-ai/dsh-session-persistence' import type { ProjectionSnapshot } from '@deepseek-ai/dsh-session-projection' -import type { SessionProjectionStateMap } from '@deepseek-ai/dsh-session-projection/types' import type {} from '@deepseek-ai/dsh-session-projection-cache' import { SESSION_QUERY_DEFAULT_PREPARED_SESSION_CACHE_SIZE, SessionQueryError } from './config.ts' import { readColdSessionLog, type ColdSessionLog } from './cold-read.ts' @@ -34,11 +33,6 @@ export interface SessionObservation extends Disposable { readonly revision?: SessionPersistenceRevision /** Exact projection baseline at {@link cursor}, when the registry is mounted. */ readonly projections?: ProjectionSnapshot - /** - * All registered host projection states detached at {@link cursor}; omitted - * when projections are disabled or the registry is absent. - */ - readonly projectionStates?: Partial /** * Retain the same immutable cut for another Host owner. * @returns an independently disposable lease over this observation. @@ -50,7 +44,7 @@ export interface SessionObservation extends Disposable { export interface SessionObservationOptions { /** Optional cancellation while resolving a cold source. */ readonly signal?: AbortSignal - /** Whether to return projection views and states or leave projections untouched. */ + /** Whether to compute every projection or leave projection state untouched. */ readonly projectionMode?: 'all' | 'none' } @@ -97,7 +91,7 @@ export class SessionObservationReader { /** * Observe one live-preferred Session and retain a cold preparation until disposal. * @param sessionId - logical Session identity. - * @param options - cancellation and projection views and states for this read. + * @param options - cancellation and all-or-none projection computation for this read. * @returns one exact immutable observation. */ async read( @@ -114,9 +108,7 @@ export class SessionObservationReader { const snapshot = await this.statSource(persistence, sessionId, signal) const attachedDuringStat = this.ctx.sessions.get(sessionId) - if (attachedDuringStat !== undefined) { - return this.live(attachedDuringStat, projectionMode) - } + if (attachedDuringStat !== undefined) return this.live(attachedDuringStat, projectionMode) let entry = this.cachedEntry(persistence, sessionId, snapshot.revision) if (entry === undefined) { const loaded = await this.loadSource(persistence, sessionId, signal) @@ -156,12 +148,8 @@ export class SessionObservationReader { } let projections: ProjectionSnapshot | undefined - let projectionStates: Partial | undefined try { - if (projectionMode === 'all') { - projections = this.preparedProjections(entry) - projectionStates = this.projectionStates(entry.session) - } + projections = projectionMode === 'none' ? undefined : this.preparedProjections(entry) } catch (error: unknown) { throw new SessionQueryError( `failed to project session "${sessionId}": ${errorMessage(error)}`, @@ -169,7 +157,7 @@ export class SessionObservationReader { { cause: error }, ) } - return this.preparedLease(sessionId, entry, projections, projectionStates) + return this.preparedLease(sessionId, entry, projections) } } @@ -255,7 +243,6 @@ export class SessionObservationReader { sessionId: SessionId, entry: PreparedEntry, projections: ProjectionSnapshot | undefined, - projectionStates: Partial | undefined, ): SessionObservation { entry.refs += 1 const lease = (): SessionObservation => { @@ -268,7 +255,6 @@ export class SessionObservationReader { cursor: entry.events.at(-1)?.seq ?? -1, revision: entry.revision, ...projections === undefined ? {} : { projections }, - ...projectionStates === undefined ? {} : { projectionStates }, retain: () => { if (disposed) throw new Error(`session observation "${sessionId}" is disposed`) entry.refs += 1 @@ -296,7 +282,6 @@ export class SessionObservationReader { const projections = projectionMode === 'none' ? undefined : this.ctx.get('sessionProjections')?.snapshot(session) - const projectionStates = projectionMode === 'none' ? undefined : this.projectionStates(session) const lease = (): SessionObservation => { let disposed = false return { @@ -309,7 +294,6 @@ export class SessionObservationReader { }, cursor: seq === 0 ? -1 : SessionSeq(seq - 1), ...projections === undefined ? {} : { projections }, - ...projectionStates === undefined ? {} : { projectionStates }, retain: () => { if (disposed) throw new Error(`session observation "${session.id}" is disposed`) return lease() @@ -328,13 +312,6 @@ export class SessionObservationReader { ? registry.hydrate(entry.session, {}, entry.events, SessionLogOffset(0)) : cache.hydratePrepared(entry.session, entry.events) } - - /** Checkpoint rows already own detached state values at the Session cut. */ - private projectionStates(session: Session): Partial | undefined { - const registry = this.ctx.get('sessionProjections') - if (registry === undefined) return undefined - return Object.fromEntries(Object.entries(registry.checkpoint(session)).map(([key, row]) => [key, row.val])) - } } function throwIfObservationAborted(signal: AbortSignal | undefined): void { diff --git a/packages/session-query/session-query/tests/observation.spec.ts b/packages/session-query/session-query/tests/observation.spec.ts index 6521269c40..8e2879ca14 100644 --- a/packages/session-query/session-query/tests/observation.spec.ts +++ b/packages/session-query/session-query/tests/observation.spec.ts @@ -17,16 +17,9 @@ import type { SessionPersistenceStatOptions, } from '@deepseek-ai/dsh-session-persistence' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' -import { titleProjectionDefinition } from '@deepseek-ai/dsh-session-title' import { describe, expect, it, vi } from 'vitest' import { SessionObservationReader } from '../src/observation.ts' -declare module '@deepseek-ai/dsh-session-projection/types' { - interface SessionProjectionStateMap { - 'observation-test/titles': (string | null)[] - } -} - function header(id: string): SessionHeader { return { version: SESSION_FORMAT_VERSION, id: SessionId(id), createdAt: 1, isSeeded: false, cwd: '/workspace' } } @@ -43,15 +36,6 @@ function messageEvent(seq: number, text: string): SessionEvent { } } -function titleEvent(seq: number, title: string): SessionEvent { - return { - type: 'session/title', - seq: SessionSeq(seq), - time: seq + 1, - data: { title, messageSeqs: [], source: { kind: 'user' } }, - } -} - /** A log whose writer crashed mid-turn: read-only viewing must balance it in memory. */ function interruptedLog(text: string): SessionEvent[] { return [ @@ -802,98 +786,3 @@ describe('SessionObservationReader cold projections', () => { await ctx.fiber.dispose() }) }) - -describe('SessionObservationReader host projection states', () => { - it('returns detached live states and views at the observed cut', async () => { - const ctx = await readerContext() - await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(titleProjectionDefinition) - const session = ctx.sessions.create(SessionId('live-host-state')) - session.append('session/title', { title: 'Live title', messageSeqs: [], source: { kind: 'user' } }) - const reader = new SessionObservationReader(ctx) - - using observed = await reader.read(session.id) - session.append('session/title', { title: 'Later title', messageSeqs: [], source: { kind: 'user' } }) - using later = await reader.read(session.id) - - expect(observed.projections?.values.title).toBeDefined() - expect(observed.projectionStates).toEqual({ title: 'Live title' }) - expect(later.projectionStates).toEqual({ title: 'Later title' }) - await ctx.fiber.dispose() - }) - - it('returns cold states and views from the prepared Session', async () => { - const ctx = await readerContext() - await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(titleProjectionDefinition) - const meta = header('cold-host-state') - const store = new Map([[meta.id, { header: meta, events: [titleEvent(0, 'Cold title')], revision: 'r1' }]]) - ctx.provide('sessionPersistence', stubPersistence(store, { stat: 0, open: 0, read: 0 })) - const hydrate = vi.spyOn(ctx.sessionProjections, 'hydrate') - - using observed = await new SessionObservationReader(ctx).read(meta.id) - - expect(observed.source).toBe('prepared') - expect(observed.projections?.values.title).toBeDefined() - expect(observed.projectionStates).toEqual({ title: 'Cold title' }) - expect(hydrate).toHaveBeenCalledOnce() - await ctx.fiber.dispose() - }) - - it.each(['live', 'cold'] as const)('returns all detached states on repeated %s reads and omits them in none mode', async (source) => { - const ctx = await readerContext() - try { - await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(titleProjectionDefinition) - ctx.sessionProjections.register({ - key: 'observation-test/titles', - stateVersion: 1, - stateSchema: titleProjectionDefinition.stateSchema.array(), - init: () => [] as (string | null)[], - apply: (state, event) => event.type === 'session/title' - ? [...state, event.data.title] : state, - }) - const meta = header(`all-states-${source}`) - if (source === 'live') { - const session = ctx.sessions.create(meta.id) - session.append('session/title', { title: 'First', messageSeqs: [], source: { kind: 'user' } }) - } else { - const store = new Map([[meta.id, { header: meta, events: [titleEvent(0, 'First')], revision: 'r1' }]]) - ctx.provide('sessionPersistence', stubPersistence(store, { stat: 0, open: 0, read: 0 })) - } - const reader = new SessionObservationReader(ctx) - using first = await reader.read(meta.id) - expect(first.projectionStates).toEqual({ title: 'First', 'observation-test/titles': ['First'] }) - first.projectionStates?.['observation-test/titles']?.push('caller edit') - using second = await reader.read(meta.id) - expect(second.projectionStates).toEqual({ title: 'First', 'observation-test/titles': ['First'] }) - expect(second.projections?.values).toEqual({ title: 'First' }) - const checkpoint = vi.spyOn(ctx.sessionProjections, 'checkpoint') - const hydrate = vi.spyOn(ctx.sessionProjections, 'hydrate') - using none = await reader.read(meta.id, { projectionMode: 'none' }) - expect(none.projections).toBeUndefined() - expect(none.projectionStates).toBeUndefined() - expect(checkpoint).not.toHaveBeenCalled() - expect(hydrate).not.toHaveBeenCalled() - } finally { - await ctx.fiber.dispose() - } - }) - - it('returns an empty state map for an empty registry and omits it without a registry', async () => { - const withRegistry = await readerContext() - await withRegistry.plugin(SessionProjectionRegistry) - const registered = withRegistry.sessions.create(SessionId('unregistered-key')) - using partial = await new SessionObservationReader(withRegistry).read(registered.id) - expect(partial.projectionStates).toEqual({}) - await withRegistry.fiber.dispose() - - const withoutRegistry = await readerContext() - const meta = header('no-registry') - const store = new Map([[meta.id, { header: meta, events: [titleEvent(0, 'Cold title')], revision: 'r1' }]]) - withoutRegistry.provide('sessionPersistence', stubPersistence(store, { stat: 0, open: 0, read: 0 })) - using absent = await new SessionObservationReader(withoutRegistry).read(meta.id) - expect(absent.projectionStates).toBeUndefined() - await withoutRegistry.fiber.dispose() - }) -}) diff --git a/packages/subagent/subagent/README.i18n.yaml b/packages/subagent/subagent/README.i18n.yaml index 9d53d1de77..844b27593b 100644 --- a/packages/subagent/subagent/README.i18n.yaml +++ b/packages/subagent/subagent/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/subagent/subagent/README.md -README.md: 4c2f8254663214bdd1400cb9859f3e54e33a41dc -README.zh.md: a65f0322972c473a7c7945c1d2cf479864c18e8d +README.md: 61ac5d212d4ce3292a5880c520d0f731dfbc3766 +README.zh.md: ff2b83ea41d23aa3efe3b43d73bb4865add74fc2 diff --git a/packages/subagent/subagent/README.md b/packages/subagent/subagent/README.md index 4c2f825466..61ac5d212d 100644 --- a/packages/subagent/subagent/README.md +++ b/packages/subagent/subagent/README.md @@ -96,7 +96,7 @@ A request is validated against the provider's advertised capabilities, a durable The manager reserves a child identity, resolves the durable descriptor, creates (or cold-resumes) the child Agent, installs it in an Activation, and submits the prompt. Model-authored messages cross one parent/child edge through fixed Steer scheduling; browser human prompts choose Queue or best-effort Steer through an internal adapter, while other host protocols may retain Queue for distinct turns. A Session queue command admits a live subagent-owned Agent only from its own continuable descriptor. Settlement waits for Agent activity to finish, an empty Inbox, and no owned children, then flushes final Session state with admission open. Under the child lock, the manager revalidates the wake generation, Session sequence, Inbox, and owned children; the synchronous task entry of `Agent.runMaintenance()` claims the idle phase and closes the private subagent Inbox in the same JavaScript turn before handle disposal. An absent direct-child Activation cold-resumes from the persisted session. When a resident Activation settles, the manager tells the child's direct parent in the parent's own turn stream. -Successful local child creation appends a `subagent/catalog` fact to the parent Session. One-shot creation records it after the provider returns; continuable creation records it after initial inbox admission and before returning the child id. Failure releases the child without publishing a compensating catalog event. The host-only `subagentCatalog` projection excludes fork-inherited facts and returns entries in parent catalog event order in O(D) time for D facts; it can be read through an observation's requested projection state. [The parent-catalog decision](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md) owns ordering, persistence costs, and alternatives. +Successful local child creation appends a `subagent/catalog` fact to the parent Session. One-shot creation records it after the provider returns; continuable creation records it after initial inbox admission and before returning the child id. Failure releases the child without publishing a compensating catalog event. The `subagentCatalog` projection excludes fork-inherited facts and exposes a direct-child list through `projections.values.subagentCatalog` in Session observations and client snapshots. Its view preserves parent catalog event order in O(D) time for D facts. [The parent-catalog decision](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md) owns ordering, persistence costs, and alternatives. ### Ownership and invariants diff --git a/packages/subagent/subagent/README.zh.md b/packages/subagent/subagent/README.zh.md index a65f032297..ff2b83ea41 100644 --- a/packages/subagent/subagent/README.zh.md +++ b/packages/subagent/subagent/README.zh.md @@ -96,7 +96,7 @@ kind: "package-reference" 管理器预留 child 身份、解析持久化描述符、创建(或冷恢复)child、把它安装进 Activation 并提交提示词。模型编写的消息通过固定 Steer 调度跨一条 parent/child 边;浏览器人类 prompt 通过内部适配器选择 Queue 或 best-effort Steer,其他 host 协议仍可保留 Queue 以创建独立轮次。Session queue command 仅根据 child 自身的 continuable descriptor 准入在线 subagent-owned Agent。Settlement 会等待 Agent 活动结束、Inbox 为空且没有所拥有子级,再在准入开放时 flush 最终 Session 状态。管理器随后在 child lock 内重新验证 wake generation、Session 序号、Inbox 与所拥有子级;`Agent.runMaintenance()` 的同步 task 入口会占用 idle 阶段,并在同一个 JavaScript turn 内关闭私有 subagent Inbox,然后才 dispose handle。直接 child 不存在 Activation 时会从持久化会话冷恢复。当驻留 Activation 结算时,管理器会在 parent 自身的轮次流中告知该 child 的直接 parent。 -本地子级创建成功时,父 Session 追加一条 `subagent/catalog` 事实。一次性创建在 provider 返回后记录;可继续创建在初始 inbox 准入后、返回子级 id 前记录。失败会释放子级,不发布补偿性目录事件。仅 host 使用的 `subagentCatalog` projection 排除 fork 继承的事实,对 D 条事实以 O(D) 时间按父目录事件顺序返回条目,并可通过观察请求的 projection state 读取。[父目录决策](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md) 说明排序、持久化成本和替代方案。 +本地子级创建成功时,父 Session 追加一条 `subagent/catalog` 事实。一次性创建在 provider 返回后记录;可继续创建在初始 inbox 准入后、返回子级 id 前记录。失败会释放子级,不发布补偿性目录事件。`subagentCatalog` projection 排除 fork 继承的事实,通过 Session 观察和客户端快照中的 `projections.values.subagentCatalog` 暴露直接子级列表。其视图对 D 条事实以 O(D) 时间保留父目录事件顺序。[父目录决策](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md) 说明排序、持久化成本和替代方案。 ### 所有权与不变式 diff --git a/packages/subagent/subagent/src/catalog.ts b/packages/subagent/subagent/src/catalog.ts index 890b04f6fd..c68943a707 100644 --- a/packages/subagent/subagent/src/catalog.ts +++ b/packages/subagent/subagent/src/catalog.ts @@ -13,6 +13,7 @@ import type { SessionLogOffset, } from '@deepseek-ai/dsh-session' import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection' +import type { SubagentCatalogEntry } from './projection-types.ts' /** Current payload version for `subagent/catalog` events. */ export const SUBAGENT_CATALOG_VERSION = 0 @@ -38,27 +39,13 @@ declare module '@deepseek-ai/dsh-session/types' { } } -/** One current direct-child discovery row materialized from parent facts. */ -export type SubagentCatalogEntry = - & { - readonly id: SessionId - readonly createdAt: number - } - & ( - | { readonly mode: 'one-shot'; readonly label?: string } - | { readonly mode: 'continuable'; readonly label: string } - ) - /** A fixed-size persistent stack node; newest facts occupy the head chunk. */ interface CatalogChunk { readonly facts: readonly SubagentCatalogEvent[] readonly previous?: CatalogChunk | undefined } -/** - * Host fold state for one parent catalog. A cold reader detaches it from a - * Session observation for direct-child materialization. - */ +/** Host fold state for one parent catalog. */ export interface SubagentCatalogState { readonly inheritedEventCount: SessionLogOffset readonly head?: CatalogChunk | undefined @@ -83,6 +70,16 @@ const eventDataSchema = z.union([ oneShotCatalogSchema, continuableCatalogSchema, ]) as unknown as z.ZodType +const viewSchema = z.array(z.union([ + oneShotCatalogSchema.omit({ version: true, childId: true, childCreatedAt: true }).extend({ + id: sessionIdSchema, + createdAt: oneShotCatalogSchema.shape.childCreatedAt, + }), + continuableCatalogSchema.omit({ version: true, childId: true, childCreatedAt: true }).extend({ + id: sessionIdSchema, + createdAt: continuableCatalogSchema.shape.childCreatedAt, + }), +])) as unknown as z.ZodType const chunkSchema: z.ZodType = z.lazy(() => z.object({ facts: z.array(eventDataSchema).min(1).max(64), previous: chunkSchema.optional(), @@ -118,7 +115,7 @@ function appendFact(state: SubagentCatalogState, fact: SubagentCatalogEvent): Su * @param state - parent catalog fold state. * @returns current direct-child rows in parent catalog event order. */ -export function subagentCatalogEntries(state: SubagentCatalogState): SubagentCatalogEntry[] { +function subagentCatalogEntries(state: SubagentCatalogState): SubagentCatalogEntry[] { const chunks: CatalogChunk[] = [] for (let chunk = state.head; chunk !== undefined; chunk = chunk.previous) chunks.push(chunk) const entries: SubagentCatalogEntry[] = [] @@ -154,6 +151,7 @@ export const subagentCatalogProjectionDefinition = { return appendFact(state, parsed.data) }, stateVersion: 1, + wire: { viewSchema, view: subagentCatalogEntries }, } satisfies ProjectionDefinition<'subagentCatalog', SubagentCatalogState> /** diff --git a/packages/subagent/subagent/src/client.ts b/packages/subagent/subagent/src/client.ts index 8a643d6284..baa704b200 100644 --- a/packages/subagent/subagent/src/client.ts +++ b/packages/subagent/subagent/src/client.ts @@ -5,4 +5,4 @@ */ export type * from './control-types.ts' -export type { SubagentIdentityProjection, SubagentTimingProjection } from './projection-types.ts' +export type { SubagentCatalogEntry, SubagentIdentityProjection, SubagentTimingProjection } from './projection-types.ts' diff --git a/packages/subagent/subagent/src/projection-types.ts b/packages/subagent/subagent/src/projection-types.ts index bf31d8b0aa..931459df3d 100644 --- a/packages/subagent/subagent/src/projection-types.ts +++ b/packages/subagent/subagent/src/projection-types.ts @@ -4,7 +4,18 @@ * @module @deepseek-ai/dsh-subagent/projection-types */ -import type { SessionSeq } from '@deepseek-ai/dsh-session/types' +import type { SessionId, SessionSeq } from '@deepseek-ai/dsh-session/types' + +/** One current direct-child discovery row materialized from parent facts. */ +export type SubagentCatalogEntry = + & { + readonly id: SessionId + readonly createdAt: number + } + & ( + | { readonly mode: 'one-shot'; readonly label?: string } + | { readonly mode: 'continuable'; readonly label: string } + ) /** Durable active-turn timing for one descriptor-backed child session. */ export interface SubagentTimingProjection { @@ -50,6 +61,8 @@ export type SubagentIdentityProjection = declare module '@deepseek-ai/dsh-session-projection/types' { interface SessionProjectionMap { + /** Direct children in parent catalog event order, excluding fork-inherited facts. */ + subagentCatalog: SubagentCatalogEntry[] /** Active-turn duration for a descriptor-backed subagent session. */ subagentTiming: SubagentTimingProjection /** diff --git a/packages/subagent/subagent/tests/catalog.spec.ts b/packages/subagent/subagent/tests/catalog.spec.ts index 8983679574..6d260840d7 100644 --- a/packages/subagent/subagent/tests/catalog.spec.ts +++ b/packages/subagent/subagent/tests/catalog.spec.ts @@ -1,4 +1,7 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' +import { Context } from '@deepseek-ai/cordis' +import SessionStore from '@deepseek-ai/dsh-session' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import { SessionLogOffset, SessionSeq, @@ -7,7 +10,6 @@ import { } from '@deepseek-ai/dsh-session' import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session' import { - subagentCatalogEntries, subagentCatalogProjectionDefinition, } from '../src/catalog.ts' import type { SubagentCatalogState } from '../src/catalog.ts' @@ -48,6 +50,37 @@ function fact( } describe('subagent catalog projection', () => { + it('publishes detached catalog views and changes only for new own facts', async () => { + const ctx = new Context() + try { + await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) + ctx.sessionProjections.register(subagentCatalogProjectionDefinition) + const parent = ctx.sessions.create(header.id) + const changes = vi.fn() + ctx.sessionProjections.onChanged(changes) + expect(ctx.sessionProjections.snapshot(parent).values.subagentCatalog).toEqual([]) + + parent.append('subagent/catalog', fact(0, 'first', 1, { mode: 'one-shot' }).data) + const first = ctx.sessionProjections.snapshot(parent) + expect(first.values.subagentCatalog).toEqual([ + { id: SessionId('first'), createdAt: 1, mode: 'one-shot' }, + ]) + changes.mockClear() + parent.append('session/title', { title: 'Parent', messageSeqs: [], source: { kind: 'user' } }) + expect(changes).not.toHaveBeenCalled() + parent.append('subagent/catalog', fact(2, 'second', 2, { mode: 'continuable', label: 'Next' }).data) + expect(changes).toHaveBeenCalledOnce() + expect(first.values.subagentCatalog).toHaveLength(1) + const second = ctx.sessionProjections.snapshot(parent) + expect(second.values.subagentCatalog).toHaveLength(2) + second.values.subagentCatalog?.pop() + expect(ctx.sessionProjections.snapshot(parent).values.subagentCatalog).toHaveLength(2) + } finally { + await ctx.fiber.dispose() + } + }) + it('preserves event order across chunk rollover and checkpoint restoration', () => { const first = fold(Array.from({ length: 64 }, (_, i) => fact(i, `child-${i}`, 130 - i, { mode: 'one-shot' }))) @@ -63,8 +96,8 @@ describe('subagent catalog projection', () => { ) expect(JSON.stringify(first)).toBe(checkpoint) - expect(subagentCatalogEntries(first)).toHaveLength(64) - expect(subagentCatalogEntries(restored).map(entry => entry.id)) + expect(subagentCatalogProjectionDefinition.wire.view(first)).toHaveLength(64) + expect(subagentCatalogProjectionDefinition.wire.view(restored).map(entry => entry.id)) .toEqual(Array.from({ length: 130 }, (_, i) => SessionId(`child-${i}`))) }) @@ -89,7 +122,12 @@ describe('subagent catalog projection', () => { expect(subagentCatalogProjectionDefinition.stateSchema.parse(JSON.parse(JSON.stringify(state)))) .toEqual(state) - expect(subagentCatalogEntries(state)).toEqual([ + const view = subagentCatalogProjectionDefinition.wire.view(state) + expect(subagentCatalogProjectionDefinition.wire.viewSchema.parse(JSON.parse(JSON.stringify(view)))).toEqual(view) + expect(() => subagentCatalogProjectionDefinition.wire.viewSchema.parse([ + { id: 'bad', createdAt: 1, mode: 'continuable' }, + ])).toThrow() + expect(view).toEqual([ { id: SessionId('child-b'), createdAt: 1, mode: 'one-shot' }, { id: SessionId('child-a'), createdAt: 1, mode: 'one-shot', label: 'once' }, { id: SessionId('child-d'), createdAt: 3, mode: 'continuable', label: 'later' }, From 924282cbcda68b002e9fd709c0668473e0e6e151 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Tue, 8 Sep 2026 16:00:15 +0800 Subject: [PATCH 21/58] fix(subagent): retain catalog event types in host exports --- packages/subagent/subagent/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/subagent/subagent/src/index.ts b/packages/subagent/subagent/src/index.ts index aa11318ec3..8738bbd937 100644 --- a/packages/subagent/subagent/src/index.ts +++ b/packages/subagent/subagent/src/index.ts @@ -75,6 +75,7 @@ import type { SubagentDescendantListEntry, SubagentListEntry } from './list-chil import { snapshotSubagentDescriptor } from './descriptor.ts' import { subagentIdentityProjectionDefinition, subagentTimingProjectionDefinition } from './projection.ts' import { establishCatalogChild, subagentCatalogProjectionDefinition } from './catalog.ts' +import type {} from './catalog.ts' import { deliverSubagentPrompt } from './internal.ts' export * from './out-of-process.ts' From 0274a6dd75e8eb054949134560c16d8ac1ec4501 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Tue, 8 Sep 2026 16:01:37 +0800 Subject: [PATCH 22/58] fix(subagent): preserve catalog augmentation in declarations --- packages/subagent/subagent/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/subagent/subagent/src/index.ts b/packages/subagent/subagent/src/index.ts index 8738bbd937..40af1dfed4 100644 --- a/packages/subagent/subagent/src/index.ts +++ b/packages/subagent/subagent/src/index.ts @@ -75,9 +75,9 @@ import type { SubagentDescendantListEntry, SubagentListEntry } from './list-chil import { snapshotSubagentDescriptor } from './descriptor.ts' import { subagentIdentityProjectionDefinition, subagentTimingProjectionDefinition } from './projection.ts' import { establishCatalogChild, subagentCatalogProjectionDefinition } from './catalog.ts' -import type {} from './catalog.ts' import { deliverSubagentPrompt } from './internal.ts' +export type {} from './catalog.ts' export * from './out-of-process.ts' export { AssistantOutputFold, finalAssistantOutput } from './assistant-output.ts' export { SubagentRunId } from './types.ts' From 59623c8ff4043d14db34249d552f752bff2a9bbe Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Tue, 8 Sep 2026 16:06:12 +0800 Subject: [PATCH 23/58] docs(subagent): refresh event declaration references --- docs/event-producer-consumer.i18n.yaml | 2 +- docs/event-producer-consumer.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/event-producer-consumer.i18n.yaml b/docs/event-producer-consumer.i18n.yaml index 1e314eaacc..0117f66a78 100644 --- a/docs/event-producer-consumer.i18n.yaml +++ b/docs/event-producer-consumer.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/event-producer-consumer.md -event-producer-consumer.md: 81b6fc83f0c3922df402106789c5788b4c0a6b3e +event-producer-consumer.md: d5ee3453e8694de70cfd64e44b3ce724aa76cd86 event-producer-consumer.zh.md: d857561a7d9dbdf4c4d971a1854d43d3ecc4fd2a diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 81b6fc83f0..d5ee3453e8 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -55,10 +55,10 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:105`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `remotes` | | `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:92`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) | | `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:298`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - | -| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:169`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), `server`, [`subagent`](../packages/subagent/subagent) | -| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:143`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:149`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:160`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`subagent`](../packages/subagent/subagent) | +| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:170`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), `server`, [`subagent`](../packages/subagent/subagent) | +| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:144`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:150`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:161`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`subagent`](../packages/subagent/subagent) | | `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`session-reference`](../packages/context/session-reference), [`system-prompt`](../packages/core/system-prompt) | | `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - | | `tools/change` | `emit` | [`packages/core/tools/src/index.ts:199`](../packages/core/tools/src/index.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`), [`tools`](../packages/core/tools) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | From 867c47c415789efe73b9a633c8b6f2d5ee2abc3e Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Tue, 8 Sep 2026 16:10:49 +0800 Subject: [PATCH 24/58] docs: drop unrelated projection README whitespace changes --- packages/session/session-projection-cache/README.i18n.yaml | 4 ++-- packages/session/session-projection-cache/README.md | 1 + packages/session/session-projection-cache/README.zh.md | 1 + packages/session/session-projection/README.i18n.yaml | 4 ++-- packages/session/session-projection/README.md | 1 + packages/session/session-projection/README.zh.md | 1 + 6 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/session/session-projection-cache/README.i18n.yaml b/packages/session/session-projection-cache/README.i18n.yaml index e58dce3a5a..c738058bea 100644 --- a/packages/session/session-projection-cache/README.i18n.yaml +++ b/packages/session/session-projection-cache/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session/session-projection-cache/README.md -README.md: 4ac2ddabe878202a3b324d5b2aaecbea8da99e5f -README.zh.md: ffed860eb59621d82672c25989d603b79634a498 +README.md: dcbe57af1f9c13f1f43572ff188dfdaaf9287160 +README.zh.md: 367d893c56a23e0965eb1e10f42fb6705284af6b diff --git a/packages/session/session-projection-cache/README.md b/packages/session/session-projection-cache/README.md index 4ac2ddabe8..dcbe57af1f 100644 --- a/packages/session/session-projection-cache/README.md +++ b/packages/session/session-projection-cache/README.md @@ -122,6 +122,7 @@ None; the cache never assembles or sends provider requests. + These limits define where the cache needs operational care. They are current package constraints, not a task backlog. - **No eviction or retention surface** — records accumulate per session; pruning stored checkpoints is out-of-band maintenance, same stance as session persistence itself. diff --git a/packages/session/session-projection-cache/README.zh.md b/packages/session/session-projection-cache/README.zh.md index ffed860eb5..367d893c56 100644 --- a/packages/session/session-projection-cache/README.zh.md +++ b/packages/session/session-projection-cache/README.zh.md @@ -122,6 +122,7 @@ kind: "package-reference" + 这些限制说明缓存何时需要运维注意。它们是当前包约束,不是任务积压。 - **无淘汰或保留接口**——记录按会话持续累积;清理已存储检查点属于带外维护,与会话持久化采用相同策略。 diff --git a/packages/session/session-projection/README.i18n.yaml b/packages/session/session-projection/README.i18n.yaml index b1f80c30a6..b92fb0f575 100644 --- a/packages/session/session-projection/README.i18n.yaml +++ b/packages/session/session-projection/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session/session-projection/README.md -README.md: ced9837dbf69a0fc168da7f574342e803d83bfea -README.zh.md: 2af7d33d617414baac24575ad425db0db8300cd8 +README.md: 9bd08d119cf1961a47595bec29efabdc8d7fb276 +README.zh.md: e7982626f6e0c734ef426ec1a2851562ca01525a diff --git a/packages/session/session-projection/README.md b/packages/session/session-projection/README.md index ced9837dbf..9bd08d119c 100644 --- a/packages/session/session-projection/README.md +++ b/packages/session/session-projection/README.md @@ -126,6 +126,7 @@ None; projections never assemble or send provider requests. + These limits define where the projection registry needs care at scale. They are current package constraints, not a task backlog. - **Every tail page carries every client-visible key** — there is no per-key opt-out or lazy-key request shape yet; acceptable while values are UI-scale whole states, revisit if a domain's value grows large. diff --git a/packages/session/session-projection/README.zh.md b/packages/session/session-projection/README.zh.md index 2af7d33d61..e7982626f6 100644 --- a/packages/session/session-projection/README.zh.md +++ b/packages/session/session-projection/README.zh.md @@ -126,6 +126,7 @@ const { asOfSeq, values } = ctx.sessionProjections.snapshot(session) + 这些限制说明投影注册表在大规模下何时需要特别处理。它们是当前包约束,不是任务积压。 - **每个尾页携带每个 client-visible key**——尚无逐 key 的 opt-out 或惰性 key 请求形状;在值都是 UI 量级的全量状态时可以接受,若某领域的值变大再重议。 From ba731d1c9e2f94e1f8d1b6c42db5744280f26608 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Tue, 8 Sep 2026 16:40:08 +0800 Subject: [PATCH 25/58] fix(subagent): handle rejected runs after catalog failure --- docs/subsystems/subagent.i18n.yaml | 4 ++-- docs/subsystems/subagent.md | 2 ++ docs/subsystems/subagent.zh.md | 2 ++ .../extensions/tool-cordis/src/api-catalog.ts | 2 +- .../session-projection/README.i18n.yaml | 4 ++-- packages/session/session-projection/README.md | 2 -- .../session/session-projection/README.zh.md | 2 -- packages/subagent/subagent/README.i18n.yaml | 4 ++-- packages/subagent/subagent/README.md | 2 +- packages/subagent/subagent/README.zh.md | 2 +- packages/subagent/subagent/src/index.ts | 4 ++++ .../subagent/subagent/tests/service.spec.ts | 19 ++++++++++++++----- 12 files changed, 31 insertions(+), 18 deletions(-) diff --git a/docs/subsystems/subagent.i18n.yaml b/docs/subsystems/subagent.i18n.yaml index 9c6a2880e4..4750a413d5 100644 --- a/docs/subsystems/subagent.i18n.yaml +++ b/docs/subsystems/subagent.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/subagent.md -subagent.md: 24a10bb4a47c58e063797509abc4863f6cfe93a7 -subagent.zh.md: 66a9ac5eab7ecf1a93f60fad29f89da135f68c4b +subagent.md: 5f927f4c0bef57c0fda0bcb39280a5ac2012343e +subagent.zh.md: ce827f154989404825e322682c4af6ba83182253 diff --git a/docs/subsystems/subagent.md b/docs/subsystems/subagent.md index 24a10bb4a4..5f927f4c0b 100644 --- a/docs/subsystems/subagent.md +++ b/docs/subsystems/subagent.md @@ -674,6 +674,8 @@ list(): string[] * fulfills; a rejection therefore has no run for the caller to dispose and * emits no run lifecycle events. Post-publication turn and infrastructure * failures settle through the returned run. + * A catalog append failure disposes the run and handles its result rejection; + * the caller receives the catalog error even if disposal also fails. * @param name - the provider to use. * @param request - child label, prompt, parent, signal, and optional capabilities. * @returns the published holder-owned run. diff --git a/docs/subsystems/subagent.zh.md b/docs/subsystems/subagent.zh.md index 66a9ac5eab..ce827f1549 100644 --- a/docs/subsystems/subagent.zh.md +++ b/docs/subsystems/subagent.zh.md @@ -678,6 +678,8 @@ list(): string[] * fulfills; a rejection therefore has no run for the caller to dispose and * emits no run lifecycle events. Post-publication turn and infrastructure * failures settle through the returned run. + * A catalog append failure disposes the run and handles its result rejection; + * the caller receives the catalog error even if disposal also fails. * @param name - the provider to use. * @param request - child label, prompt, parent, signal, and optional capabilities. * @returns the published holder-owned run. diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index f74e1d9e51..149a4e20d8 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -2351,7 +2351,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ }, { signature: 'async start(name: string, request: SubagentStartRequest): Promise', - description: 'Establish a published child on the named provider. Capability and semantic checks run before delegation. Provider ownership lasts until its promise fulfills; a rejection therefore has no run for the caller to dispose and emits no run lifecycle events. Post-publication turn and infrastructure failures settle through the returned run.', + description: 'Establish a published child on the named provider. Capability and semantic checks run before delegation. Provider ownership lasts until its promise fulfills; a rejection therefore has no run for the caller to dispose and emits no run lifecycle events. Post-publication turn and infrastructure failures settle through the returned run. A catalog append failure disposes the run and handles its result rejection; the caller receives the catalog error even if disposal also fails.', parameters: [{ name: 'name', description: 'the provider to use.' }, { name: 'request', description: 'child label, prompt, parent, signal, and optional capabilities.' }], returns: 'the published holder-owned run.', }, diff --git a/packages/session/session-projection/README.i18n.yaml b/packages/session/session-projection/README.i18n.yaml index b92fb0f575..1b5c91d061 100644 --- a/packages/session/session-projection/README.i18n.yaml +++ b/packages/session/session-projection/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session/session-projection/README.md -README.md: 9bd08d119cf1961a47595bec29efabdc8d7fb276 -README.zh.md: e7982626f6e0c734ef426ec1a2851562ca01525a +README.md: d1aee8aed891fe04585781772757a8a1ea307590 +README.zh.md: e97617a2006f9a944e7fce3aed26cd0e922cce0b diff --git a/packages/session/session-projection/README.md b/packages/session/session-projection/README.md index 9bd08d119c..d1aee8aed8 100644 --- a/packages/session/session-projection/README.md +++ b/packages/session/session-projection/README.md @@ -64,8 +64,6 @@ const { asOfSeq, values } = ctx.sessionProjections.snapshot(session) A domain that requires projected state declares `sessionProjections` as a Cordis service dependency; optional contributors may register under `ctx.inject(['sessionProjections'], …)`. Carriers use `ctx.get('sessionProjections')` and omit their block or frames when the registry is absent. -`restore` and `hydrate` accept `projectionMode`, which defaults to `all`. Mode `none` restores every host state without computing or validating client views. - ### Persisted checkpoints Every unit's state is checkpointed — client-visible and host-only alike — through `checkpoint(session)`, and the sibling [session-projection-cache](../session-projection-cache/README.md) persists those checkpoints so cold reads skip full log loads. Checkpoint watermarks use `SessionSeqCursor` (`-1` for an empty log), while replay starts use `SessionLogOffset`; `restoreFloor` and `restore` implement the read recipe without conflating an existing event with a log gap. diff --git a/packages/session/session-projection/README.zh.md b/packages/session/session-projection/README.zh.md index e7982626f6..e97617a200 100644 --- a/packages/session/session-projection/README.zh.md +++ b/packages/session/session-projection/README.zh.md @@ -64,8 +64,6 @@ const { asOfSeq, values } = ctx.sessionProjections.snapshot(session) 必须使用投影状态的领域把 `sessionProjections` 声明为 Cordis 服务依赖;可选贡献方可以在 `ctx.inject(['sessionProjections'], …)` 下注册。载体使用 `ctx.get('sessionProjections')`,注册表缺席时省略自己的块或帧。 -`restore` 和 `hydrate` 接受 `projectionMode`,默认值为 `all`。模式 `none` 恢复所有 host state,但不计算或校验客户端视图。 - ### 持久检查点 每个单元的状态都会被检查点化——client-visible 与 host-only 一视同仁——通过 `checkpoint(session)`,同级包 [session-projection-cache](../session-projection-cache/README.zh.md) 持久化这些检查点,使冷读跳过全量日志加载。检查点水位使用 `SessionSeqCursor`(空日志为 `-1`),回放起点使用 `SessionLogOffset`;`restoreFloor` 与 `restore` 在无活动会话的情况下实现读取配方,且不会混淆已有事件与日志间隙。 diff --git a/packages/subagent/subagent/README.i18n.yaml b/packages/subagent/subagent/README.i18n.yaml index 844b27593b..8962207eb9 100644 --- a/packages/subagent/subagent/README.i18n.yaml +++ b/packages/subagent/subagent/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/subagent/subagent/README.md -README.md: 61ac5d212d4ce3292a5880c520d0f731dfbc3766 -README.zh.md: ff2b83ea41d23aa3efe3b43d73bb4865add74fc2 +README.md: a37f8d476383185dd6d7a7cbdffb1b577f410328 +README.zh.md: 2cbb4aded2241a822ffe919f8b9baab0ffc3775a diff --git a/packages/subagent/subagent/README.md b/packages/subagent/subagent/README.md index 61ac5d212d..a37f8d4763 100644 --- a/packages/subagent/subagent/README.md +++ b/packages/subagent/subagent/README.md @@ -96,7 +96,7 @@ A request is validated against the provider's advertised capabilities, a durable The manager reserves a child identity, resolves the durable descriptor, creates (or cold-resumes) the child Agent, installs it in an Activation, and submits the prompt. Model-authored messages cross one parent/child edge through fixed Steer scheduling; browser human prompts choose Queue or best-effort Steer through an internal adapter, while other host protocols may retain Queue for distinct turns. A Session queue command admits a live subagent-owned Agent only from its own continuable descriptor. Settlement waits for Agent activity to finish, an empty Inbox, and no owned children, then flushes final Session state with admission open. Under the child lock, the manager revalidates the wake generation, Session sequence, Inbox, and owned children; the synchronous task entry of `Agent.runMaintenance()` claims the idle phase and closes the private subagent Inbox in the same JavaScript turn before handle disposal. An absent direct-child Activation cold-resumes from the persisted session. When a resident Activation settles, the manager tells the child's direct parent in the parent's own turn stream. -Successful local child creation appends a `subagent/catalog` fact to the parent Session. One-shot creation records it after the provider returns; continuable creation records it after initial inbox admission and before returning the child id. Failure releases the child without publishing a compensating catalog event. The `subagentCatalog` projection excludes fork-inherited facts and exposes a direct-child list through `projections.values.subagentCatalog` in Session observations and client snapshots. Its view preserves parent catalog event order in O(D) time for D facts. [The parent-catalog decision](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md) owns ordering, persistence costs, and alternatives. +Successful local child creation appends a `subagent/catalog` fact to the parent Session. One-shot creation records it after the provider returns; continuable creation records it after initial inbox admission and before returning the child id. Failure releases the child without publishing a compensating catalog event. A one-shot catalog append failure handles the run’s result rejection and preserves the catalog error; disposal failures are logged separately. The `subagentCatalog` projection excludes fork-inherited facts and exposes a direct-child list through `projections.values.subagentCatalog` in Session observations and client snapshots. Its view preserves parent catalog event order in O(D) time for D facts. [The parent-catalog decision](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md) owns ordering, persistence costs, and alternatives. ### Ownership and invariants diff --git a/packages/subagent/subagent/README.zh.md b/packages/subagent/subagent/README.zh.md index ff2b83ea41..2cbb4aded2 100644 --- a/packages/subagent/subagent/README.zh.md +++ b/packages/subagent/subagent/README.zh.md @@ -96,7 +96,7 @@ kind: "package-reference" 管理器预留 child 身份、解析持久化描述符、创建(或冷恢复)child、把它安装进 Activation 并提交提示词。模型编写的消息通过固定 Steer 调度跨一条 parent/child 边;浏览器人类 prompt 通过内部适配器选择 Queue 或 best-effort Steer,其他 host 协议仍可保留 Queue 以创建独立轮次。Session queue command 仅根据 child 自身的 continuable descriptor 准入在线 subagent-owned Agent。Settlement 会等待 Agent 活动结束、Inbox 为空且没有所拥有子级,再在准入开放时 flush 最终 Session 状态。管理器随后在 child lock 内重新验证 wake generation、Session 序号、Inbox 与所拥有子级;`Agent.runMaintenance()` 的同步 task 入口会占用 idle 阶段,并在同一个 JavaScript turn 内关闭私有 subagent Inbox,然后才 dispose handle。直接 child 不存在 Activation 时会从持久化会话冷恢复。当驻留 Activation 结算时,管理器会在 parent 自身的轮次流中告知该 child 的直接 parent。 -本地子级创建成功时,父 Session 追加一条 `subagent/catalog` 事实。一次性创建在 provider 返回后记录;可继续创建在初始 inbox 准入后、返回子级 id 前记录。失败会释放子级,不发布补偿性目录事件。`subagentCatalog` projection 排除 fork 继承的事实,通过 Session 观察和客户端快照中的 `projections.values.subagentCatalog` 暴露直接子级列表。其视图对 D 条事实以 O(D) 时间保留父目录事件顺序。[父目录决策](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md) 说明排序、持久化成本和替代方案。 +本地子级创建成功时,父 Session 追加一条 `subagent/catalog` 事实。一次性创建在 provider 返回后记录;可继续创建在初始 inbox 准入后、返回子级 id 前记录。失败会释放子级,不发布补偿性目录事件。一次性目录追加失败时会处理 run 的结果拒绝,并保留目录错误;资源释放失败会单独记录。`subagentCatalog` projection 排除 fork 继承的事实,通过 Session 观察和客户端快照中的 `projections.values.subagentCatalog` 暴露直接子级列表。其视图对 D 条事实以 O(D) 时间保留父目录事件顺序。[父目录决策](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md) 说明排序、持久化成本和替代方案。 ### 所有权与不变式 diff --git a/packages/subagent/subagent/src/index.ts b/packages/subagent/subagent/src/index.ts index 40af1dfed4..8a91e06e56 100644 --- a/packages/subagent/subagent/src/index.ts +++ b/packages/subagent/subagent/src/index.ts @@ -547,6 +547,8 @@ export class SubagentRuntime extends TypertRemoteService { * fulfills; a rejection therefore has no run for the caller to dispose and * emits no run lifecycle events. Post-publication turn and infrastructure * failures settle through the returned run. + * A catalog append failure disposes the run and handles its result rejection; + * the caller receives the catalog error even if disposal also fails. * @param name - the provider to use. * @param request - child label, prompt, parent, signal, and optional capabilities. * @returns the published holder-owned run. @@ -568,6 +570,8 @@ export class SubagentRuntime extends TypertRemoteService { try { establishCatalogChild(request.parent.session, child.header, descriptor) } catch (error: unknown) { + // No caller receives this run; the catalog error owns the failed start. + void run.result.catch(() => undefined) try { await run.dispose() } catch (cleanupError: unknown) { diff --git a/packages/subagent/subagent/tests/service.spec.ts b/packages/subagent/subagent/tests/service.spec.ts index 899f34eb9b..3de6e83193 100644 --- a/packages/subagent/subagent/tests/service.spec.ts +++ b/packages/subagent/subagent/tests/service.spec.ts @@ -1,4 +1,4 @@ -import { describe, expect, expectTypeOf, it, vi } from 'vitest' +import { describe, expect, expectTypeOf, it, onTestFinished, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import { type Agent } from '@deepseek-ai/dsh-agent' @@ -270,14 +270,22 @@ describe('SubagentRuntime', () => { expect(lifecycle).not.toHaveBeenCalled() }) - it('quiesces a local run when the parent catalog append fails', async () => { - const { subagents } = await service() + it.each([false, true])('handles a rejected local result after catalog failure (disposal fails: %s)', async (failsDisposal) => { + const { ctx, subagents } = await service() + onTestFinished(() => ctx.fiber.dispose()) const parentSession = Session.create(SessionId('catalog-parent')) const childSession = Session.create(SessionId('catalog-child')) const parent = { id: parentSession.id, session: parentSession } as Agent const localAgent = { id: childSession.id, session: childSession } as Agent + const result = Promise.withResolvers() const cleanupFailure = new Error('dispose also failed') - const dispose = vi.fn().mockRejectedValue(cleanupFailure) + const warnings = vi.spyOn(ctx.logger, 'warn') + const dispose = vi.fn(async () => { + result.reject(new Error('run infrastructure failed')) + // Cross Node's unhandled-rejection checkpoint while disposal is pending. + await new Promise(resolve => setImmediate(resolve)) + if (failsDisposal) throw cleanupFailure + }) subagents.registerProvider({ name: 'catalog-failure', capabilities: NO_CAPS, @@ -285,7 +293,7 @@ describe('SubagentRuntime', () => { start: () => Promise.resolve({ id: childSession.id, localAgent, - result: Promise.resolve({ output: [], stopReason: 'completed' }), + result: result.promise, dispose, }), }) @@ -298,6 +306,7 @@ describe('SubagentRuntime', () => { .rejects.toBe(catalogFailure) expect(append).toHaveBeenCalledOnce() expect(dispose).toHaveBeenCalledOnce() + expect(warnings).toHaveBeenCalledTimes(failsDisposal ? 1 : 0) }) it('emits an enriched end event and maps result rejection to error telemetry', async () => { From 661135fe2930464481496d8b4859e7cbf5d324ef Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Tue, 8 Sep 2026 17:53:17 +0800 Subject: [PATCH 26/58] fix(subagent): reject invalid catalogs and preserve snapshot event order --- ...01-parent-owned-subagent-catalog.i18n.yaml | 4 +- ...026-09-01-parent-owned-subagent-catalog.md | 4 +- ...-09-01-parent-owned-subagent-catalog.zh.md | 4 +- packages/subagent/subagent/README.i18n.yaml | 4 +- packages/subagent/subagent/README.md | 2 +- packages/subagent/subagent/README.zh.md | 2 +- packages/subagent/subagent/src/catalog.ts | 12 ++--- .../subagent/subagent/tests/catalog.spec.ts | 40 ++++++++++------- .../session-snapshot/README.i18n.yaml | 4 +- .../test-support/session-snapshot/README.md | 2 +- .../session-snapshot/README.zh.md | 2 +- .../session-snapshot/src/normalize.ts | 45 +++---------------- .../session-snapshot/tests/normalize.spec.ts | 16 +++---- snapshots/session/headless.snapshot.ts | 12 ++++- 14 files changed, 68 insertions(+), 85 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml index 284744c942..2cca303545 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md -2026-09-01-parent-owned-subagent-catalog.md: d4b6a20dfbceb30e3f7e5dc827895a25f823b59a -2026-09-01-parent-owned-subagent-catalog.zh.md: d4a394aee3ee1b9f86aff5872657c37acca93852 +2026-09-01-parent-owned-subagent-catalog.md: 3cfc513d70becf82cf5971be03aa6499de162a6f +2026-09-01-parent-owned-subagent-catalog.zh.md: ef3f099156703dd18e57c8e11e28ddbe7c9b5e8e diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md index d4b6a20dfb..3cfc513d70 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md @@ -12,7 +12,7 @@ The child descriptor remains necessary for recovery and composition, but it cann ## Decision -The parent Session's required `subagent/catalog` events are the persistent authority for direct-child discovery. Each event is one successful creation fact containing `childId`, `childCreatedAt`, mode, and the mode-discriminated label. Remote one-shot runs without a local Session remain outside this catalog. +The parent Session's required `subagent/catalog` events are the persistent authority for direct-child discovery. Each event is one successful creation fact containing `childId`, `childCreatedAt`, mode, and the mode-discriminated label. Remote one-shot runs without a local Session remain outside this catalog. Invalid own facts, including unsupported payload versions, reject projection restoration because silently dropping a required fact would return an incomplete catalog. Creation publishes only successful facts. A one-shot run appends the catalog event after its provider returns a local child and before the run reaches its caller. A continuable run admits the initial prompt, appends the catalog event, then returns the child id. If admission or catalog append fails, creation fails and releases the activation; there is no compensating catalog event or rollback protocol. @@ -22,7 +22,7 @@ The registered `subagentCatalog` projection materializes the parent facts. It st Fork isolation uses the exact `Session.inheritedEventCount` supplied to projection initialization. The fold ignores `subagent/catalog` events below that offset. The state stores the inherited offset but not each event seq because acceptance is decided during folding. -Snapshot normalizers zero `childCreatedAt` because it originates from the process clock. The TypeScript normalizer also sorts adjacent catalog facts by distinct child id because parallel successful creations may append in either order. Non-catalog events remain ordering barriers. Source-event references are remapped to the sorted positions so each citation retains its original fact. +Snapshot normalizers zero `childCreatedAt` because it originates from the process clock. Event order and source-event references remain intact: adjacent facts can come from sequential creation, so adjacency does not establish commutativity. ## Alternatives considered diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md index d4a394aee3..ef3f099156 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md @@ -12,7 +12,7 @@ child descriptor 对恢复与 composition 仍然必要,但它不能作为 disc ## 决策 -parent Session 的 required `subagent/catalog` 事件是直接 child discovery 的持久化权威。每个事件都是一条成功创建事实,包含 `childId`、`childCreatedAt`、mode 与按 mode 区分的 label。没有本地 Session 的远程 one-shot run 不进入该目录。 +parent Session 的 required `subagent/catalog` 事件是直接 child discovery 的持久化权威。每个事件都是一条成功创建事实,包含 `childId`、`childCreatedAt`、mode 与按 mode 区分的 label。没有本地 Session 的远程 one-shot run 不进入该目录。无效的自身 fact(包括不支持的 payload 版本)会使 projection 恢复失败,因为静默丢弃 required fact 会返回不完整的目录。 创建只发布成功事实。one-shot run 在 provider 返回本地 child 后、run 到达调用方前追加目录事件。continuable run 先准入初始 prompt,再追加目录事件,最后返回 child id。准入或目录追加失败时,创建失败并释放 activation;不存在补偿目录事件或 rollback 协议。 @@ -22,7 +22,7 @@ child header 与 `subagent/descriptor` 继续拥有恢复与 composition 权威 fork 隔离使用 projection 初始化时提供的精确 `Session.inheritedEventCount`。fold 忽略该 offset 之前的 `subagent/catalog` 事件。state 保存 inherited offset,但不保存每条 event seq,因为接受判定已在 fold 时完成。 -snapshot normalizer 会把 `childCreatedAt` 归零,因为它来自 process clock。TypeScript normalizer 还会按不同 child id 排序相邻 catalog fact,因为并行成功创建的 append 顺序可能不同。非 catalog event 继续作为顺序 barrier。来源事件引用会重映射到排序后的位置,使每个引用仍指向原来的事实。 +snapshot normalizer 会把 `childCreatedAt` 归零,因为它来自 process clock。事件顺序与来源事件引用保持不变:相邻 fact 也可能来自顺序创建,因此相邻关系不能证明可交换性。 ## 考虑过的替代方案 diff --git a/packages/subagent/subagent/README.i18n.yaml b/packages/subagent/subagent/README.i18n.yaml index 8962207eb9..c0ea2f21c9 100644 --- a/packages/subagent/subagent/README.i18n.yaml +++ b/packages/subagent/subagent/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/subagent/subagent/README.md -README.md: a37f8d476383185dd6d7a7cbdffb1b577f410328 -README.zh.md: 2cbb4aded2241a822ffe919f8b9baab0ffc3775a +README.md: 96bfe3ffcf097f4b8e8326478c9840632801233d +README.zh.md: b692806732fc6c479877881f32a904f663fa81b5 diff --git a/packages/subagent/subagent/README.md b/packages/subagent/subagent/README.md index a37f8d4763..96bfe3ffcf 100644 --- a/packages/subagent/subagent/README.md +++ b/packages/subagent/subagent/README.md @@ -96,7 +96,7 @@ A request is validated against the provider's advertised capabilities, a durable The manager reserves a child identity, resolves the durable descriptor, creates (or cold-resumes) the child Agent, installs it in an Activation, and submits the prompt. Model-authored messages cross one parent/child edge through fixed Steer scheduling; browser human prompts choose Queue or best-effort Steer through an internal adapter, while other host protocols may retain Queue for distinct turns. A Session queue command admits a live subagent-owned Agent only from its own continuable descriptor. Settlement waits for Agent activity to finish, an empty Inbox, and no owned children, then flushes final Session state with admission open. Under the child lock, the manager revalidates the wake generation, Session sequence, Inbox, and owned children; the synchronous task entry of `Agent.runMaintenance()` claims the idle phase and closes the private subagent Inbox in the same JavaScript turn before handle disposal. An absent direct-child Activation cold-resumes from the persisted session. When a resident Activation settles, the manager tells the child's direct parent in the parent's own turn stream. -Successful local child creation appends a `subagent/catalog` fact to the parent Session. One-shot creation records it after the provider returns; continuable creation records it after initial inbox admission and before returning the child id. Failure releases the child without publishing a compensating catalog event. A one-shot catalog append failure handles the run’s result rejection and preserves the catalog error; disposal failures are logged separately. The `subagentCatalog` projection excludes fork-inherited facts and exposes a direct-child list through `projections.values.subagentCatalog` in Session observations and client snapshots. Its view preserves parent catalog event order in O(D) time for D facts. [The parent-catalog decision](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md) owns ordering, persistence costs, and alternatives. +Successful local child creation appends a `subagent/catalog` fact to the parent Session. One-shot creation records it after the provider returns; continuable creation records it after initial inbox admission and before returning the child id. Failure releases the child without publishing a compensating catalog event. A one-shot catalog append failure handles the run’s result rejection and preserves the catalog error; disposal failures are logged separately. The `subagentCatalog` projection excludes fork-inherited facts and exposes a direct-child list through `projections.values.subagentCatalog` in Session observations and client snapshots. Invalid own catalog payloads, including unsupported versions, reject projection restoration. Its view preserves parent catalog event order in O(D) time for D facts. [The parent-catalog decision](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md) owns ordering, persistence costs, and alternatives. ### Ownership and invariants diff --git a/packages/subagent/subagent/README.zh.md b/packages/subagent/subagent/README.zh.md index 2cbb4aded2..b692806732 100644 --- a/packages/subagent/subagent/README.zh.md +++ b/packages/subagent/subagent/README.zh.md @@ -96,7 +96,7 @@ kind: "package-reference" 管理器预留 child 身份、解析持久化描述符、创建(或冷恢复)child、把它安装进 Activation 并提交提示词。模型编写的消息通过固定 Steer 调度跨一条 parent/child 边;浏览器人类 prompt 通过内部适配器选择 Queue 或 best-effort Steer,其他 host 协议仍可保留 Queue 以创建独立轮次。Session queue command 仅根据 child 自身的 continuable descriptor 准入在线 subagent-owned Agent。Settlement 会等待 Agent 活动结束、Inbox 为空且没有所拥有子级,再在准入开放时 flush 最终 Session 状态。管理器随后在 child lock 内重新验证 wake generation、Session 序号、Inbox 与所拥有子级;`Agent.runMaintenance()` 的同步 task 入口会占用 idle 阶段,并在同一个 JavaScript turn 内关闭私有 subagent Inbox,然后才 dispose handle。直接 child 不存在 Activation 时会从持久化会话冷恢复。当驻留 Activation 结算时,管理器会在 parent 自身的轮次流中告知该 child 的直接 parent。 -本地子级创建成功时,父 Session 追加一条 `subagent/catalog` 事实。一次性创建在 provider 返回后记录;可继续创建在初始 inbox 准入后、返回子级 id 前记录。失败会释放子级,不发布补偿性目录事件。一次性目录追加失败时会处理 run 的结果拒绝,并保留目录错误;资源释放失败会单独记录。`subagentCatalog` projection 排除 fork 继承的事实,通过 Session 观察和客户端快照中的 `projections.values.subagentCatalog` 暴露直接子级列表。其视图对 D 条事实以 O(D) 时间保留父目录事件顺序。[父目录决策](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md) 说明排序、持久化成本和替代方案。 +本地子级创建成功时,父 Session 追加一条 `subagent/catalog` 事实。一次性创建在 provider 返回后记录;可继续创建在初始 inbox 准入后、返回子级 id 前记录。失败会释放子级,不发布补偿性目录事件。一次性目录追加失败时会处理 run 的结果拒绝,并保留目录错误;资源释放失败会单独记录。`subagentCatalog` projection 排除 fork 继承的事实,通过 Session 观察和客户端快照中的 `projections.values.subagentCatalog` 暴露直接子级列表。无效的自身 catalog payload(包括不支持的版本)会使 projection 恢复失败。其视图对 D 条事实以 O(D) 时间保留父目录事件顺序。[父目录决策](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md) 说明排序、持久化成本和替代方案。 ### 所有权与不变式 diff --git a/packages/subagent/subagent/src/catalog.ts b/packages/subagent/subagent/src/catalog.ts index c68943a707..da41b91d2e 100644 --- a/packages/subagent/subagent/src/catalog.ts +++ b/packages/subagent/subagent/src/catalog.ts @@ -51,6 +51,8 @@ export interface SubagentCatalogState { readonly head?: CatalogChunk | undefined } +const CATALOG_CHUNK_CAPACITY = 64 + const sessionIdSchema = z.string() as unknown as z.ZodType const oneShotCatalogSchema = z.object({ version: z.literal(SUBAGENT_CATALOG_VERSION), @@ -81,7 +83,7 @@ const viewSchema = z.array(z.union([ }), ])) as unknown as z.ZodType const chunkSchema: z.ZodType = z.lazy(() => z.object({ - facts: z.array(eventDataSchema).min(1).max(64), + facts: z.array(eventDataSchema).min(1).max(CATALOG_CHUNK_CAPACITY), previous: chunkSchema.optional(), }).strict()) const stateSchema: z.ZodType = z.object({ @@ -98,7 +100,7 @@ declare module '@deepseek-ai/dsh-session-projection/types' { /** Append one fact to the persistent chunk stack in constant bounded work. */ function appendFact(state: SubagentCatalogState, fact: SubagentCatalogEvent): SubagentCatalogState { const head = state.head - if (head === undefined || head.facts.length === 64) { + if (head === undefined || head.facts.length === CATALOG_CHUNK_CAPACITY) { return { ...state, head: { facts: [fact], ...head === undefined ? {} : { previous: head } } } } return { @@ -139,16 +141,14 @@ function subagentCatalogEntries(state: SubagentCatalogState): SubagentCatalogEnt return entries } -/** Parent-owned direct-child catalog projection. */ +/** Parent-owned direct-child catalog projection; invalid own facts reject restoration. */ export const subagentCatalogProjectionDefinition = { key: 'subagentCatalog', stateSchema, init: (_header: SessionHeader, inheritedEventCount: SessionLogOffset) => ({ inheritedEventCount }), apply: (state, event: SessionEvent) => { if (event.type !== 'subagent/catalog' || event.seq < state.inheritedEventCount) return state - const parsed = eventDataSchema.safeParse(event.data) - if (!parsed.success) return state - return appendFact(state, parsed.data) + return appendFact(state, eventDataSchema.parse(event.data)) }, stateVersion: 1, wire: { viewSchema, view: subagentCatalogEntries }, diff --git a/packages/subagent/subagent/tests/catalog.spec.ts b/packages/subagent/subagent/tests/catalog.spec.ts index 6d260840d7..cfd51813fe 100644 --- a/packages/subagent/subagent/tests/catalog.spec.ts +++ b/packages/subagent/subagent/tests/catalog.spec.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi } from 'vitest' +import { z } from 'zod' import { Context } from '@deepseek-ai/cordis' import SessionStore from '@deepseek-ai/dsh-session' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' @@ -105,18 +106,7 @@ describe('subagent catalog projection', () => { const events: SessionEvent[] = [ fact(0, 'child-b', 1, { mode: 'one-shot' }), fact(1, 'child-a', 1, { mode: 'one-shot', label: 'once' }), - { - type: 'subagent/catalog', - seq: SessionSeq(2), - time: 0, - data: { - version: 0, - childId: SessionId('child-c'), - childCreatedAt: 2, - mode: 'continuable', - }, - } as unknown as SessionEvent, - fact(3, 'child-d', 3, { mode: 'continuable', label: 'later' }), + fact(2, 'child-d', 3, { mode: 'continuable', label: 'later' }), ] const state = fold(events) @@ -134,7 +124,7 @@ describe('subagent catalog projection', () => { ]) }) - it('ignores unrelated, inherited, and malformed events without changing state', () => { + it('ignores unrelated and inherited events without changing state', () => { const seededHeader = { ...header, isSeeded: true } const initial = subagentCatalogProjectionDefinition.init(seededHeader, SessionLogOffset(2)) const unrelated = subagentCatalogProjectionDefinition.apply(initial, { @@ -144,12 +134,28 @@ describe('subagent catalog projection', () => { unrelated, fact(1, 'inherited', 1, { mode: 'one-shot' }), ) - const malformed = subagentCatalogProjectionDefinition.apply(inherited, { - type: 'subagent/catalog', seq: SessionSeq(2), time: 0, data: { version: 9 }, - } as unknown as SessionEvent) expect(unrelated).toBe(initial) expect(inherited).toBe(initial) - expect(malformed).toBe(initial) + }) + + it.each([ + { version: 9, childId: 'child', childCreatedAt: 0, mode: 'one-shot' }, + { version: 0, childId: 'child', childCreatedAt: 0, mode: 'continuable' }, + { version: 0, childId: 'child', childCreatedAt: -1, mode: 'one-shot' }, + ])('refuses to restore a catalog containing an invalid fact: %j', async (data) => { + const ctx = new Context() + try { + await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) + ctx.sessionProjections.register(subagentCatalogProjectionDefinition) + const events = [ + fact(0, 'valid-child', 0, { mode: 'one-shot' }), + { type: 'subagent/catalog', seq: SessionSeq(1), time: 0, data } as unknown as SessionEvent, + ] + expect(() => ctx.sessionProjections.restore({}, events, SessionLogOffset(0), header, SessionLogOffset(0))).toThrow(z.ZodError) + } finally { + await ctx.fiber.dispose() + } }) }) diff --git a/packages/test-support/session-snapshot/README.i18n.yaml b/packages/test-support/session-snapshot/README.i18n.yaml index 9f145f4ecb..8cc6afcbb6 100644 --- a/packages/test-support/session-snapshot/README.i18n.yaml +++ b/packages/test-support/session-snapshot/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/test-support/session-snapshot/README.md -README.md: 55580faef36cd99b2179371e50b8a8ac680056c6 -README.zh.md: 9c23a09a75e3ba37305a3209098039623011a0f3 +README.md: c8d2ecc7c9d187006b711fdf4efd1ca205773cd9 +README.zh.md: 20fea4e4351a857908d5a9d9f78c1f80131c0ada diff --git a/packages/test-support/session-snapshot/README.md b/packages/test-support/session-snapshot/README.md index 55580faef3..c8d2ecc7c9 100644 --- a/packages/test-support/session-snapshot/README.md +++ b/packages/test-support/session-snapshot/README.md @@ -70,7 +70,7 @@ defineAcpSnapshotSuite({ Each recorded-session directory carries a closed `snapshot.yml` manifest plus canonical parent and contiguous child roles. Parent filenames are `session[.vN].jsonl`; children are `session.[.vN].jsonl`; v0 omits `.v0`, positive versions use lowercase `.vN`, and every filename agrees with its header. A role may retain older generations, but the harness selects the numerically highest one. An owning manifest may declare `sessionFormat.version` plus one or more closed `coverage` names to keep that historical generation as an explicit migration fixture; absence tracks the current writer. The manifest also names the scenario, shipped profile, composition/header class, recording source, and only the replay, platform, permission, environment, workspace, or input facts the completed Session cannot reconstruct. Storage guards check tool results and portable paths in every selected parent and child role. Prompt/schema scrubbing, message identity, and prompt-before-request ordering apply to current generations; retained predecessors keep their historical representation. The adapter registers expected-output, Session-log, and optional `workspace.expected/` comparisons; guards reject orphan directories, missing roles, noncanonical names, absolute paths, malformed manifests, and platform-specific separators. -`normalizeSessionSnapshot` retains the complete Session header and event payloads but omits top-level `seq`/`time` envelopes from committed fixtures after normalizing paths and scrubbing system-prompt text and tool schemas; it also normalizes embedded stream clocks and historical packed-row `seq0`/`time0` envelopes, and catalog child-creation clocks. Replay synthesizes the top-level envelopes in memory, while runtime persistence continues to write complete logs. Multi-session comparison validates expected and harvested logs through the strict build-static Session format catalog before identity redaction and normalization; source filenames cannot alter format validation. A retained historical replay input is not the native V3 writer-output oracle: structural migration preserves request meaning but can produce a different event layout. Normalization preserves unexpected request-header fields, including `system`, so regressions remain visible. Versionless protocol-adapter unit fixtures remain outside the released Session format corpus. Current v3 fixtures use one row per event; retained v0/v1 fixtures may use canonical packed rows. The [temporary repository migrator](../../../scripts/migrate-packed-session-fixtures.ts) (`pnpm run migrate:packed-session-fixtures`) rewrites older historical layouts, and its [removal proposal](../../../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.md) owns its deletion. +`normalizeSessionSnapshot` retains the complete Session header and event payloads but omits top-level `seq`/`time` envelopes from committed fixtures after normalizing paths and scrubbing system-prompt text and tool schemas; it also normalizes embedded stream clocks and historical packed-row `seq0`/`time0` envelopes, and catalog child-creation clocks. Event order and source-event references are preserved. Replay synthesizes the top-level envelopes in memory, while runtime persistence continues to write complete logs. Multi-session comparison validates expected and harvested logs through the strict build-static Session format catalog before identity redaction and normalization; source filenames cannot alter format validation. A retained historical replay input is not the native V3 writer-output oracle: structural migration preserves request meaning but can produce a different event layout. Normalization preserves unexpected request-header fields, including `system`, so regressions remain visible. Versionless protocol-adapter unit fixtures remain outside the released Session format corpus. Current v3 fixtures use one row per event; retained v0/v1 fixtures may use canonical packed rows. The [temporary repository migrator](../../../scripts/migrate-packed-session-fixtures.ts) (`pnpm run migrate:packed-session-fixtures`) rewrites older historical layouts, and its [removal proposal](../../../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.md) owns its deletion. Spill scenarios save through the real local provider under a private temporary root. Their fixture adapter exposes fixed-length logical locators and maps only this run’s saved locators back to real files for retrieval, preserving preview budgets without writing into a shared logical path. Known snapshot spill paths normalize to stable locator tokens, including paths quoted inside JSON omission notices with JSON-escaped Windows separators. Refresh extraction preserves the matched serialized path spelling for literal replacement. Normalization changes only the locator: saved byte lengths and omission counts remain comparison evidence. diff --git a/packages/test-support/session-snapshot/README.zh.md b/packages/test-support/session-snapshot/README.zh.md index 9c23a09a75..20fea4e435 100644 --- a/packages/test-support/session-snapshot/README.zh.md +++ b/packages/test-support/session-snapshot/README.zh.md @@ -70,7 +70,7 @@ defineAcpSnapshotSuite({ 每个已记录 Session 目录携带封闭的 `snapshot.yml` manifest,以及规范 parent 与连续 child 角色。parent 文件名是 `session[.vN].jsonl`;child 是 `session.[.vN].jsonl`;v0 省略 `.v0`,正版本使用小写 `.vN`,且每个文件名与其 header 一致。一个角色可以保留旧 generation,但 harness 会选择数值最高的一项。拥有 fixture 的 manifest 可以声明 `sessionFormat.version` 与一个或多个封闭 `coverage` 名称,把该历史 generation 保留为显式迁移 fixture;省略此字段时跟随当前 writer。manifest 还会指名场景、随附 profile、组合/header 类别、录制来源,以及已完成 Session 无法重建的 replay、平台、权限、环境、workspace 或输入事实。存储保护检查每个选定 parent 与 child 角色的工具结果和可移植路径。提示词/schema 擦除、消息身份及提示词先于请求的顺序检查适用于当前 generation;保留的前代维持其历史表示。适配器注册预期输出、Session 日志与可选 `workspace.expected/` 比较;保护会拒绝遗留目录、缺失角色、非规范名称、绝对路径、malformed manifest 与平台专用分隔符。 -`normalizeSessionSnapshot` 在规范化路径并擦除系统提示文本与工具 schema 后,会保留完整 Session header 与事件 payload,但从已提交 fixture 中省略顶层 `seq`/`time` envelope;它还会规范化嵌入式 stream clock 与历史 packed-row 的 `seq0`/`time0` envelope 与 catalog child 创建时钟。Replay 只在内存中合成顶层 envelope,而运行时持久化仍写入完整日志。多 Session 比较会先通过严格的构建期静态 Session 格式目录校验预期日志与收集日志,再进行身份脱敏与规范化;来源文件名不能改变格式校验。保留的历史 replay 输入不是原生 V3 writer 输出的比较基准:结构迁移保留请求含义,但可以产生不同的事件布局。归一化保留意外的 request-header 字段(包括 `system`),使回归保持可见。无版本的协议适配器单元测试 fixture 不属于已发布 Session 格式语料。当前 v3 fixture 每个事件占一行;保留的 v0/v1 fixture 可以使用规范 packed row。[临时仓库迁移器](../../../scripts/migrate-packed-session-fixtures.ts)(`pnpm run migrate:packed-session-fixtures`)会改写更旧的历史布局,由其[移除提案](../../../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.zh.md)负责删除该迁移器。 +`normalizeSessionSnapshot` 在规范化路径并擦除系统提示文本与工具 schema 后,会保留完整 Session header 与事件 payload,但从已提交 fixture 中省略顶层 `seq`/`time` envelope;它还会规范化嵌入式 stream clock 与历史 packed-row 的 `seq0`/`time0` envelope 与 catalog child 创建时钟。事件顺序与来源事件引用保持不变。Replay 只在内存中合成顶层 envelope,而运行时持久化仍写入完整日志。多 Session 比较会先通过严格的构建期静态 Session 格式目录校验预期日志与收集日志,再进行身份脱敏与规范化;来源文件名不能改变格式校验。保留的历史 replay 输入不是原生 V3 writer 输出的比较基准:结构迁移保留请求含义,但可以产生不同的事件布局。归一化保留意外的 request-header 字段(包括 `system`),使回归保持可见。无版本的协议适配器单元测试 fixture 不属于已发布 Session 格式语料。当前 v3 fixture 每个事件占一行;保留的 v0/v1 fixture 可以使用规范 packed row。[临时仓库迁移器](../../../scripts/migrate-packed-session-fixtures.ts)(`pnpm run migrate:packed-session-fixtures`)会改写更旧的历史布局,由其[移除提案](../../../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.zh.md)负责删除该迁移器。 Spill 场景通过真实本地 provider 保存到私有临时根目录。夹具适配器提供固定长度的逻辑定位符,并仅将本次运行已保存的定位符映射回实际文件以供检索,在不写入共享逻辑路径的情况下保留预览预算。已知的快照 spill 路径会规范化为稳定的定位信息 token,包括 JSON 省略通知中带引号、使用 JSON 转义 Windows 分隔符的路径。刷新提取会保留匹配路径的序列化写法,以便进行字面替换。规范化只改变定位信息:保存字节数与省略计数仍作为比较证据。 diff --git a/packages/test-support/session-snapshot/src/normalize.ts b/packages/test-support/session-snapshot/src/normalize.ts index fe4469e1c9..a6e5d3d341 100644 --- a/packages/test-support/session-snapshot/src/normalize.ts +++ b/packages/test-support/session-snapshot/src/normalize.ts @@ -33,31 +33,6 @@ function omitFixtureEnvelope(record: Record): void { delete record.time0 } -/** Return a catalog event's child id, or undefined for every ordering barrier. */ -function catalogChildId(event: { readonly type: string; readonly data?: unknown }): string | undefined { - if (event.type !== 'subagent/catalog' || event.data === null || typeof event.data !== 'object') return undefined - const data = event.data as { childId?: unknown } - return typeof data.childId === 'string' ? data.childId : undefined -} - -/** Sort commutative adjacent catalog facts without crossing another event. */ -function canonicalizeCatalogRuns(events: readonly T[]): T[] { - const canonical = [...events] - let start = 0 - while (start < canonical.length) { - if (catalogChildId(canonical[start] as T) === undefined) { - start += 1 - continue - } - let end = start + 1 - while (end < canonical.length && catalogChildId(canonical[end] as T) !== undefined) end += 1 - canonical.splice(start, end - start, ...canonical.slice(start, end).sort((left, right) => - (catalogChildId(left) as string).localeCompare(catalogChildId(right) as string))) - start = end - } - return canonical -} - /** A cwd-rooted path after volatile cwd replacement, through its last separator-delimited segment. */ const CWD_ROOTED_PATH_RE = /\{\{cwd\}\}(?:[\\/][^\s<>"'`]+)+/g const PATH_TAG_RE = /()([^<]*)(<\/path>)/g @@ -427,19 +402,10 @@ function projectSessionSnapshot(rawLog: string): string { const lines = rawLog.split('\n').filter(line => line.trim().length > 0) const header = lines.shift() as string - const original = lines.map((line) => { - return JSON.parse(line) as Record & { type: string } - }) - const records = canonicalizeCatalogRuns(original) - const positions = new Map(records.map((record, seq) => [record, seq])) - const sourcePositions = original.map(record => positions.get(record) as number) - const body = records.map((record) => { - const projected = { ...record } - if (Array.isArray(record.sourceEventSeqs)) { - projected.sourceEventSeqs = record.sourceEventSeqs.map((seq: number) => sourcePositions[seq] ?? seq) - } - omitFixtureEnvelope(projected) - return JSON.stringify(projected) + const body = lines.map((line) => { + const record = JSON.parse(line) as Record + omitFixtureEnvelope(record) + return JSON.stringify(record) }) return [header, ...body, ''].join('\n') } @@ -448,7 +414,8 @@ function projectSessionSnapshot(rawLog: string): string { * Normalize and project persisted session JSONL for a committed fixture. * This composes ordinary log normalization with request-header scrubbing and * persistence-envelope projection, then writes the v3 logical event stream as - * one record per event, independent of persistence flush boundaries. + * one record per event, independent of persistence flush boundaries. Event order + * and source-event references are preserved. * * @param rawLog - persisted or already-projected session JSONL. * @param ctx - the run's volatile values to scrub. diff --git a/packages/test-support/session-snapshot/tests/normalize.spec.ts b/packages/test-support/session-snapshot/tests/normalize.spec.ts index 339e190411..4f136fbcb5 100644 --- a/packages/test-support/session-snapshot/tests/normalize.spec.ts +++ b/packages/test-support/session-snapshot/tests/normalize.spec.ts @@ -621,7 +621,7 @@ describe('normalizeSessionSnapshot', () => { ].join('\n')) }) - it('orders adjacent catalog facts by child id', () => { + it('preserves adjacent catalog facts in parent event order', () => { const raw = [ JSON.stringify({ type: 'session', version: 0 }), JSON.stringify({ type: 'tool/call', data: { callId: 'parallel' } }), @@ -636,11 +636,11 @@ describe('normalizeSessionSnapshot', () => { JSON.stringify({ type: 'tool/result', data: { callId: 'parallel' } }), ].join('\n') + '\n' const normalized = normalizeSessionSnapshot(raw, ctx) - expect(normalized.indexOf('{{session:2}}')).toBeLessThan(normalized.indexOf('{{session:3}}')) + expect(normalized.indexOf('{{session:3}}')).toBeLessThan(normalized.indexOf('{{session:2}}')) expect(normalized).toContain('"childCreatedAt":0') }) - it('treats malformed catalog facts as ordering barriers', () => { + it('preserves malformed catalog payloads', () => { const raw = [ JSON.stringify({ type: 'session', version: 0 }), JSON.stringify({ @@ -658,9 +658,9 @@ describe('normalizeSessionSnapshot', () => { }) it.each([ - { sources: [0, 1], remapped: [0, 2] }, - { sources: [0, 2], remapped: [0, 1] }, - ])('preserves cited children when adjacent catalog facts change position: $sources', ({ sources, remapped }) => { + { sources: [0, 1] }, + { sources: [0, 2] }, + ])('preserves source references and catalog order: $sources', ({ sources }) => { const records = [ { type: 'session', version: 2 }, { type: 'tool/call', data: { callId: 'parallel' } }, @@ -672,9 +672,9 @@ describe('normalizeSessionSnapshot', () => { expect(normalized).toBe([ records[0], records[1], - { ...records[3], data: { ...records[3]?.data, childCreatedAt: 0 } }, { ...records[2], data: { ...records[2]?.data, childCreatedAt: 0 } }, - { ...records[4], sourceEventSeqs: remapped }, + { ...records[3], data: { ...records[3]?.data, childCreatedAt: 0 } }, + records[4], ].map(record => JSON.stringify(record)).join('\n') + '\n') expect(normalizeSessionSnapshot(normalized, ctx)).toBe(normalized) }) diff --git a/snapshots/session/headless.snapshot.ts b/snapshots/session/headless.snapshot.ts index b10a87b47f..fb5af64a62 100644 --- a/snapshots/session/headless.snapshot.ts +++ b/snapshots/session/headless.snapshot.ts @@ -176,11 +176,21 @@ async function persistedSessions(cwd: string): Promise { expect(assertPersistedSessionVersion(basename(file), content), `${file}: current writer`).toBe(SESSION_FORMAT_VERSION) return { content, header: headerOf(content) } })) + // Same-millisecond siblings bind to fixture roles by their parent's recorded + // discovery order; random persistence filenames do not identify child roles. + const catalogOrders = new Map(logs.map(log => [log.header.id, new Map( + records(log.content) + .filter(event => event.type === 'subagent/catalog') + .map((event, index) => [(event.data as JsonObject).childId, index]), + )])) return logs.sort((left, right) => { const leftChild = typeof left.header.parentSession === 'string' const rightChild = typeof right.header.parentSession === 'string' if (leftChild !== rightChild) return leftChild ? 1 : -1 - return Number(left.header.createdAt) - Number(right.header.createdAt) + const timeOrder = Number(left.header.createdAt) - Number(right.header.createdAt) + if (timeOrder !== 0 || left.header.parentSession !== right.header.parentSession) return timeOrder + const catalogOrder = catalogOrders.get(left.header.parentSession) + return (catalogOrder?.get(left.header.id) ?? Infinity) - (catalogOrder?.get(right.header.id) ?? Infinity) }) } From 9f69a7fb16114f6474302531c868320b61bb2a8b Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Tue, 8 Sep 2026 20:04:40 +0800 Subject: [PATCH 27/58] refactor(subagent): extract persistent chunked list utility --- ...01-parent-owned-subagent-catalog.i18n.yaml | 4 +- ...026-09-01-parent-owned-subagent-catalog.md | 4 +- ...-09-01-parent-owned-subagent-catalog.zh.md | 4 +- docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 1 + docs/config-catalog.zh.md | 1 + docs/module-graph.i18n.yaml | 4 +- docs/module-graph.md | 2 + docs/module-graph.zh.md | 2 + docs/persistence-catalog.i18n.yaml | 4 +- docs/persistence-catalog.md | 2 +- docs/persistence-catalog.zh.md | 2 +- packages/subagent/subagent/README.i18n.yaml | 4 +- packages/subagent/subagent/README.md | 2 +- packages/subagent/subagent/README.zh.md | 2 +- packages/subagent/subagent/package.json | 1 + packages/subagent/subagent/src/catalog.ts | 69 ++++---------- .../subagent/subagent/tests/service.spec.ts | 2 +- packages/subagent/subagent/tsconfig.json | 3 + packages/util/README.i18n.yaml | 4 +- packages/util/README.md | 1 + packages/util/README.zh.md | 1 + packages/util/chunked-list/README.i18n.yaml | 6 ++ packages/util/chunked-list/README.md | 93 +++++++++++++++++++ packages/util/chunked-list/README.zh.md | 93 +++++++++++++++++++ packages/util/chunked-list/package.json | 38 ++++++++ packages/util/chunked-list/src/index.ts | 58 ++++++++++++ .../chunked-list/tests/chunked-list.spec.ts | 59 ++++++++++++ packages/util/chunked-list/tsconfig.json | 11 +++ pnpm-lock.yaml | 13 +++ scripts/doc-standard.spec.ts | 1 + .../verify-package-readme-model-experience.ts | 1 + tsconfig.base.json | 1 + tsconfig.host.json | 1 + 34 files changed, 430 insertions(+), 68 deletions(-) create mode 100644 packages/util/chunked-list/README.i18n.yaml create mode 100644 packages/util/chunked-list/README.md create mode 100644 packages/util/chunked-list/README.zh.md create mode 100644 packages/util/chunked-list/package.json create mode 100644 packages/util/chunked-list/src/index.ts create mode 100644 packages/util/chunked-list/tests/chunked-list.spec.ts create mode 100644 packages/util/chunked-list/tsconfig.json diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml index 2cca303545..f6a34bbc6b 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md -2026-09-01-parent-owned-subagent-catalog.md: 3cfc513d70becf82cf5971be03aa6499de162a6f -2026-09-01-parent-owned-subagent-catalog.zh.md: ef3f099156703dd18e57c8e11e28ddbe7c9b5e8e +2026-09-01-parent-owned-subagent-catalog.md: b0eee83499f8f9f3b3e2be652037dc882e005c94 +2026-09-01-parent-owned-subagent-catalog.zh.md: 8985eb291c5dbfadb53eb3576dcd9306496aa983 diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md index 3cfc513d70..b0eee83499 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md @@ -18,7 +18,9 @@ Creation publishes only successful facts. A one-shot run appends the catalog eve The child header and `subagent/descriptor` remain authoritative for recovery and composition. An Activation and the exact parent relationship remain authoritative for authorization and delivery. Mode and label are snapshotted once and the same detached values reach the parent catalog fact and child descriptor. -The registered `subagentCatalog` projection materializes the parent facts. It stores facts in a persistent stack of 64-entry chunks, so an append copies at most the head chunk in bounded O(1) work. Materialization visits chunks from oldest to newest and preserves parent catalog event order in O(D) time for D facts. Concurrent creation is ordered by successful catalog append, independent of child timestamps and ids. A projection checkpoint clones the state once in O(D); projection-cache writes remain asynchronous and use the existing mandatory creation, turn-end, and disposal points. +The registered `subagentCatalog` projection materializes the parent facts. It delegates storage, append, iteration, and checkpoint validation to [`dsh-chunked-list`](../../../../packages/util/chunked-list/README.md), which stores facts in a persistent stack of 64-entry chunks, so an append copies at most the head chunk in bounded O(1) work. Materialization visits chunks from oldest to newest and preserves parent catalog event order in O(D) time for D facts. Concurrent creation is ordered by successful catalog append, independent of child timestamps and ids. A projection checkpoint clones the state once in O(D); projection-cache writes remain asynchronous and use the existing mandatory creation, turn-end, and disposal points. + +The utility owns chunk layout and its shared capacity constant; the catalog owns event validation, fork filtering, and row conversion. Catalog projection state version 2 stores generic chunk values, so the projection registry rebuilds incompatible caches from Session events. Session event payloads and public catalog rows retain their formats. Fork isolation uses the exact `Session.inheritedEventCount` supplied to projection initialization. The fold ignores `subagent/catalog` events below that offset. The state stores the inherited offset but not each event seq because acceptance is decided during folding. diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md index ef3f099156..8985eb291c 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md @@ -18,7 +18,9 @@ parent Session 的 required `subagent/catalog` 事件是直接 child discovery child header 与 `subagent/descriptor` 继续拥有恢复与 composition 权威。Activation 与精确 parent 关系继续拥有授权与投递权威。mode 与 label 只快照一次,同一份分离值写入 parent catalog fact 与 child descriptor。 -注册的 `subagentCatalog` projection 物化 parent fact。它以每块 64 项的持久 stack 保存事实,因此 append 最多复制 head chunk,以有界 O(1) 工作完成。materialization 从旧到新访问 chunk,对 D 条事实以 O(D) 时间保留父目录事件顺序。并发创建按目录成功追加的顺序排列,与 child 时间戳和 id 无关。projection checkpoint 以 O(D) 克隆 state;projection-cache 继续异步写入,并使用既有创建、turn-end 与 disposal 强制点。 +注册的 `subagentCatalog` projection 物化 parent fact。它将存储、追加、迭代和检查点校验交给 [`dsh-chunked-list`](../../../../packages/util/chunked-list/README.zh.md),后者以每块 64 项的持久 stack 保存事实,因此 append 最多复制 head chunk,以有界 O(1) 工作完成。materialization 从旧到新访问 chunk,对 D 条事实以 O(D) 时间保留父目录事件顺序。并发创建按目录成功追加的顺序排列,与 child 时间戳和 id 无关。projection checkpoint 以 O(D) 克隆 state;projection-cache 继续异步写入,并使用既有创建、turn-end 与 disposal 强制点。 + +工具库拥有分块布局及其共享容量常量;目录拥有事件校验、fork 过滤和目录行转换。目录 projection state 版本 2 保存通用块值,因此 projection registry 从 Session 事件重建不兼容的缓存。Session 事件载荷和公开目录行保持各自格式。 fork 隔离使用 projection 初始化时提供的精确 `Session.inheritedEventCount`。fold 忽略该 offset 之前的 `subagent/catalog` 事件。state 保存 inherited offset,但不保存每条 event seq,因为接受判定已在 fold 时完成。 diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 3957eee85a..c7b1711528 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-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/config-catalog.md -config-catalog.md: 9099ca894f0cab6fb8030c5ffced74cf7547df77 -config-catalog.zh.md: a9d7502de051857bb947361c1808a48ebcedb2ee +config-catalog.md: cd6449b29479dabfd9f686c7cd33f30c60d14739 +config-catalog.zh.md: 52e563119240e5fbdbffbb2aa0664d4a6abe3f2b diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 9099ca894f..cd6449b294 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -3539,6 +3539,7 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them. - `@deepseek-ai/dsh-atomic-write` ([`packages/util/atomic-write/src/index.ts`](../packages/util/atomic-write/src/index.ts)) - `@deepseek-ai/dsh-base` ([`packages/bundle/base/src/index.ts`](../packages/bundle/base/src/index.ts)) - `@deepseek-ai/dsh-brand` ([`packages/util/brand/src/index.ts`](../packages/util/brand/src/index.ts)) +- `@deepseek-ai/dsh-chunked-list` ([`packages/util/chunked-list/src/index.ts`](../packages/util/chunked-list/src/index.ts)) - `@deepseek-ai/dsh-client-store` ([`packages/client/store/src/index.ts`](../packages/client/store/src/index.ts)) - `@deepseek-ai/dsh-client-test-runtime` ([`packages/test-support/client-runtime/src/index.ts`](../packages/test-support/client-runtime/src/index.ts)) - `@deepseek-ai/dsh-client-ui-dockkit` ([`packages/client/ui-dockkit/src/index.ts`](../packages/client/ui-dockkit/src/index.ts)) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index a9d7502de0..52e5631192 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -3540,6 +3540,7 @@ export interface Config { - `@deepseek-ai/dsh-atomic-write`([`packages/util/atomic-write/src/index.ts`](../packages/util/atomic-write/src/index.ts)) - `@deepseek-ai/dsh-base`([`packages/bundle/base/src/index.ts`](../packages/bundle/base/src/index.ts)) - `@deepseek-ai/dsh-brand`([`packages/util/brand/src/index.ts`](../packages/util/brand/src/index.ts)) +- `@deepseek-ai/dsh-chunked-list`([`packages/util/chunked-list/src/index.ts`](../packages/util/chunked-list/src/index.ts)) - `@deepseek-ai/dsh-client-store`([`packages/client/store/src/index.ts`](../packages/client/store/src/index.ts)) - `@deepseek-ai/dsh-client-test-runtime`([`packages/test-support/client-runtime/src/index.ts`](../packages/test-support/client-runtime/src/index.ts)) - `@deepseek-ai/dsh-client-ui-dockkit`([`packages/client/ui-dockkit/src/index.ts`](../packages/client/ui-dockkit/src/index.ts)) diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index ba1dd07a2f..594186cd2f 100644 --- a/docs/module-graph.i18n.yaml +++ b/docs/module-graph.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/module-graph.md -module-graph.md: 233ae6b3fb49b07a2f7237aef2674c782df07720 -module-graph.zh.md: 64c5aa749aa76631c308d4b724ed1cd356068019 +module-graph.md: 9d229ba5d25fbf9f4763a96667150dd2103d0231 +module-graph.zh.md: 830f777dde991098ad2f4279a879ecf9cb144900 diff --git a/docs/module-graph.md b/docs/module-graph.md index 233ae6b3fb..9d229ba5d2 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -10,6 +10,7 @@ flowchart TD subgraph group_util["packages/util"] pkg_atomic_write["atomic-write"] pkg_brand["brand"] + pkg_chunked_list["chunked-list"] pkg_deque["deque"] pkg_home_paths["home-paths"] pkg_http_proxy["http-proxy"] @@ -1172,6 +1173,7 @@ flowchart TD | --- | --- | --- | | [`atomic-write`](../packages/util/atomic-write) | `util` | — | | [`brand`](../packages/util/brand) | `util` | — | +| [`chunked-list`](../packages/util/chunked-list) | `util` | — | | [`deque`](../packages/util/deque) | `util` | — | | [`home-paths`](../packages/util/home-paths) | `util` | — | | [`http-proxy`](../packages/util/http-proxy) | `util` | — | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index 64c5aa749a..830f777dde 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -12,6 +12,7 @@ flowchart TD subgraph group_util["packages/util"] pkg_atomic_write["atomic-write"] pkg_brand["brand"] + pkg_chunked_list["chunked-list"] pkg_deque["deque"] pkg_home_paths["home-paths"] pkg_http_proxy["http-proxy"] @@ -1174,6 +1175,7 @@ flowchart TD | --- | --- | --- | | [`atomic-write`](../packages/util/atomic-write) | `util` | — | | [`brand`](../packages/util/brand) | `util` | — | +| [`chunked-list`](../packages/util/chunked-list) | `util` | — | | [`deque`](../packages/util/deque) | `util` | — | | [`home-paths`](../packages/util/home-paths) | `util` | — | | [`http-proxy`](../packages/util/http-proxy) | `util` | — | diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index dc6e192b50..147c3a869a 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-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/persistence-catalog.md -persistence-catalog.md: d9a9449a4369988d4031d921c4124d8cb74f8791 -persistence-catalog.zh.md: 538b20e430c8d061e7f443d9a412443e81851415 +persistence-catalog.md: 06242185a988d6908be0c66e13a45af4e4974e1c +persistence-catalog.zh.md: bc6f4f623e60962df7adfd3771d4bf95b1c1ff3b diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index d9a9449a43..06242185a9 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -773,7 +773,7 @@ Source: [`packages/core/session/src/types.ts:287`](../packages/core/session/src/ 'subagent/catalog': SubagentCatalogEvent ``` -Source: [`packages/subagent/subagent/src/catalog.ts:38`](../packages/subagent/subagent/src/catalog.ts) +Source: [`packages/subagent/subagent/src/catalog.ts:40`](../packages/subagent/subagent/src/catalog.ts) diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index 538b20e430..bc6f4f623e 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -775,7 +775,7 @@ export type SessionEvent = { 'subagent/catalog': SubagentCatalogEvent ``` -来源:[`packages/subagent/subagent/src/catalog.ts:38`](../packages/subagent/subagent/src/catalog.ts) +来源:[`packages/subagent/subagent/src/catalog.ts:40`](../packages/subagent/subagent/src/catalog.ts) diff --git a/packages/subagent/subagent/README.i18n.yaml b/packages/subagent/subagent/README.i18n.yaml index c0ea2f21c9..9a6bff0b24 100644 --- a/packages/subagent/subagent/README.i18n.yaml +++ b/packages/subagent/subagent/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/subagent/subagent/README.md -README.md: 96bfe3ffcf097f4b8e8326478c9840632801233d -README.zh.md: b692806732fc6c479877881f32a904f663fa81b5 +README.md: 5f9668fd7a14cdfca33ab72a217c07372a24502f +README.zh.md: 563ed06237088c4bc250ca3c4f5a863b6ffe8239 diff --git a/packages/subagent/subagent/README.md b/packages/subagent/subagent/README.md index 96bfe3ffcf..5f9668fd7a 100644 --- a/packages/subagent/subagent/README.md +++ b/packages/subagent/subagent/README.md @@ -96,7 +96,7 @@ A request is validated against the provider's advertised capabilities, a durable The manager reserves a child identity, resolves the durable descriptor, creates (or cold-resumes) the child Agent, installs it in an Activation, and submits the prompt. Model-authored messages cross one parent/child edge through fixed Steer scheduling; browser human prompts choose Queue or best-effort Steer through an internal adapter, while other host protocols may retain Queue for distinct turns. A Session queue command admits a live subagent-owned Agent only from its own continuable descriptor. Settlement waits for Agent activity to finish, an empty Inbox, and no owned children, then flushes final Session state with admission open. Under the child lock, the manager revalidates the wake generation, Session sequence, Inbox, and owned children; the synchronous task entry of `Agent.runMaintenance()` claims the idle phase and closes the private subagent Inbox in the same JavaScript turn before handle disposal. An absent direct-child Activation cold-resumes from the persisted session. When a resident Activation settles, the manager tells the child's direct parent in the parent's own turn stream. -Successful local child creation appends a `subagent/catalog` fact to the parent Session. One-shot creation records it after the provider returns; continuable creation records it after initial inbox admission and before returning the child id. Failure releases the child without publishing a compensating catalog event. A one-shot catalog append failure handles the run’s result rejection and preserves the catalog error; disposal failures are logged separately. The `subagentCatalog` projection excludes fork-inherited facts and exposes a direct-child list through `projections.values.subagentCatalog` in Session observations and client snapshots. Invalid own catalog payloads, including unsupported versions, reject projection restoration. Its view preserves parent catalog event order in O(D) time for D facts. [The parent-catalog decision](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md) owns ordering, persistence costs, and alternatives. +Successful local child creation appends a `subagent/catalog` fact to the parent Session. One-shot creation records it after the provider returns; continuable creation records it after initial inbox admission and before returning the child id. Failure releases the child without publishing a compensating catalog event. A one-shot catalog append failure handles the run’s result rejection and preserves the catalog error; disposal failures are logged separately. The `subagentCatalog` projection excludes fork-inherited facts and exposes a direct-child list through `projections.values.subagentCatalog` in Session observations and client snapshots. Invalid own catalog payloads, including unsupported versions, reject projection restoration. Its immutable storage and checkpoint validation use [`dsh-chunked-list`](../../util/chunked-list/README.md). Its view preserves parent catalog event order in O(D) time for D facts. [The parent-catalog decision](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md) owns ordering, persistence costs, and alternatives. ### Ownership and invariants diff --git a/packages/subagent/subagent/README.zh.md b/packages/subagent/subagent/README.zh.md index b692806732..563ed06237 100644 --- a/packages/subagent/subagent/README.zh.md +++ b/packages/subagent/subagent/README.zh.md @@ -96,7 +96,7 @@ kind: "package-reference" 管理器预留 child 身份、解析持久化描述符、创建(或冷恢复)child、把它安装进 Activation 并提交提示词。模型编写的消息通过固定 Steer 调度跨一条 parent/child 边;浏览器人类 prompt 通过内部适配器选择 Queue 或 best-effort Steer,其他 host 协议仍可保留 Queue 以创建独立轮次。Session queue command 仅根据 child 自身的 continuable descriptor 准入在线 subagent-owned Agent。Settlement 会等待 Agent 活动结束、Inbox 为空且没有所拥有子级,再在准入开放时 flush 最终 Session 状态。管理器随后在 child lock 内重新验证 wake generation、Session 序号、Inbox 与所拥有子级;`Agent.runMaintenance()` 的同步 task 入口会占用 idle 阶段,并在同一个 JavaScript turn 内关闭私有 subagent Inbox,然后才 dispose handle。直接 child 不存在 Activation 时会从持久化会话冷恢复。当驻留 Activation 结算时,管理器会在 parent 自身的轮次流中告知该 child 的直接 parent。 -本地子级创建成功时,父 Session 追加一条 `subagent/catalog` 事实。一次性创建在 provider 返回后记录;可继续创建在初始 inbox 准入后、返回子级 id 前记录。失败会释放子级,不发布补偿性目录事件。一次性目录追加失败时会处理 run 的结果拒绝,并保留目录错误;资源释放失败会单独记录。`subagentCatalog` projection 排除 fork 继承的事实,通过 Session 观察和客户端快照中的 `projections.values.subagentCatalog` 暴露直接子级列表。无效的自身 catalog payload(包括不支持的版本)会使 projection 恢复失败。其视图对 D 条事实以 O(D) 时间保留父目录事件顺序。[父目录决策](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md) 说明排序、持久化成本和替代方案。 +本地子级创建成功时,父 Session 追加一条 `subagent/catalog` 事实。一次性创建在 provider 返回后记录;可继续创建在初始 inbox 准入后、返回子级 id 前记录。失败会释放子级,不发布补偿性目录事件。一次性目录追加失败时会处理 run 的结果拒绝,并保留目录错误;资源释放失败会单独记录。`subagentCatalog` projection 排除 fork 继承的事实,通过 Session 观察和客户端快照中的 `projections.values.subagentCatalog` 暴露直接子级列表。无效的自身 catalog payload(包括不支持的版本)会使 projection 恢复失败。其不可变存储和检查点校验使用 [`dsh-chunked-list`](../../util/chunked-list/README.zh.md)。其视图对 D 条事实以 O(D) 时间保留父目录事件顺序。[父目录决策](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md) 说明排序、持久化成本和替代方案。 ### 所有权与不变式 diff --git a/packages/subagent/subagent/package.json b/packages/subagent/subagent/package.json index 306236896d..22e000973f 100644 --- a/packages/subagent/subagent/package.json +++ b/packages/subagent/subagent/package.json @@ -54,6 +54,7 @@ "license": "MIT", "dependencies": { "@deepseek-ai/dsh-brand": "workspace:^", + "@deepseek-ai/dsh-chunked-list": "workspace:^", "@deepseek-ai/dsh-util-values": "workspace:^", "zod": "^4.4.3" }, diff --git a/packages/subagent/subagent/src/catalog.ts b/packages/subagent/subagent/src/catalog.ts index da41b91d2e..3ec29bbd2f 100644 --- a/packages/subagent/subagent/src/catalog.ts +++ b/packages/subagent/subagent/src/catalog.ts @@ -5,6 +5,8 @@ */ import { z } from 'zod' +import { appendChunkedList, chunkedListSchema, iterateChunkedList } from '@deepseek-ai/dsh-chunked-list' +import type { ChunkedList } from '@deepseek-ai/dsh-chunked-list' import type { Session, SessionEvent, @@ -39,20 +41,12 @@ declare module '@deepseek-ai/dsh-session/types' { } } -/** A fixed-size persistent stack node; newest facts occupy the head chunk. */ -interface CatalogChunk { - readonly facts: readonly SubagentCatalogEvent[] - readonly previous?: CatalogChunk | undefined -} - /** Host fold state for one parent catalog. */ export interface SubagentCatalogState { readonly inheritedEventCount: SessionLogOffset - readonly head?: CatalogChunk | undefined + readonly head?: ChunkedList | undefined } -const CATALOG_CHUNK_CAPACITY = 64 - const sessionIdSchema = z.string() as unknown as z.ZodType const oneShotCatalogSchema = z.object({ version: z.literal(SUBAGENT_CATALOG_VERSION), @@ -82,13 +76,9 @@ const viewSchema = z.array(z.union([ createdAt: continuableCatalogSchema.shape.childCreatedAt, }), ])) as unknown as z.ZodType -const chunkSchema: z.ZodType = z.lazy(() => z.object({ - facts: z.array(eventDataSchema).min(1).max(CATALOG_CHUNK_CAPACITY), - previous: chunkSchema.optional(), -}).strict()) const stateSchema: z.ZodType = z.object({ inheritedEventCount: z.number().int().nonnegative() as unknown as z.ZodType, - head: chunkSchema.optional(), + head: chunkedListSchema(eventDataSchema).optional(), }).strict() declare module '@deepseek-ai/dsh-session-projection/types' { @@ -97,46 +87,27 @@ declare module '@deepseek-ai/dsh-session-projection/types' { } } -/** Append one fact to the persistent chunk stack in constant bounded work. */ -function appendFact(state: SubagentCatalogState, fact: SubagentCatalogEvent): SubagentCatalogState { - const head = state.head - if (head === undefined || head.facts.length === CATALOG_CHUNK_CAPACITY) { - return { ...state, head: { facts: [fact], ...head === undefined ? {} : { previous: head } } } - } - return { - ...state, - head: { - facts: [...head.facts, fact], - ...head.previous === undefined ? {} : { previous: head.previous }, - }, - } -} - /** * Materialize direct children from their parent's successful creation facts. * @param state - parent catalog fold state. * @returns current direct-child rows in parent catalog event order. */ function subagentCatalogEntries(state: SubagentCatalogState): SubagentCatalogEntry[] { - const chunks: CatalogChunk[] = [] - for (let chunk = state.head; chunk !== undefined; chunk = chunk.previous) chunks.push(chunk) const entries: SubagentCatalogEntry[] = [] - for (const chunk of chunks.reverse()) { - for (const data of chunk.facts) { - entries.push(data.mode === 'one-shot' - ? { - id: data.childId, - createdAt: data.childCreatedAt, - mode: data.mode, - ...data.label === undefined ? {} : { label: data.label }, - } - : { - id: data.childId, - createdAt: data.childCreatedAt, - mode: data.mode, - label: data.label, - }) - } + for (const data of iterateChunkedList(state.head)) { + entries.push(data.mode === 'one-shot' + ? { + id: data.childId, + createdAt: data.childCreatedAt, + mode: data.mode, + ...data.label === undefined ? {} : { label: data.label }, + } + : { + id: data.childId, + createdAt: data.childCreatedAt, + mode: data.mode, + label: data.label, + }) } return entries } @@ -148,9 +119,9 @@ export const subagentCatalogProjectionDefinition = { init: (_header: SessionHeader, inheritedEventCount: SessionLogOffset) => ({ inheritedEventCount }), apply: (state, event: SessionEvent) => { if (event.type !== 'subagent/catalog' || event.seq < state.inheritedEventCount) return state - return appendFact(state, eventDataSchema.parse(event.data)) + return { ...state, head: appendChunkedList(state.head, eventDataSchema.parse(event.data)) } }, - stateVersion: 1, + stateVersion: 2, wire: { viewSchema, view: subagentCatalogEntries }, } satisfies ProjectionDefinition<'subagentCatalog', SubagentCatalogState> diff --git a/packages/subagent/subagent/tests/service.spec.ts b/packages/subagent/subagent/tests/service.spec.ts index 3de6e83193..f40fc11dff 100644 --- a/packages/subagent/subagent/tests/service.spec.ts +++ b/packages/subagent/subagent/tests/service.spec.ts @@ -84,7 +84,7 @@ describe('SubagentRuntime', () => { childCreatedAt: 1, mode: 'one-shot', }) - expect(ctx.sessionProjections.stateOf(parent, 'subagentCatalog')?.head?.facts).toHaveLength(1) + expect(ctx.sessionProjections.snapshot(parent).values.subagentCatalog).toHaveLength(1) await fiber.dispose() diff --git a/packages/subagent/subagent/tsconfig.json b/packages/subagent/subagent/tsconfig.json index 4a90ca128c..923a2e119c 100644 --- a/packages/subagent/subagent/tsconfig.json +++ b/packages/subagent/subagent/tsconfig.json @@ -67,6 +67,9 @@ }, { "path": "../../runtime-diagnostics/invariants" + }, + { + "path": "../../util/chunked-list" } ] } diff --git a/packages/util/README.i18n.yaml b/packages/util/README.i18n.yaml index 8453741048..f472989bc0 100644 --- a/packages/util/README.i18n.yaml +++ b/packages/util/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/util/README.md -README.md: 2e46d9d4573b31afa14d7f1380af106c391d19d6 -README.zh.md: c7d8b63dc3ee7e748e8a07a3d65315ff81d5476f +README.md: c6fd72b85fe302392c41818263396c316f987422 +README.zh.md: 879671b642e4872084926e510a84ee7dc1a3b39b diff --git a/packages/util/README.md b/packages/util/README.md index 2e46d9d457..c6fd72b85f 100644 --- a/packages/util/README.md +++ b/packages/util/README.md @@ -30,6 +30,7 @@ Each package provides one primitive; open a package page for how to use it. | [`package-manifest/`](package-manifest/README.md) | Shared TypeScript declarations for package manifests | | [`crypto/`](crypto/README.md) | Mints RFC 9562 v4 UUIDs from the cross-runtime `crypto.getRandomValues` primitive | | [`deque/`](deque/README.md) | Provides amortized constant-time queue operations with bounded vacant storage | +| [`chunked-list/`](chunked-list/README.md) | Retains immutable list versions with bounded append copying and checkpoint validation | | [`values/`](values/README.md) | Validates, snapshots, compares, and freezes lossless JSON-compatible values | | [`home-paths/`](home-paths/README.md) | Resolves the single Harness home and joins shared user-data paths | | [`http-proxy/`](http-proxy/README.md) | Resolves one outbound proxy policy and installs it for `fetch`, SDK agents, and spawned children | diff --git a/packages/util/README.zh.md b/packages/util/README.zh.md index c7d8b63dc3..879671b642 100644 --- a/packages/util/README.zh.md +++ b/packages/util/README.zh.md @@ -30,6 +30,7 @@ kind: "package-group" | [`package-manifest/`](package-manifest/README.zh.md) | Package manifest 的共享 TypeScript 声明 | | [`crypto/`](crypto/README.zh.md) | 基于跨运行时 `crypto.getRandomValues` 原语生成 RFC 9562 v4 UUID | | [`deque/`](deque/README.zh.md) | 提供摊销常数时间的队列操作和有界空闲存储 | +| [`chunked-list/`](chunked-list/README.zh.md) | 通过有界追加复制和检查点校验保留不可变列表版本 | | [`values/`](values/README.zh.md) | 校验、创建快照、比较和冻结无损 JSON 兼容值 | | [`home-paths/`](home-paths/README.zh.md) | 解析统一的 Harness 主目录并拼接共享的用户数据路径 | | [`http-proxy/`](http-proxy/README.zh.md) | 解析出唯一的出站代理策略,并为 `fetch`、SDK agent 与派生子进程安装它 | diff --git a/packages/util/chunked-list/README.i18n.yaml b/packages/util/chunked-list/README.i18n.yaml new file mode 100644 index 0000000000..981bf95bdf --- /dev/null +++ b/packages/util/chunked-list/README.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write packages/util/chunked-list/README.md +README.md: f0af89e5dd6cfdc6388fe90aaeaf2fb70b149827 +README.zh.md: 9f28884ad7414ceeae1d7e624cb20efca9033b25 diff --git a/packages/util/chunked-list/README.md b/packages/util/chunked-list/README.md new file mode 100644 index 0000000000..f0af89e5dd --- /dev/null +++ b/packages/util/chunked-list/README.md @@ -0,0 +1,93 @@ +--- +description: "Immutable append-only lists for projection state, with bounded append copying, insertion-order iteration, and Zod checkpoint validation." +kind: "package-library" +--- + +# @deepseek-ai/dsh-chunked-list + +English | [中文](README.zh.md) + +## Summary + +`dsh-chunked-list` lets callers append values while retaining earlier list versions without copying the whole collection. Callers can iterate every value in insertion order and validate JSON checkpoints with their own value schema. The subagent catalog uses it for immutable projection state. + +## Table of Contents + +- [Use this package](#use-this-package) +- [Understand the implementation](#understand-the-implementation) +- [Further Exploration](#further-exploration) +- [Model Experience](#model-experience) +- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work) +- [Dev Note](#dev-note) + +----- + + +## Use this package + +Use this list when an append-only collection needs immutable versions and JSON-compatible storage. An empty list is `undefined`; appending returns a new head without modifying existing nodes. The list shares stored values by reference, so callers must treat them as immutable. + +```ts +import { appendChunkedList, iterateChunkedList } from '@deepseek-ai/dsh-chunked-list' + +const first = appendChunkedList(undefined, 'first') +const second = appendChunkedList(first, 'second') +console.log([...iterateChunkedList(second)]) +``` + +The example produces `['first', 'second']`; `first` still contains only its original value. `chunkedListSchema(valueSchema)` validates JSON checkpoints and rejects unknown fields, invalid values, and empty or oversized chunks. Use `.optional()` on the schema when the containing field also permits an empty list. See the [source contracts](src/index.ts) for the operations. + +----- + + +## Understand the implementation + +
    +Implementation internals — click to expand + +The newest chunk stores up to 64 values. Appends copy at most that chunk and share older nodes, taking bounded O(1) work. The capacity controls storage layout, not total list length. Iteration visits all N values in O(N) time and uses O(N / 64) scratch space to visit chunks from oldest to newest. A single capacity constant governs append rollover and recursive Zod validation. + +| File | Role | +|---|---| +| [`src/index.ts`](src/index.ts) | Persistent list operations and checkpoint validation | +| [`tests/chunked-list.spec.ts`](tests/chunked-list.spec.ts) | Version isolation, ordering, structural sharing, and checkpoint acceptance | + +No runtime invariant companion is published because this library has no independently changing observations; its operations return caller-owned immutable values. + +
    + +----- + + +## Further Exploration + +- [Utility package map](../README.md) — shared primitives. +- [Subagent catalog decision](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md) — why projection state uses chunks. + +----- + + +## Model Experience + +None, as this collection registers nothing model-facing. + +#### KV Cache effect + +Nothing here enters a model request, so provider cache reuse is unaffected. + +## Known Limitations and Deferred Work + + + +- **Append-only access** — callers needing removal or random access need another collection. +- **Recursive checkpoints** — JSON serialization and schema validation remain subject to runtime nesting limits. Stored values must themselves support the caller's serialization format. + + +### Dev Note + +
    +Working context for maintainers — click to expand + +None. + +
    diff --git a/packages/util/chunked-list/README.zh.md b/packages/util/chunked-list/README.zh.md new file mode 100644 index 0000000000..9f28884ad7 --- /dev/null +++ b/packages/util/chunked-list/README.zh.md @@ -0,0 +1,93 @@ +--- +description: "用于 projection state 的不可变追加列表,提供有界追加复制、按插入顺序迭代和 Zod 检查点校验。" +kind: "package-library" +--- + +# @deepseek-ai/dsh-chunked-list + +[English](README.md) | 中文 + +## 概述 + +`dsh-chunked-list` 让调用方追加值并保留早期列表版本,无需复制整个集合。调用方可以按插入顺序迭代所有值,并使用自己的值 schema 校验 JSON 检查点。subagent 目录用它保存不可变的 projection state。 + +## 目录 + +- [使用此包](#use-this-package) +- [理解实现](#understand-the-implementation) +- [进一步探索](#further-exploration) +- [模型体验](#model-experience) +- [已知限制与延后工作](#known-limitations-and-deferred-work) +- [开发备注](#dev-note) + +----- + + +## 使用此包 + +当仅追加集合需要不可变版本和兼容 JSON 的存储时,使用此列表。空列表用 `undefined` 表示;追加返回新的头节点,不修改已有节点。列表按引用共享所存的值,因此调用方必须将这些值视为不可变。 + +```ts +import { appendChunkedList, iterateChunkedList } from '@deepseek-ai/dsh-chunked-list' + +const first = appendChunkedList(undefined, 'first') +const second = appendChunkedList(first, 'second') +console.log([...iterateChunkedList(second)]) +``` + +示例输出 `['first', 'second']`;`first` 仍只包含原来的值。`chunkedListSchema(valueSchema)` 校验 JSON 检查点并拒绝未知字段、无效值和空块或超大块。当外层字段也允许空列表时,在 schema 上使用 `.optional()`。各操作详见[源码约定](src/index.ts)。 + +----- + + +## 理解实现 + +
    +实现内部机制——点击展开 + +最新的块最多存储 64 个值。追加最多复制该块并共享较旧的节点,工作量为有界 O(1)。容量控制存储布局,不限制列表总长度。迭代以 O(N) 时间访问全部 N 个值,并使用 O(N / 64) 临时空间按从旧到新的顺序访问各块。追加换块与递归 Zod 校验共用一个容量常量。 + +| 文件 | 职责 | +|---|---| +| [`src/index.ts`](src/index.ts) | 持久化列表操作与检查点校验 | +| [`tests/chunked-list.spec.ts`](tests/chunked-list.spec.ts) | 版本隔离、排序、结构共享与检查点接受条件 | + +此库没有独立变化的观测值,因此不发布运行时不变式伴随模块;其操作返回调用方拥有的不可变值。 + +
    + +----- + + +## 进一步探索 + +- [工具包映射](../README.zh.md)——共享原语。 +- [Subagent 目录决策](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md)——projection state 使用分块的原因。 + +----- + + +## 模型体验 + +无,因为此集合不注册任何面向模型的内容。 + +#### KV Cache 影响 + +本包没有内容进入模型请求,因此不影响提供方缓存复用。 + +## 已知限制与延后工作 + + + +- **仅追加访问**——需要删除或随机访问的调用方应使用其他集合。 +- **递归检查点**——JSON 序列化与 schema 校验仍受运行时嵌套深度限制。所存的值本身必须支持调用方的序列化格式。 + + +### 开发备注 + +
    +维护者的工作上下文——点击展开 + +无。 + +
    diff --git a/packages/util/chunked-list/package.json b/packages/util/chunked-list/package.json new file mode 100644 index 0000000000..130bde0ae2 --- /dev/null +++ b/packages/util/chunked-list/package.json @@ -0,0 +1,38 @@ +{ + "name": "@deepseek-ai/dsh-chunked-list", + "description": "Persistent append-only chunked lists with bounded copying and JSON checkpoint validation", + "version": "0.1.3-alpha.2", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", + "directory": "packages/util/chunked-list" + }, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./src/*": "./src/*", + "./package.json": "./package.json" + }, + "files": [ + "lib/index.js", + "lib/types/**/*.d.ts" + ], + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "workspace:^" + }, + "devDependencies": { + "@deepseek-ai/cordis": "workspace:^" + }, + "dependencies": { + "zod": "^4.4.3" + } +} diff --git a/packages/util/chunked-list/src/index.ts b/packages/util/chunked-list/src/index.ts new file mode 100644 index 0000000000..98078079fe --- /dev/null +++ b/packages/util/chunked-list/src/index.ts @@ -0,0 +1,58 @@ +/** + * Persistent append-only lists with bounded copying and JSON checkpoint validation. + * @module @deepseek-ai/dsh-chunked-list + */ + +import { z } from 'zod' + +const CHUNK_CAPACITY = 64 + +/** + * Newest chunk of an immutable list; `undefined` represents the empty list. + * Values within each chunk follow insertion order. Callers treat nodes, arrays, + * and stored values as immutable; operations share values and older chunks. + */ +export interface ChunkedList { + readonly values: readonly T[] + readonly previous?: ChunkedList | undefined +} + +/** + * Append without modifying the input, copying at most one 64-value chunk. + * @param head - current list, or `undefined` for an empty list. + * @param value - value to retain by reference. + * @returns new list sharing the unchanged older chunks. + */ +export function appendChunkedList(head: ChunkedList | undefined, value: T): ChunkedList { + if (head === undefined || head.values.length === CHUNK_CAPACITY) { + return { values: [value], ...head === undefined ? {} : { previous: head } } + } + return { + values: [...head.values, value], + ...head.previous === undefined ? {} : { previous: head.previous }, + } +} + +/** + * Visit all values in insertion order, with O(N) time and O(N / 64) scratch space. + * @param head - current list, or `undefined` for an empty list. + * @returns iterator yielding the stored values by reference, without truncation. + */ +export function* iterateChunkedList(head: ChunkedList | undefined): Generator { + const chunks: ChunkedList[] = [] + for (let chunk = head; chunk !== undefined; chunk = chunk.previous) chunks.push(chunk) + for (const chunk of chunks.reverse()) yield* chunk.values +} + +/** + * Validate nonempty list checkpoints, including every stored value and chunk size. + * @param valueSchema - caller-owned validation for each stored value. + * @returns recursive Zod schema rejecting empty or oversized chunks and unknown fields. + */ +export function chunkedListSchema(valueSchema: z.ZodType): z.ZodType> { + const schema: z.ZodType> = z.lazy(() => z.object({ + values: z.array(valueSchema).min(1).max(CHUNK_CAPACITY), + previous: schema.optional(), + }).strict()) + return schema +} diff --git a/packages/util/chunked-list/tests/chunked-list.spec.ts b/packages/util/chunked-list/tests/chunked-list.spec.ts new file mode 100644 index 0000000000..24946e3ad5 --- /dev/null +++ b/packages/util/chunked-list/tests/chunked-list.spec.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'vitest' +import { z } from 'zod' +import { appendChunkedList, chunkedListSchema, iterateChunkedList } from '../src/index.ts' +import type { ChunkedList } from '../src/index.ts' + +describe('persistent chunked list', () => { + it('iterates an empty list and retains undefined values', () => { + expect([...iterateChunkedList(undefined)]).toEqual([]) + const head = appendChunkedList(undefined, undefined) + expect([...iterateChunkedList(head)]).toEqual([undefined]) + }) + + it('keeps every prefix unchanged across chunk rollovers and divergent appends', () => { + let head: ChunkedList | undefined + const prefixes: ChunkedList[] = [] + for (let value = 0; value < 200; value += 1) { + head = appendChunkedList(head, value) + Object.freeze(head.values) + Object.freeze(head) + prefixes.push(head) + } + for (const [index, prefix] of prefixes.entries()) { + expect([...iterateChunkedList(prefix)]).toEqual(Array.from({ length: index + 1 }, (_, i) => i)) + } + expect(prefixes[64]?.previous).toBe(prefixes[63]) + expect(prefixes[65]?.previous).toBe(prefixes[63]) + expect([...iterateChunkedList(appendChunkedList(prefixes[64], -1))]) + .toEqual([...Array.from({ length: 65 }, (_, i) => i), -1]) + expect([...iterateChunkedList(prefixes[65])]).toEqual(Array.from({ length: 66 }, (_, i) => i)) + }) + + it('retains stored objects by reference', () => { + const value = Object.freeze({ id: 'first' }) + expect([...iterateChunkedList(appendChunkedList(undefined, value))][0]).toBe(value) + }) + + it('restores JSON checkpoints and appends at and after rollover', () => { + const schema = chunkedListSchema(z.number()) + let head: ChunkedList | undefined + for (let value = 0; value < 130; value += 1) { + head = appendChunkedList(head, value) + head = schema.parse(JSON.parse(JSON.stringify(head))) + } + expect([...iterateChunkedList(head)]).toEqual(Array.from({ length: 130 }, (_, i) => i)) + }) + + it.each([ + { values: [] }, + { values: Array.from({ length: 65 }, () => 0) }, + { values: ['invalid'] }, + { values: [0], extra: true }, + { values: [0], previous: { values: [] } }, + { values: [0], previous: { values: Array.from({ length: 65 }, () => 0) } }, + { values: [0], previous: { values: ['invalid'] } }, + { values: [0], previous: { values: [1], extra: true } }, + ])('rejects an invalid checkpoint: %j', (checkpoint) => { + expect(() => chunkedListSchema(z.number()).parse(checkpoint)).toThrow(z.ZodError) + }) +}) diff --git a/packages/util/chunked-list/tsconfig.json b/packages/util/chunked-list/tsconfig.json new file mode 100644 index 0000000000..749cb0208e --- /dev/null +++ b/packages/util/chunked-list/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": [ + "src" + ], + "references": [] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c3122497b..52b935f622 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9036,6 +9036,9 @@ importers: '@deepseek-ai/dsh-brand': specifier: workspace:^ version: link:../../util/brand + '@deepseek-ai/dsh-chunked-list': + specifier: workspace:^ + version: link:../../util/chunked-list '@deepseek-ai/dsh-util-values': specifier: workspace:^ version: link:../../util/values @@ -10169,6 +10172,16 @@ importers: specifier: workspace:^ version: link:../../../vendor/cordis + packages/util/chunked-list: + dependencies: + zod: + specifier: ^4.4.3 + version: 4.4.3 + devDependencies: + '@deepseek-ai/cordis': + specifier: workspace:^ + version: link:../../../vendor/cordis + packages/util/crypto: devDependencies: '@deepseek-ai/cordis': diff --git a/scripts/doc-standard.spec.ts b/scripts/doc-standard.spec.ts index 0faeb049dc..bfb90703e0 100644 --- a/scripts/doc-standard.spec.ts +++ b/scripts/doc-standard.spec.ts @@ -82,6 +82,7 @@ const PACKAGE_LIBRARIES: Readonly> = { 'packages/util/brand': 'Stateless nominal-string and canonical-key constructors.', 'packages/util/crypto': 'Zero-dependency identifier minting utility.', 'packages/util/deque': 'Zero-dependency circular deque utility.', + 'packages/util/chunked-list': 'Persistent collection operations and checkpoint validation without a plugin surface.', 'packages/util/home-paths': 'Zero-dependency harness-home path resolver.', 'packages/util/launch-environment': 'Zero-dependency environment resolver.', 'packages/util/native-command': 'Host-side subprocess runner utility.', diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts index 449ee7322a..627e72bd21 100644 --- a/scripts/verify-package-readme-model-experience.ts +++ b/scripts/verify-package-readme-model-experience.ts @@ -57,6 +57,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly> = { 'packages/client/ui-agent-preset': { kind: 'indirect', reason: 'Browser-side settings row; the preset it selects owns every model-facing effect.' }, 'packages/util/crypto': { kind: 'indirect', reason: 'Pure identifier minting; the ids consumers mint with it never enter prompts as semantic content.' }, 'packages/util/deque': { kind: 'none', reason: 'In-process collection primitive; registers nothing model-facing.' }, + 'packages/util/chunked-list': { kind: 'none', reason: 'Immutable collection primitive; registers nothing model-facing.' }, 'packages/util/package-manifest': { kind: 'none', reason: 'Type declarations only; registers nothing model-facing.' }, 'packages/util/time': { kind: 'indirect', reason: 'Pure zone validation; the consumer that records a canonical zone owns the model-visible line derived from it.' }, 'packages/core/agent-default-model': { kind: 'indirect', reason: 'The service supplies a ModelSelection; request assembly and adapters own the model-visible request.' }, diff --git a/tsconfig.base.json b/tsconfig.base.json index 20d789c2a1..637fc6e454 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -269,6 +269,7 @@ "@deepseek-ai/dsh-bash-local": ["./packages/shell/bash-local/src"], "@deepseek-ai/dsh-bash-sandbox": ["./packages/shell/bash-sandbox/src"], "@deepseek-ai/dsh-brand": ["./packages/util/brand/src"], + "@deepseek-ai/dsh-chunked-list": ["./packages/util/chunked-list/src"], "@deepseek-ai/dsh-cmdline": ["./packages/boot/cmdline/src"], "@deepseek-ai/dsh-code-runtime": ["./packages/code-runtime/code-runtime/src"], "@deepseek-ai/dsh-code-runtime-worker-thread": ["./packages/code-runtime/code-runtime-worker-thread/src"], diff --git a/tsconfig.host.json b/tsconfig.host.json index 55f065c7a7..e13180bbdf 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -148,6 +148,7 @@ { "path": "./packages/util/timeout" }, { "path": "./packages/util/crypto" }, { "path": "./packages/util/deque" }, + { "path": "./packages/util/chunked-list" }, { "path": "./packages/util/values" }, { "path": "./packages/util/workspace-path" }, { "path": "./packages/util/output-retention" }, From e2fa69f5e20d0350f72acf34caa3a58582df4976 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Tue, 8 Sep 2026 20:21:15 +0800 Subject: [PATCH 28/58] test(snapshot): bind sibling roles to catalog publication order --- ...01-parent-owned-subagent-catalog.i18n.yaml | 4 +- ...026-09-01-parent-owned-subagent-catalog.md | 2 + ...-09-01-parent-owned-subagent-catalog.zh.md | 2 + snapshots/session/headless.snapshot.ts | 39 ++++++++++++++++--- 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml index f6a34bbc6b..2e12236e27 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md -2026-09-01-parent-owned-subagent-catalog.md: b0eee83499f8f9f3b3e2be652037dc882e005c94 -2026-09-01-parent-owned-subagent-catalog.zh.md: 8985eb291c5dbfadb53eb3576dcd9306496aa983 +2026-09-01-parent-owned-subagent-catalog.md: 24036545d72070e0c46dd4d6ac4ac30728140afd +2026-09-01-parent-owned-subagent-catalog.zh.md: 416fba9fc75842b3b4a2a46c67568d232135cae2 diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md index b0eee83499..24036545d7 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md @@ -24,6 +24,8 @@ The utility owns chunk layout and its shared capacity constant; the catalog owns Fork isolation uses the exact `Session.inheritedEventCount` supplied to projection initialization. The fold ignores `subagent/catalog` events below that offset. The state stores the inherited offset but not each event seq because acceptance is decided during folding. +Headless snapshot collection assigns sibling fixture roles by their parent catalog order, regardless of child creation timestamps: provider startup can publish an older Session after a newer one. The collection preserves each log verbatim. + Snapshot normalizers zero `childCreatedAt` because it originates from the process clock. Event order and source-event references remain intact: adjacent facts can come from sequential creation, so adjacency does not establish commutativity. ## Alternatives considered diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md index 8985eb291c..416fba9fc7 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md @@ -24,6 +24,8 @@ child header 与 `subagent/descriptor` 继续拥有恢复与 composition 权威 fork 隔离使用 projection 初始化时提供的精确 `Session.inheritedEventCount`。fold 忽略该 offset 之前的 `subagent/catalog` 事件。state 保存 inherited offset,但不保存每条 event seq,因为接受判定已在 fold 时完成。 +Headless 快照采集按父目录顺序分配同父子级的 fixture 角色,不依赖子级创建时间戳:provider 启动可能在较新的 Session 之后发布较旧的 Session。采集过程原样保留每份日志。 + snapshot normalizer 会把 `childCreatedAt` 归零,因为它来自 process clock。事件顺序与来源事件引用保持不变:相邻 fact 也可能来自顺序创建,因此相邻关系不能证明可交换性。 ## 考虑过的替代方案 diff --git a/snapshots/session/headless.snapshot.ts b/snapshots/session/headless.snapshot.ts index fb5af64a62..552378d261 100644 --- a/snapshots/session/headless.snapshot.ts +++ b/snapshots/session/headless.snapshot.ts @@ -176,8 +176,8 @@ async function persistedSessions(cwd: string): Promise { expect(assertPersistedSessionVersion(basename(file), content), `${file}: current writer`).toBe(SESSION_FORMAT_VERSION) return { content, header: headerOf(content) } })) - // Same-millisecond siblings bind to fixture roles by their parent's recorded - // discovery order; random persistence filenames do not identify child roles. + // Siblings bind to fixture roles by catalog publication order; concurrent + // provider startup can publish an older Session after a newer one. const catalogOrders = new Map(logs.map(log => [log.header.id, new Map( records(log.content) .filter(event => event.type === 'subagent/catalog') @@ -187,10 +187,12 @@ async function persistedSessions(cwd: string): Promise { const leftChild = typeof left.header.parentSession === 'string' const rightChild = typeof right.header.parentSession === 'string' if (leftChild !== rightChild) return leftChild ? 1 : -1 - const timeOrder = Number(left.header.createdAt) - Number(right.header.createdAt) - if (timeOrder !== 0 || left.header.parentSession !== right.header.parentSession) return timeOrder - const catalogOrder = catalogOrders.get(left.header.parentSession) - return (catalogOrder?.get(left.header.id) ?? Infinity) - (catalogOrder?.get(right.header.id) ?? Infinity) + if (left.header.parentSession === right.header.parentSession) { + const catalogOrder = catalogOrders.get(left.header.parentSession) + const childOrder = (catalogOrder?.get(left.header.id) ?? Infinity) - (catalogOrder?.get(right.header.id) ?? Infinity) + if (childOrder) return childOrder + } + return Number(left.header.createdAt) - Number(right.header.createdAt) }) } @@ -782,6 +784,31 @@ describe('headless recorded-session snapshots', () => { expect(stderrFromSession(log)).toBe('dsh: reasoning:\nfirst thought\ndsh: reasoning:\nsecond\n') }) + it.each([10, 20])('assigns sibling roles by catalog order when the first child timestamp is %i', async (firstCreatedAt) => { + const cwd = await mkdtemp(join(tmpdir(), 'dsh-headless-catalog-order-')) + try { + const logs = [ + [ + { type: 'session', version: 2, id: 'parent', createdAt: 1 }, + { type: 'subagent/catalog', data: { childId: 'child-z' } }, + { type: 'subagent/catalog', data: { childId: 'child-a' } }, + ], + [{ type: 'session', version: 2, id: 'child-z', createdAt: firstCreatedAt, parentSession: 'parent' }], + [{ type: 'session', version: 2, id: 'child-a', createdAt: 10, parentSession: 'parent' }], + ].map(rows => rows.map(row => JSON.stringify(row)).join('\n') + '\n') + for (const content of logs) { + const directory = join(cwd, '.dsh', 'sessions', String(headerOf(content).id)) + await mkdir(directory, { recursive: true }) + await writeFile(join(directory, 'session.v2.jsonl'), content) + } + const actual = await persistedSessions(cwd) + expect(actual.map(log => log.header.id)).toEqual(['parent', 'child-z', 'child-a']) + expect(actual.map(log => log.content)).toEqual(logs) + } finally { + await rm(cwd, { recursive: true, force: true }) + } + }) + it('writes header sidecars without replacing a retained Session generation', async () => { const directory = await mkdtemp(join(tmpdir(), 'dsh-headless-sidecars-')) try { From 79dda6cf4512e466e58ffce930ffa7153cc328c7 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Tue, 8 Sep 2026 23:19:42 +0800 Subject: [PATCH 29/58] test(subagent): align catalog snapshots with Session V3 --- ...01-parent-owned-subagent-catalog.i18n.yaml | 4 +- ...026-09-01-parent-owned-subagent-catalog.md | 2 + ...-09-01-parent-owned-subagent-catalog.zh.md | 2 + .../session-snapshot/src/index.ts | 1 - .../session-snapshot/src/normalize.ts | 38 ---- .../session-snapshot/tests/normalize.spec.ts | 65 ------ .../advanced/result.json | 202 +++++++++++------- .../advanced/session.v2.jsonl | 12 +- .../advanced/session.v3.jsonl | 12 +- .../session.v2.jsonl | 7 +- .../session.v3.jsonl | 7 +- .../sdk/subagent-continuable/session.v2.jsonl | 19 +- .../sdk/subagent-continuable/session.v3.jsonl | 19 +- .../subagent-fork-in-process/session.v2.jsonl | 7 +- .../subagent-fork-in-process/session.v3.jsonl | 7 +- .../sdk/subagent-list-agents/session.v2.jsonl | 13 +- .../sdk/subagent-list-agents/session.v3.jsonl | 13 +- snapshots/sdk/subagent-mixed/session.v2.jsonl | 12 +- snapshots/sdk/subagent-mixed/session.v3.jsonl | 12 +- .../subagent-send-message/session.v2.jsonl | 9 +- .../subagent-send-message/session.v3.jsonl | 9 +- .../notifications.expected.jsonl | 13 +- .../session.v2.jsonl | 5 +- .../session.v3.jsonl | 5 +- .../session.v2.jsonl | 16 +- .../session.v3.jsonl | 16 +- .../advanced-toolchain/session.v2.jsonl | 18 +- .../advanced-toolchain/session.v3.jsonl | 18 +- snapshots/session/headless.snapshot.ts | 12 +- snapshots/session/ralph-loop/session.v2.jsonl | 6 +- snapshots/session/ralph-loop/session.v3.jsonl | 6 +- .../session.v2.jsonl | 5 +- .../session.v3.jsonl | 5 +- .../session.1.v2.jsonl | 5 +- .../session.1.v3.jsonl | 5 +- .../session.v2.jsonl | 5 +- .../session.v3.jsonl | 5 +- .../session.v2.jsonl | 5 +- .../session.v3.jsonl | 5 +- .../subagent-multi/writer.expected.jsonl | 10 +- .../subagent-parallel/session.v2.jsonl | 6 +- .../subagent-parallel/session.v3.jsonl | 6 +- .../session.v2.jsonl | 5 +- .../session.v3.jsonl | 5 +- .../session.v2.jsonl | 5 +- .../session.v3.jsonl | 5 +- .../session/workflow-run/session.v2.jsonl | 5 +- .../session/workflow-run/session.v3.jsonl | 5 +- 48 files changed, 318 insertions(+), 361 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml index 2e12236e27..697c410f3a 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md -2026-09-01-parent-owned-subagent-catalog.md: 24036545d72070e0c46dd4d6ac4ac30728140afd -2026-09-01-parent-owned-subagent-catalog.zh.md: 416fba9fc75842b3b4a2a46c67568d232135cae2 +2026-09-01-parent-owned-subagent-catalog.md: 5926af77219680a48af1e5fb062ddc11a91a2280 +2026-09-01-parent-owned-subagent-catalog.zh.md: ddc77a665207169cfc048b9f2cfbd4f912cb93dc diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md index 24036545d7..5926af7721 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md @@ -28,6 +28,8 @@ Headless snapshot collection assigns sibling fixture roles by their parent catal Snapshot normalizers zero `childCreatedAt` because it originates from the process clock. Event order and source-event references remain intact: adjacent facts can come from sequential creation, so adjacency does not establish commutativity. +Current-writer snapshot expectations include catalog facts even when replay input retains a historical Session generation. The comparison preserves the catalog and its source-event references; historical replay files remain unchanged. + ## Alternatives considered **A flat immutable array.** Appending with `[...facts, fact]` copies D facts, so creation is O(D). Mutating a shared array would violate projection state ownership and checkpoint safety. diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md index 416fba9fc7..ddc77a6652 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md @@ -28,6 +28,8 @@ Headless 快照采集按父目录顺序分配同父子级的 fixture 角色, snapshot normalizer 会把 `childCreatedAt` 归零,因为它来自 process clock。事件顺序与来源事件引用保持不变:相邻 fact 也可能来自顺序创建,因此相邻关系不能证明可交换性。 +即使 replay 输入保留历史 Session generation,当前 writer 的快照预期也包含 catalog 事实。比较保留 catalog 及其来源事件引用;历史 replay 文件保持不变。 + ## 考虑过的替代方案 **扁平不可变数组。** 用 `[...facts, fact]` append 会复制 D 个 fact,因此创建是 O(D)。修改共享数组会违反 projection state ownership 与 checkpoint 安全。 diff --git a/packages/test-support/session-snapshot/src/index.ts b/packages/test-support/session-snapshot/src/index.ts index eb4d900a74..1b53cccfe2 100644 --- a/packages/test-support/session-snapshot/src/index.ts +++ b/packages/test-support/session-snapshot/src/index.ts @@ -44,7 +44,6 @@ export { normalizeSessionSnapshot, normalizeSessionSnapshots, normalizeStdout, - omitSubagentCatalogForHistoricalComparison, scrubModelRequestBulk, scrubSessionSnapshot, scrubSystemPrompts, diff --git a/packages/test-support/session-snapshot/src/normalize.ts b/packages/test-support/session-snapshot/src/normalize.ts index a6e5d3d341..bb1a507453 100644 --- a/packages/test-support/session-snapshot/src/normalize.ts +++ b/packages/test-support/session-snapshot/src/normalize.ts @@ -455,44 +455,6 @@ export function normalizeSessionSnapshots( )) } -/** - * Omit current subagent discovery facts when comparing a fresh run with an - * immutable historical Session generation. The remaining source-event - * references are rebased to the projected stream and refuse to cite an - * omitted catalog event. - * - * @param rawLog - normalized current-format snapshot JSONL. - * @returns the snapshot without `subagent/catalog` events. - */ -export function omitSubagentCatalogForHistoricalComparison(rawLog: string): string { - const lines = rawLog.split('\n').filter(line => line.trim().length > 0) - const header = lines.shift() - if (header === undefined) throw new Error('session snapshot must start with a session header') - const events = lines.map(line => JSON.parse(line) as Record) - const omitted = new Set(events.flatMap((event, seq) => event.type === 'subagent/catalog' ? [seq] : [])) - if (omitted.size === 0) return `${[header, ...lines].join('\n')}\n` - - const retained = events.flatMap((event, seq) => { - if (omitted.has(seq)) return [] - if (!Array.isArray(event.sourceEventSeqs)) return [event] - const sourceEventSeqs = event.sourceEventSeqs.map((source) => { - if (typeof source !== 'number' || !Number.isSafeInteger(source) || source < 0) { - throw new Error('normalized sourceEventSeqs must contain non-negative safe integers') - } - if (omitted.has(source)) { - throw new Error(`historical comparison cannot omit cited catalog event ${source}`) - } - let preceding = 0 - for (const omittedSeq of omitted) { - if (omittedSeq < source) preceding += 1 - } - return source - preceding - }) - return [{ ...event, sourceEventSeqs }] - }) - return [header, ...retained.map(event => JSON.stringify(event)), ''].join('\n') -} - /** * Omit the artifact header generation after official migration for comparison. * Delivery and captured-source generations retain their opaque recorded values. diff --git a/packages/test-support/session-snapshot/tests/normalize.spec.ts b/packages/test-support/session-snapshot/tests/normalize.spec.ts index 4f136fbcb5..62321e5759 100644 --- a/packages/test-support/session-snapshot/tests/normalize.spec.ts +++ b/packages/test-support/session-snapshot/tests/normalize.spec.ts @@ -7,7 +7,6 @@ import { normalizeSessionSnapshot, normalizeSessionSnapshots, normalizeStdout, - omitSubagentCatalogForHistoricalComparison, scrubModelRequestBulk, scrubSessionSnapshot, scrubSystemPrompts, @@ -679,70 +678,6 @@ describe('normalizeSessionSnapshot', () => { expect(normalizeSessionSnapshot(normalized, ctx)).toBe(normalized) }) - it('omits current catalog facts and rebases source references for historical comparison', () => { - const normalized = [ - JSON.stringify({ type: 'session', id: '{{session:1}}' }), - JSON.stringify({ type: 'tool/call', data: { callId: 'first' } }), - JSON.stringify({ - type: 'subagent/catalog', - data: { version: 0, childId: '{{session:2}}', childCreatedAt: 0, mode: 'one-shot' }, - }), - JSON.stringify({ type: 'tool/result', data: { callId: 'first' }, sourceEventSeqs: [0] }), - JSON.stringify({ type: 'tool/call', data: { callId: 'second' } }), - JSON.stringify({ - type: 'subagent/catalog', - data: { version: 0, childId: '{{session:3}}', childCreatedAt: 0, mode: 'one-shot' }, - }), - JSON.stringify({ type: 'tool/result', data: { callId: 'second' }, sourceEventSeqs: [3] }), - '', - ].join('\n') - expect(omitSubagentCatalogForHistoricalComparison(normalized)).toBe([ - JSON.stringify({ type: 'session', id: '{{session:1}}' }), - JSON.stringify({ type: 'tool/call', data: { callId: 'first' } }), - JSON.stringify({ type: 'tool/result', data: { callId: 'first' }, sourceEventSeqs: [0] }), - JSON.stringify({ type: 'tool/call', data: { callId: 'second' } }), - JSON.stringify({ type: 'tool/result', data: { callId: 'second' }, sourceEventSeqs: [2] }), - '', - ].join('\n')) - }) - - it('rejects an empty historical comparison input', () => { - expect(() => omitSubagentCatalogForHistoricalComparison('\n')) - .toThrow('session snapshot must start with a session header') - }) - - it('preserves snapshots and source references when no catalog fact is present', () => { - const normalized = [ - { type: 'session', id: '{{session:1}}' }, - { type: 'tool/call', data: { callId: 'result' } }, - { type: 'tool/result', data: { callId: 'result' }, sourceEventSeqs: [0] }, - ].map(record => JSON.stringify(record)).join('\n') + '\n' - - expect(omitSubagentCatalogForHistoricalComparison(normalized)).toBe(normalized) - }) - - it.each(['0', 0.5, -1])('refuses malformed source references during historical comparison: %s', (source) => { - const normalized = [ - { type: 'session', id: '{{session:1}}' }, - { type: 'subagent/catalog', data: { childId: '{{session:2}}' } }, - { type: 'tool/result', data: { callId: 'result' }, sourceEventSeqs: [source] }, - ].map(record => JSON.stringify(record)).join('\n') - - expect(() => omitSubagentCatalogForHistoricalComparison(normalized)) - .toThrow('normalized sourceEventSeqs must contain non-negative safe integers') - }) - - it('refuses to remove a catalog fact cited by a retained event', () => { - const normalized = [ - { type: 'session', id: '{{session:1}}' }, - { type: 'subagent/catalog', data: { childId: '{{session:2}}' } }, - { type: 'tool/result', data: { callId: 'result' }, sourceEventSeqs: [0] }, - ].map(record => JSON.stringify(record)).join('\n') - - expect(() => omitSubagentCatalogForHistoricalComparison(normalized)) - .toThrow('historical comparison cannot omit cited catalog event 0') - }) - it('migrates and re-packs multi-session fixtures after relationship-preserving id redaction', () => { const raw = [ JSON.stringify({ type: 'session', version: 0, id: '{{session:1}}', createdAt: 0, delegationDepth: 0 }), diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/result.json b/scripts/snapshots/python-sdk-single-exe/advanced/result.json index a2378fb094..36c51fe50a 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/result.json +++ b/scripts/snapshots/python-sdk-single-exe/advanced/result.json @@ -904,9 +904,21 @@ } }, { - "type": "tool/result", + "type": "subagent/catalog", "seq": 38, "time": 0, + "data": { + "version": 0, + "childId": "{{child-1}}", + "childCreatedAt": 0, + "mode": "one-shot", + "label": "Check direct child" + } + }, + { + "type": "tool/result", + "seq": 39, + "time": 0, "data": { "turn": 1, "step": 4, @@ -939,7 +951,7 @@ }, { "type": "step/end", - "seq": 39, + "seq": 40, "time": 0, "data": { "turn": 1, @@ -948,7 +960,7 @@ }, { "type": "step/start", - "seq": 40, + "seq": 41, "time": 0, "data": { "turn": 1, @@ -957,17 +969,17 @@ }, { "type": "session-log-deepseek/delivery-accepted", - "seq": 41, + "seq": 42, "time": 0, "data": { "sessionId": "{{parent}}", "sessionFormatVersion": 3, - "throughSeq": 40 + "throughSeq": 41 } }, { "type": "assistant/message", - "seq": 42, + "seq": 43, "time": 0, "data": { "turn": 1, @@ -1057,7 +1069,7 @@ }, { "type": "tool/call", - "seq": 43, + "seq": 44, "time": 0, "data": { "turn": 1, @@ -1069,16 +1081,27 @@ }, { "type": "tool-workflow/run-start", - "seq": 44, + "seq": 45, "time": 0, "data": { "runId": "{{workflow-run}}", "name": "advanced-exe-snapshot" } }, + { + "type": "subagent/catalog", + "seq": 46, + "time": 0, + "data": { + "version": 0, + "childId": "{{child-2}}", + "childCreatedAt": 0, + "mode": "one-shot" + } + }, { "type": "tool-workflow/agent-start", - "seq": 45, + "seq": 47, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -1090,7 +1113,7 @@ }, { "type": "tool-workflow/agent-end", - "seq": 46, + "seq": 48, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -1100,7 +1123,7 @@ }, { "type": "tool-workflow/run-end", - "seq": 47, + "seq": 49, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -1109,7 +1132,7 @@ }, { "type": "tool/result", - "seq": 48, + "seq": 50, "time": 0, "data": { "turn": 1, @@ -1137,13 +1160,13 @@ } }, "sourceEventSeqs": [ - 43 + 44 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 49, + "seq": 51, "time": 0, "data": { "turn": 1, @@ -1152,7 +1175,7 @@ }, { "type": "step/start", - "seq": 50, + "seq": 52, "time": 0, "data": { "turn": 1, @@ -1161,17 +1184,17 @@ }, { "type": "session-log-deepseek/delivery-accepted", - "seq": 51, + "seq": 53, "time": 0, "data": { "sessionId": "{{parent}}", "sessionFormatVersion": 3, - "throughSeq": 50 + "throughSeq": 52 } }, { "type": "assistant/message", - "seq": 52, + "seq": 54, "time": 0, "data": { "turn": 1, @@ -1261,7 +1284,7 @@ }, { "type": "tool/call", - "seq": 53, + "seq": 55, "time": 0, "data": { "turn": 1, @@ -1273,7 +1296,7 @@ }, { "type": "tool/result", - "seq": 54, + "seq": 56, "time": 0, "data": { "turn": 1, @@ -1301,13 +1324,13 @@ } }, "sourceEventSeqs": [ - 53 + 55 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 55, + "seq": 57, "time": 0, "data": { "turn": 1, @@ -1316,7 +1339,7 @@ }, { "type": "step/start", - "seq": 56, + "seq": 58, "time": 0, "data": { "turn": 1, @@ -1325,7 +1348,7 @@ }, { "type": "system/message", - "seq": 57, + "seq": 59, "time": 0, "data": { "turn": 1, @@ -1356,7 +1379,7 @@ }, { "type": "request/header", - "seq": 58, + "seq": 60, "time": 0, "data": { "header": { @@ -1392,17 +1415,17 @@ }, { "type": "session-log-deepseek/delivery-accepted", - "seq": 59, + "seq": 61, "time": 0, "data": { "sessionId": "{{parent}}", "sessionFormatVersion": 3, - "throughSeq": 58 + "throughSeq": 60 } }, { "type": "assistant/message", - "seq": 60, + "seq": 62, "time": 0, "data": { "turn": 1, @@ -1486,7 +1509,7 @@ }, { "type": "step/end", - "seq": 61, + "seq": 63, "time": 0, "data": { "turn": 1, @@ -1495,7 +1518,7 @@ }, { "type": "command/run", - "seq": 62, + "seq": 64, "time": 0, "data": { "commandId": "{{command:1}}", @@ -1507,7 +1530,7 @@ }, { "type": "feedback/record", - "seq": 63, + "seq": 65, "time": 0, "data": { "text": "The session needs a clearer explanation." @@ -1515,7 +1538,7 @@ }, { "type": "command/done", - "seq": 64, + "seq": 66, "time": 0, "data": { "commandId": "{{command:1}}", @@ -1525,7 +1548,7 @@ }, { "type": "feedback/message-put", - "seq": 65, + "seq": 67, "time": 0, "data": { "sessionId": "{{parent}}", @@ -1541,7 +1564,7 @@ }, { "type": "feedback/message-put", - "seq": 66, + "seq": 68, "time": 0, "data": { "sessionId": "{{parent}}", @@ -1557,7 +1580,7 @@ }, { "type": "feedback/message-delete", - "seq": 67, + "seq": 69, "time": 0, "data": { "sessionId": "{{parent}}", @@ -1566,7 +1589,7 @@ }, { "type": "turn/end", - "seq": 68, + "seq": 70, "time": 0, "data": { "turn": 1, @@ -2784,6 +2807,24 @@ } } }, + { + "method": "session.event", + "payload": { + "sessionId": "{{parent}}", + "event": { + "type": "subagent/catalog", + "seq": 38, + "time": 0, + "data": { + "version": 0, + "childId": "{{child-1}}", + "childCreatedAt": 0, + "mode": "one-shot", + "label": "Check direct child" + } + } + } + }, { "method": "session.event", "payload": { @@ -3157,7 +3198,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 38, + "seq": 39, "time": 0, "data": { "turn": 1, @@ -3197,7 +3238,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 39, + "seq": 40, "time": 0, "data": { "turn": 1, @@ -3212,7 +3253,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 40, + "seq": 41, "time": 0, "data": { "turn": 1, @@ -3227,12 +3268,12 @@ "sessionId": "{{parent}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 41, + "seq": 42, "time": 0, "data": { "sessionId": "{{parent}}", "sessionFormatVersion": 3, - "throughSeq": 40 + "throughSeq": 41 } } } @@ -3243,7 +3284,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 42, + "seq": 43, "time": 0, "data": { "turn": 1, @@ -3339,7 +3380,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 43, + "seq": 44, "time": 0, "data": { "turn": 1, @@ -3357,7 +3398,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool-workflow/run-start", - "seq": 44, + "seq": 45, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -3455,13 +3496,30 @@ } } }, + { + "method": "session.event", + "payload": { + "sessionId": "{{parent}}", + "event": { + "type": "subagent/catalog", + "seq": 46, + "time": 0, + "data": { + "version": 0, + "childId": "{{child-2}}", + "childCreatedAt": 0, + "mode": "one-shot" + } + } + } + }, { "method": "session.event", "payload": { "sessionId": "{{parent}}", "event": { "type": "tool-workflow/agent-start", - "seq": 45, + "seq": 47, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -3845,7 +3903,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool-workflow/agent-end", - "seq": 46, + "seq": 48, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -3861,7 +3919,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool-workflow/run-end", - "seq": 47, + "seq": 49, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -3876,7 +3934,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 48, + "seq": 50, "time": 0, "data": { "turn": 1, @@ -3904,7 +3962,7 @@ } }, "sourceEventSeqs": [ - 43 + 44 ], "surfaceOp": "append" } @@ -3916,7 +3974,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 49, + "seq": 51, "time": 0, "data": { "turn": 1, @@ -3931,7 +3989,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 50, + "seq": 52, "time": 0, "data": { "turn": 1, @@ -3946,12 +4004,12 @@ "sessionId": "{{parent}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 51, + "seq": 53, "time": 0, "data": { "sessionId": "{{parent}}", "sessionFormatVersion": 3, - "throughSeq": 50 + "throughSeq": 52 } } } @@ -3962,7 +4020,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 52, + "seq": 54, "time": 0, "data": { "turn": 1, @@ -4058,7 +4116,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 53, + "seq": 55, "time": 0, "data": { "turn": 1, @@ -4076,7 +4134,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 54, + "seq": 56, "time": 0, "data": { "turn": 1, @@ -4104,7 +4162,7 @@ } }, "sourceEventSeqs": [ - 53 + 55 ], "surfaceOp": "append" } @@ -4116,7 +4174,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 55, + "seq": 57, "time": 0, "data": { "turn": 1, @@ -4131,7 +4189,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 56, + "seq": 58, "time": 0, "data": { "turn": 1, @@ -4146,7 +4204,7 @@ "sessionId": "{{parent}}", "event": { "type": "system/message", - "seq": 57, + "seq": 59, "time": 0, "data": { "turn": 1, @@ -4183,7 +4241,7 @@ "sessionId": "{{parent}}", "event": { "type": "request/header", - "seq": 58, + "seq": 60, "time": 0, "data": { "header": { @@ -4225,12 +4283,12 @@ "sessionId": "{{parent}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 59, + "seq": 61, "time": 0, "data": { "sessionId": "{{parent}}", "sessionFormatVersion": 3, - "throughSeq": 58 + "throughSeq": 60 } } } @@ -4241,7 +4299,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 60, + "seq": 62, "time": 0, "data": { "turn": 1, @@ -4331,7 +4389,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 61, + "seq": 63, "time": 0, "data": { "turn": 1, @@ -4346,7 +4404,7 @@ "sessionId": "{{parent}}", "event": { "type": "command/run", - "seq": 62, + "seq": 64, "time": 0, "data": { "commandId": "{{command:1}}", @@ -4364,7 +4422,7 @@ "sessionId": "{{parent}}", "event": { "type": "feedback/record", - "seq": 63, + "seq": 65, "time": 0, "data": { "text": "The session needs a clearer explanation." @@ -4378,7 +4436,7 @@ "sessionId": "{{parent}}", "event": { "type": "command/done", - "seq": 64, + "seq": 66, "time": 0, "data": { "commandId": "{{command:1}}", @@ -4394,7 +4452,7 @@ "sessionId": "{{parent}}", "event": { "type": "feedback/message-put", - "seq": 65, + "seq": 67, "time": 0, "data": { "sessionId": "{{parent}}", @@ -4416,7 +4474,7 @@ "sessionId": "{{parent}}", "event": { "type": "feedback/message-put", - "seq": 66, + "seq": 68, "time": 0, "data": { "sessionId": "{{parent}}", @@ -4438,7 +4496,7 @@ "sessionId": "{{parent}}", "event": { "type": "feedback/message-delete", - "seq": 67, + "seq": 69, "time": 0, "data": { "sessionId": "{{parent}}", @@ -4453,7 +4511,7 @@ "sessionId": "{{parent}}", "event": { "type": "turn/end", - "seq": 68, + "seq": 70, "time": 0, "data": { "turn": 1, diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.v2.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.v2.jsonl index a3c3e49f68..651f662c19 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.v2.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.v2.jsonl @@ -35,29 +35,27 @@ {"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":2,"throughSeq":32}} {"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-direct-child","name":"subagent","args":["{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":4,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{child-1}}","childCreatedAt":0,"mode":"one-shot","label":"Check direct child"}} {"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[35],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":4}} {"type":"step/start","data":{"turn":1,"step":5}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":2,"throughSeq":39}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":2,"throughSeq":38}} {"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-workflow","name":"workflow","args":["{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":5,"callId":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}} {"type":"tool-workflow/run-start","data":{"runId":"{{workflow-run}}","name":"advanced-exe-snapshot"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{child-2}}","childCreatedAt":0,"mode":"one-shot"}} {"type":"tool-workflow/agent-start","data":{"runId":"{{workflow-run}}","seq":1,"label":"workflow-child","phase":"Delegate","childId":"{{child-2}}"}} {"type":"tool-workflow/agent-end","data":{"runId":"{{workflow-run}}","seq":1,"outcome":"completed"}} {"type":"tool-workflow/run-end","data":{"runId":"{{workflow-run}}","stopReason":"completed"}} -{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[42],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[41],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":5}} {"type":"step/start","data":{"turn":1,"step":6}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":2,"throughSeq":50}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":2,"throughSeq":48}} {"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-undefine","name":"cordis_undefine","args":["{\"pluginId\": \"snap-1\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":6,"callId":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}} -{"type":"tool/result","data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[53],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[51],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":6}} {"type":"step/start","data":{"turn":1,"step":7}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"change"}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":2,"throughSeq":57}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":2,"throughSeq":55}} {"type":"assistant/message","data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{message:1}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["ADVANCED_EXECUTABLE_OK"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":7}} {"type":"command/run","data":{"commandId":"{{command:1}}","name":"feedback","source":{"kind":"user"}}} diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.v3.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.v3.jsonl index a10d2e9be7..2df45edf5b 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.v3.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.v3.jsonl @@ -37,28 +37,30 @@ {"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":34}} {"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-direct-child","name":"subagent","args":["{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":4,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{child-1}}","childCreatedAt":0,"mode":"one-shot","label":"Check direct child"}} {"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[37],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":4}} {"type":"step/start","data":{"turn":1,"step":5}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":40}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":41}} {"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-workflow","name":"workflow","args":["{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":5,"callId":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}} {"type":"tool-workflow/run-start","data":{"runId":"{{workflow-run}}","name":"advanced-exe-snapshot"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{child-2}}","childCreatedAt":0,"mode":"one-shot"}} {"type":"tool-workflow/agent-start","data":{"runId":"{{workflow-run}}","seq":1,"label":"workflow-child","phase":"Delegate","childId":"{{child-2}}"}} {"type":"tool-workflow/agent-end","data":{"runId":"{{workflow-run}}","seq":1,"outcome":"completed"}} {"type":"tool-workflow/run-end","data":{"runId":"{{workflow-run}}","stopReason":"completed"}} -{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[43],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[44],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":5}} {"type":"step/start","data":{"turn":1,"step":6}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":50}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":52}} {"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-undefine","name":"cordis_undefine","args":["{\"pluginId\": \"snap-1\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":6,"callId":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}} -{"type":"tool/result","data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[53],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[55],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":6}} {"type":"step/start","data":{"turn":1,"step":7}} {"type":"system/message","data":{"turn":1,"step":7,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{messageId}}"}},"sourceEventSeqs":[25],"surfaceOp":{"op":"replace","startSeq":25,"endSeq":25}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"change","startsSeries":true}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":58}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":60}} {"type":"assistant/message","data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{message:1}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["ADVANCED_EXECUTABLE_OK"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":7}} {"type":"command/run","data":{"commandId":"{{command:1}}","name":"feedback","source":{"kind":"user"}}} diff --git a/snapshots/sdk/subagent-continuable-inheritance/session.v2.jsonl b/snapshots/sdk/subagent-continuable-inheritance/session.v2.jsonl index 53e7059320..a4d9939d8d 100644 --- a/snapshots/sdk/subagent-continuable-inheritance/session.v2.jsonl +++ b/snapshots/sdk/subagent-continuable-inheritance/session.v2.jsonl @@ -12,13 +12,12 @@ {"type":"session/title","data":{"title":"Follow these steps exactly, then","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500533971,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500533971,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"]},{"type":"chunk","time":1788500533971,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}},{"type":"chunk","time":1788500533971,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500533971,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788199056889,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788199056889,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"]},{"type":"chunk","time":1788199056889,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}},{"type":"chunk","time":1788199056889,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788199056889,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500533982,"mode":"continuable","label":"Reply with CHILD_OK"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500534017,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500534017,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1788500534017,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1788500534017,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500534017,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788199056914,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788199056914,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1788199056914,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1788199056914,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788199056914,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:6}}"}]}} @@ -26,6 +25,6 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":2,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:6}}"},"surfaceOp":"append"} -{"type":"assistant/attempt","data":{"turn":2,"step":1,"stream":[{"type":"chunk","time":1788500534037,"chunk":{"type":"finish","reason":{"kind":"error","failure":{"message":"llm-replay: script exhausted — session requested model call #4 but its script has only 3; re-record the scenario","code":"UNKNOWN"}}}}]}} +{"type":"assistant/attempt","data":{"turn":2,"step":1,"stream":[{"type":"chunk","time":1788199056967,"chunk":{"type":"finish","reason":{"kind":"error","failure":{"message":"llm-replay: script exhausted — session requested model call #4 but its script has only 3; re-record the scenario","code":"UNKNOWN"}}}}]}} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"error","error":{"message":"llm-replay: script exhausted — session requested model call #4 but its script has only 3; re-record the scenario","code":"UNKNOWN"}}}} diff --git a/snapshots/sdk/subagent-continuable-inheritance/session.v3.jsonl b/snapshots/sdk/subagent-continuable-inheritance/session.v3.jsonl index 8eef8f7bd2..ecb8dcf95c 100644 --- a/snapshots/sdk/subagent-continuable-inheritance/session.v3.jsonl +++ b/snapshots/sdk/subagent-continuable-inheritance/session.v3.jsonl @@ -13,12 +13,13 @@ {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Follow these steps exactly, then","messageSeqs":[9],"source":{"kind":"fallback"}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788699459454,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699459454,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"]},{"type":"chunk","time":1788699459454,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}},{"type":"chunk","time":1788699459454,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788699459454,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788880385831,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880385831,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"]},{"type":"chunk","time":1788880385831,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}},{"type":"chunk","time":1788880385831,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788880385831,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788880385838,"mode":"continuable","label":"Reply with CHILD_OK"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788699459499,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788699459499,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1788699459499,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1788699459499,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788699459499,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788880385872,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788880385872,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1788880385872,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1788880385872,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788880385872,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:7}}"}]}} @@ -26,6 +27,6 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":2,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:7}}"},"surfaceOp":"append"} -{"type":"assistant/attempt","data":{"turn":2,"step":1,"stream":[{"type":"chunk","time":1788699459529,"chunk":{"type":"finish","reason":{"kind":"error","failure":{"message":"llm-replay: script exhausted — session requested model call #4 but its script has only 3; re-record the scenario","code":"UNKNOWN"}}}}]}} +{"type":"assistant/attempt","data":{"turn":2,"step":1,"stream":[{"type":"chunk","time":1788880385897,"chunk":{"type":"finish","reason":{"kind":"error","failure":{"message":"llm-replay: script exhausted — session requested model call #4 but its script has only 3; re-record the scenario","code":"UNKNOWN"}}}}]}} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"error","error":{"message":"llm-replay: script exhausted — session requested model call #4 but its script has only 3; re-record the scenario","code":"UNKNOWN"}}}} diff --git a/snapshots/sdk/subagent-continuable/session.v2.jsonl b/snapshots/sdk/subagent-continuable/session.v2.jsonl index fe561fdf71..99fa92dba8 100644 --- a/snapshots/sdk/subagent-continuable/session.v2.jsonl +++ b/snapshots/sdk/subagent-continuable/session.v2.jsonl @@ -11,28 +11,27 @@ {"type":"session/title","data":{"title":"Follow these steps exactly, then","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500520579,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500520579,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"]},{"type":"chunk","time":1788500520579,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}},{"type":"chunk","time":1788500520579,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500520579,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788269696592,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788269696592,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"]},{"type":"chunk","time":1788269696592,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}},{"type":"chunk","time":1788269696592,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788269696592,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500520585,"mode":"continuable","label":"Reply with CHILD_OK"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_1","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500520615,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500520615,"index":0,"dt":[],"id":"call_followup_1","name":"send_message","args":["{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly SECOND_OK.\"}"]},{"type":"chunk","time":1788500520615,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_1","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}}},{"type":"chunk","time":1788500520615,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500520615,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_1","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788269696618,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788269696618,"index":0,"dt":[],"id":"call_followup_1","name":"send_message","args":["{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly SECOND_OK.\"}"]},{"type":"chunk","time":1788269696618,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_1","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}}},{"type":"chunk","time":1788269696618,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788269696618,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"call_followup_1","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}} -{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_followup_1"},"content":[{"type":"tool-result","toolCallId":"call_followup_1","content":[{"type":"text","text":"message delivered to agent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:6}}"}},"sourceEventSeqs":[19],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_followup_1"},"content":[{"type":"tool-result","toolCallId":"call_followup_1","content":[{"type":"text","text":"message delivered to agent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:6}}"}},"sourceEventSeqs":[18],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} -{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_2","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500520635,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500520635,"index":0,"dt":[],"id":"call_followup_2","name":"send_message","args":["{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly THIRD_OK.\"}"]},{"type":"chunk","time":1788500520635,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_2","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}}},{"type":"chunk","time":1788500520635,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500520635,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_2","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788269696641,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788269696642,"index":0,"dt":[],"id":"call_followup_2","name":"send_message","args":["{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly THIRD_OK.\"}"]},{"type":"chunk","time":1788269696642,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_2","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}}},{"type":"chunk","time":1788269696642,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788269696642,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":3,"callId":"call_followup_2","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}} -{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"call_followup_2"},"content":[{"type":"tool-result","toolCallId":"call_followup_2","content":[{"type":"text","text":"message delivered to agent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:8}}"}},"sourceEventSeqs":[24],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"call_followup_2"},"content":[{"type":"tool-result","toolCallId":"call_followup_2","content":[{"type":"text","text":"message delivered to agent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:8}}"}},"sourceEventSeqs":[23],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} {"type":"step/start","data":{"turn":1,"step":4}} -{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_unknown","name":"send_message","arguments":"{\"agent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500520648,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500520648,"index":0,"dt":[],"id":"call_followup_unknown","name":"send_message","args":["{\"agent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"]},{"type":"chunk","time":1788500520648,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_unknown","name":"send_message","arguments":"{\"agent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}}},{"type":"chunk","time":1788500520648,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500520648,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_unknown","name":"send_message","arguments":"{\"agent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788269696663,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788269696664,"index":0,"dt":[],"id":"call_followup_unknown","name":"send_message","args":["{\"agent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"]},{"type":"chunk","time":1788269696664,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_unknown","name":"send_message","arguments":"{\"agent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}}},{"type":"chunk","time":1788269696664,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788269696664,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":4,"callId":"call_followup_unknown","name":"send_message","arguments":"{\"agent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}} -{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"call_followup_unknown"},"content":[{"type":"tool-result","toolCallId":"call_followup_unknown","content":[{"type":"text","text":"Error: subagent \"22222222-2222-4222-8222-222222222222\" is unavailable"}],"isError":true}],"role":"user","id":"{{message:10}}"},"error":{"name":"SubagentError","code":"NOT_RESUMABLE"}},"sourceEventSeqs":[29],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"call_followup_unknown"},"content":[{"type":"tool-result","toolCallId":"call_followup_unknown","content":[{"type":"text","text":"Error: subagent \"22222222-2222-4222-8222-222222222222\" is unavailable"}],"isError":true}],"role":"user","id":"{{message:10}}"},"error":{"name":"SubagentError","code":"NOT_RESUMABLE"}},"sourceEventSeqs":[28],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":4}} {"type":"step/start","data":{"turn":1,"step":5}} -{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500520662,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500520662,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1788500520662,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1788500520662,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500520662,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788269696680,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788269696680,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1788269696681,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1788269696681,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788269696681,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":5}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"SECOND_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:12}}"}]}} @@ -40,6 +39,6 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":2,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"SECOND_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:12}}"},"surfaceOp":"append"} -{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:13}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500520702,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500520702,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1788500520702,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1788500520702,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500520702,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:13}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788269696729,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788269696729,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1788269696729,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1788269696729,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788269696729,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}} diff --git a/snapshots/sdk/subagent-continuable/session.v3.jsonl b/snapshots/sdk/subagent-continuable/session.v3.jsonl index 00c493369a..7f468bd134 100644 --- a/snapshots/sdk/subagent-continuable/session.v3.jsonl +++ b/snapshots/sdk/subagent-continuable/session.v3.jsonl @@ -12,27 +12,28 @@ {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Follow these steps exactly, then","messageSeqs":[8],"source":{"kind":"fallback"}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788699458675,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699458675,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"]},{"type":"chunk","time":1788699458675,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}},{"type":"chunk","time":1788699458675,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788699458675,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788880385121,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880385122,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"]},{"type":"chunk","time":1788880385122,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}},{"type":"chunk","time":1788880385122,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788880385122,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788880385127,"mode":"continuable","label":"Reply with CHILD_OK"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_1","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788699458716,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699458716,"index":0,"dt":[],"id":"call_followup_1","name":"send_message","args":["{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly SECOND_OK.\"}"]},{"type":"chunk","time":1788699458716,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_1","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}}},{"type":"chunk","time":1788699458716,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788699458716,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_1","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788880385160,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880385160,"index":0,"dt":[],"id":"call_followup_1","name":"send_message","args":["{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly SECOND_OK.\"}"]},{"type":"chunk","time":1788880385160,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_1","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}}},{"type":"chunk","time":1788880385160,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788880385160,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"call_followup_1","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}} -{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_followup_1"},"content":[{"type":"tool-result","toolCallId":"call_followup_1","content":[{"type":"text","text":"message delivered to agent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:7}}"}},"sourceEventSeqs":[19],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_followup_1"},"content":[{"type":"tool-result","toolCallId":"call_followup_1","content":[{"type":"text","text":"message delivered to agent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:7}}"}},"sourceEventSeqs":[20],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} -{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_2","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788699458736,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699458736,"index":0,"dt":[],"id":"call_followup_2","name":"send_message","args":["{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly THIRD_OK.\"}"]},{"type":"chunk","time":1788699458736,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_2","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}}},{"type":"chunk","time":1788699458736,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788699458736,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_2","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788880385181,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880385181,"index":0,"dt":[],"id":"call_followup_2","name":"send_message","args":["{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly THIRD_OK.\"}"]},{"type":"chunk","time":1788880385181,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_2","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}}},{"type":"chunk","time":1788880385181,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788880385181,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":3,"callId":"call_followup_2","name":"send_message","arguments":"{\"agent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}} -{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"call_followup_2"},"content":[{"type":"tool-result","toolCallId":"call_followup_2","content":[{"type":"text","text":"message delivered to agent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:9}}"}},"sourceEventSeqs":[24],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"call_followup_2"},"content":[{"type":"tool-result","toolCallId":"call_followup_2","content":[{"type":"text","text":"message delivered to agent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:9}}"}},"sourceEventSeqs":[25],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} {"type":"step/start","data":{"turn":1,"step":4}} -{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_unknown","name":"send_message","arguments":"{\"agent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788699458750,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699458750,"index":0,"dt":[],"id":"call_followup_unknown","name":"send_message","args":["{\"agent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"]},{"type":"chunk","time":1788699458750,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_unknown","name":"send_message","arguments":"{\"agent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}}},{"type":"chunk","time":1788699458750,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788699458751,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_unknown","name":"send_message","arguments":"{\"agent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788880385198,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880385198,"index":0,"dt":[],"id":"call_followup_unknown","name":"send_message","args":["{\"agent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"]},{"type":"chunk","time":1788880385198,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_unknown","name":"send_message","arguments":"{\"agent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}}},{"type":"chunk","time":1788880385198,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788880385198,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":4,"callId":"call_followup_unknown","name":"send_message","arguments":"{\"agent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}} -{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"call_followup_unknown"},"content":[{"type":"tool-result","toolCallId":"call_followup_unknown","content":[{"type":"text","text":"Error: subagent \"22222222-2222-4222-8222-222222222222\" is unavailable"}],"isError":true}],"role":"user","id":"{{message:11}}"},"error":{"name":"SubagentError","code":"NOT_RESUMABLE"}},"sourceEventSeqs":[29],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"call_followup_unknown"},"content":[{"type":"tool-result","toolCallId":"call_followup_unknown","content":[{"type":"text","text":"Error: subagent \"22222222-2222-4222-8222-222222222222\" is unavailable"}],"isError":true}],"role":"user","id":"{{message:11}}"},"error":{"name":"SubagentError","code":"NOT_RESUMABLE"}},"sourceEventSeqs":[30],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":4}} {"type":"step/start","data":{"turn":1,"step":5}} -{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:12}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788699458771,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788699458771,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1788699458771,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1788699458771,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788699458771,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:12}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788880385215,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788880385215,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1788880385215,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1788880385215,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788880385215,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":5}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"SECOND_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:13}}"}]}} @@ -40,6 +41,6 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":2,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"SECOND_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:13}}"},"surfaceOp":"append"} -{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:14}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788699458821,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788699458821,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1788699458821,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1788699458821,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788699458821,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:14}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788880385266,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788880385266,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1788880385266,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1788880385266,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788880385266,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}} diff --git a/snapshots/sdk/subagent-fork-in-process/session.v2.jsonl b/snapshots/sdk/subagent-fork-in-process/session.v2.jsonl index f9f4398bfc..5f0fdd60e1 100644 --- a/snapshots/sdk/subagent-fork-in-process/session.v2.jsonl +++ b/snapshots/sdk/subagent-fork-in-process/session.v2.jsonl @@ -11,7 +11,7 @@ {"type":"session/title","data":{"title":"Remember this fact for later:","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to remember the codeword \"MARMALADE\" and reply with just \"OK\"."},{"type":"text","text":"OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":2885,"outputTokens":25,"cacheReadTokens":0,"reasoningTokens":23},"stream":[{"type":"chunk","time":1788500537271,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788500537272,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," remember"," the"," cod","ew","ord"," \"","M","ARM","AL","ADE","\""," and"," reply"," with"," just"," \"","OK","\"."]},{"type":"chunk","time":1788500537272,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788500537272,"index":1,"dt":[],"texts":["OK"]},{"type":"chunk","time":1788500537272,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to remember the codeword \"MARMALADE\" and reply with just \"OK\"."}}},{"type":"chunk","time":1788500537272,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"OK"}}},{"type":"chunk","time":1788500537272,"chunk":{"type":"usage","usage":{"inputTokens":2885,"outputTokens":25,"cacheReadTokens":0,"reasoningTokens":23}}},{"type":"chunk","time":1788500537272,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to remember the codeword \"MARMALADE\" and reply with just \"OK\"."},{"type":"text","text":"OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":2885,"outputTokens":25,"cacheReadTokens":0,"reasoningTokens":23},"stream":[{"type":"chunk","time":1788199063113,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788199063113,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," remember"," the"," cod","ew","ord"," \"","M","ARM","AL","ADE","\""," and"," reply"," with"," just"," \"","OK","\"."]},{"type":"chunk","time":1788199063113,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788199063113,"index":1,"dt":[],"texts":["OK"]},{"type":"chunk","time":1788199063113,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to remember the codeword \"MARMALADE\" and reply with just \"OK\"."}}},{"type":"chunk","time":1788199063113,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"OK"}}},{"type":"chunk","time":1788199063113,"chunk":{"type":"usage","usage":{"inputTokens":2885,"outputTokens":25,"cacheReadTokens":0,"reasoningTokens":23}}},{"type":"chunk","time":1788199063113,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Use the subagent_fork tool exactly once to delegate this subtask to a forked child agent: 'What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.' The forked child inherits this conversation, so it can answer. After the subagent returns, reply with the single word PARENT_DONE and stop. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"{{message:4}}"}]}} @@ -19,12 +19,11 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":2,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Use the subagent_fork tool exactly once to delegate this subtask to a forked child agent: 'What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.' The forked child inherits this conversation, so it can answer. After the subagent returns, reply with the single word PARENT_DONE and stop. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"{{message:4}}"},"surfaceOp":"append"} -{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to use subagent_fork to delegate a question to a child agent. The child agent inherits this conversation and should be able to answer: the project codeword is MARMALADE. After the subagent returns, I should reply with PARENT_DONE."},{"type":"tool-call","id":"call_00_sAtKUseRzHRBvL4CF7XF1334","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":158,"outputTokens":147,"cacheReadTokens":2816,"reasoningTokens":59},"stream":[{"type":"chunk","time":1788500537291,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788500537291,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," use"," sub","agent","_f","ork"," to"," delegate"," a"," question"," to"," a"," child"," agent","."," The"," child"," agent"," inher","its"," this"," conversation"," and"," should"," be"," able"," to"," answer",":"," the"," project"," cod","ew","ord"," is"," MAR","M","AL","ADE","."," After"," the"," sub","agent"," returns",","," I"," should"," reply"," with"," PAR","ENT","_D","ONE","."]},{"type":"chunk","time":1788500537291,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500537291,"index":1,"dt":[0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_sAtKUseRzHRBvL4CF7XF1334","name":"subagent_fork","args":["","{","\"","description","\"",": ","\"","Recall"," project"," cod","ew","ord","\"",", ","\"","prom","pt","\"",": ","\"","What"," is"," the"," project"," cod","ew","ord"," mentioned"," earlier"," in"," this"," conversation","?"," Reply"," with"," exactly"," that"," one"," word"," and"," nothing"," else",".","\"","}"]},{"type":"chunk","time":1788500537292,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to use subagent_fork to delegate a question to a child agent. The child agent inherits this conversation and should be able to answer: the project codeword is MARMALADE. After the subagent returns, I should reply with PARENT_DONE."}}},{"type":"chunk","time":1788500537292,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_sAtKUseRzHRBvL4CF7XF1334","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}}},{"type":"chunk","time":1788500537292,"chunk":{"type":"usage","usage":{"inputTokens":158,"outputTokens":147,"cacheReadTokens":2816,"reasoningTokens":59}}},{"type":"chunk","time":1788500537292,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to use subagent_fork to delegate a question to a child agent. The child agent inherits this conversation and should be able to answer: the project codeword is MARMALADE. After the subagent returns, I should reply with PARENT_DONE."},{"type":"tool-call","id":"call_00_sAtKUseRzHRBvL4CF7XF1334","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":158,"outputTokens":147,"cacheReadTokens":2816,"reasoningTokens":59},"stream":[{"type":"chunk","time":1788199063133,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788199063133,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," use"," sub","agent","_f","ork"," to"," delegate"," a"," question"," to"," a"," child"," agent","."," The"," child"," agent"," inher","its"," this"," conversation"," and"," should"," be"," able"," to"," answer",":"," the"," project"," cod","ew","ord"," is"," MAR","M","AL","ADE","."," After"," the"," sub","agent"," returns",","," I"," should"," reply"," with"," PAR","ENT","_D","ONE","."]},{"type":"chunk","time":1788199063133,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788199063133,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_sAtKUseRzHRBvL4CF7XF1334","name":"subagent_fork","args":["","{","\"","description","\"",": ","\"","Recall"," project"," cod","ew","ord","\"",", ","\"","prom","pt","\"",": ","\"","What"," is"," the"," project"," cod","ew","ord"," mentioned"," earlier"," in"," this"," conversation","?"," Reply"," with"," exactly"," that"," one"," word"," and"," nothing"," else",".","\"","}"]},{"type":"chunk","time":1788199063133,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to use subagent_fork to delegate a question to a child agent. The child agent inherits this conversation and should be able to answer: the project codeword is MARMALADE. After the subagent returns, I should reply with PARENT_DONE."}}},{"type":"chunk","time":1788199063133,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_sAtKUseRzHRBvL4CF7XF1334","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}}},{"type":"chunk","time":1788199063133,"chunk":{"type":"usage","usage":{"inputTokens":158,"outputTokens":147,"cacheReadTokens":2816,"reasoningTokens":59}}},{"type":"chunk","time":1788199063133,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":2,"step":1,"callId":"call_00_sAtKUseRzHRBvL4CF7XF1334","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500537300,"mode":"one-shot","label":"Recall project codeword"}} {"type":"tool/result","data":{"turn":2,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_sAtKUseRzHRBvL4CF7XF1334"},"content":[{"type":"tool-result","toolCallId":"call_00_sAtKUseRzHRBvL4CF7XF1334","content":[{"type":"text","text":"MARMALADE"}],"isError":false}],"role":"user","id":"{{message:6}}"}},"sourceEventSeqs":[21],"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"step/start","data":{"turn":2,"step":2}} -{"type":"assistant/message","data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The forked child agent correctly returned \"MARMALADE\". Now I need to reply with \"PARENT_DONE\"."},{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":65,"outputTokens":30,"cacheReadTokens":3072,"reasoningTokens":25},"stream":[{"type":"chunk","time":1788500537363,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788500537363,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," for","ked"," child"," agent"," correctly"," returned"," \"","M","ARM","AL","ADE","\"."," Now"," I"," need"," to"," reply"," with"," \"","PAR","ENT","_D","ONE","\"."]},{"type":"chunk","time":1788500537363,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788500537363,"index":1,"dt":[0,0,0],"texts":["PAR","ENT","_D","ONE"]},{"type":"chunk","time":1788500537363,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The forked child agent correctly returned \"MARMALADE\". Now I need to reply with \"PARENT_DONE\"."}}},{"type":"chunk","time":1788500537363,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788500537363,"chunk":{"type":"usage","usage":{"inputTokens":65,"outputTokens":30,"cacheReadTokens":3072,"reasoningTokens":25}}},{"type":"chunk","time":1788500537363,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The forked child agent correctly returned \"MARMALADE\". Now I need to reply with \"PARENT_DONE\"."},{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":65,"outputTokens":30,"cacheReadTokens":3072,"reasoningTokens":25},"stream":[{"type":"chunk","time":1788199063200,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788199063200,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," for","ked"," child"," agent"," correctly"," returned"," \"","M","ARM","AL","ADE","\"."," Now"," I"," need"," to"," reply"," with"," \"","PAR","ENT","_D","ONE","\"."]},{"type":"chunk","time":1788199063200,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788199063200,"index":1,"dt":[0,0,0],"texts":["PAR","ENT","_D","ONE"]},{"type":"chunk","time":1788199063200,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The forked child agent correctly returned \"MARMALADE\". Now I need to reply with \"PARENT_DONE\"."}}},{"type":"chunk","time":1788199063200,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788199063200,"chunk":{"type":"usage","usage":{"inputTokens":65,"outputTokens":30,"cacheReadTokens":3072,"reasoningTokens":25}}},{"type":"chunk","time":1788199063200,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":2}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}} diff --git a/snapshots/sdk/subagent-fork-in-process/session.v3.jsonl b/snapshots/sdk/subagent-fork-in-process/session.v3.jsonl index d2b77d95ba..7cd5e51fb9 100644 --- a/snapshots/sdk/subagent-fork-in-process/session.v3.jsonl +++ b/snapshots/sdk/subagent-fork-in-process/session.v3.jsonl @@ -12,7 +12,7 @@ {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Remember this fact for later:","messageSeqs":[8],"source":{"kind":"fallback"}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to remember the codeword \"MARMALADE\" and reply with just \"OK\"."},{"type":"text","text":"OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":2885,"outputTokens":25,"cacheReadTokens":0,"reasoningTokens":23},"stream":[{"type":"chunk","time":1788699463198,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788699463198,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," remember"," the"," cod","ew","ord"," \"","M","ARM","AL","ADE","\""," and"," reply"," with"," just"," \"","OK","\"."]},{"type":"chunk","time":1788699463198,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788699463198,"index":1,"dt":[],"texts":["OK"]},{"type":"chunk","time":1788699463198,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to remember the codeword \"MARMALADE\" and reply with just \"OK\"."}}},{"type":"chunk","time":1788699463198,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"OK"}}},{"type":"chunk","time":1788699463198,"chunk":{"type":"usage","usage":{"inputTokens":2885,"outputTokens":25,"cacheReadTokens":0,"reasoningTokens":23}}},{"type":"chunk","time":1788699463198,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to remember the codeword \"MARMALADE\" and reply with just \"OK\"."},{"type":"text","text":"OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":2885,"outputTokens":25,"cacheReadTokens":0,"reasoningTokens":23},"stream":[{"type":"chunk","time":1788880388996,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788880388996,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," remember"," the"," cod","ew","ord"," \"","M","ARM","AL","ADE","\""," and"," reply"," with"," just"," \"","OK","\"."]},{"type":"chunk","time":1788880388996,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788880388996,"index":1,"dt":[],"texts":["OK"]},{"type":"chunk","time":1788880388997,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to remember the codeword \"MARMALADE\" and reply with just \"OK\"."}}},{"type":"chunk","time":1788880388997,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"OK"}}},{"type":"chunk","time":1788880388997,"chunk":{"type":"usage","usage":{"inputTokens":2885,"outputTokens":25,"cacheReadTokens":0,"reasoningTokens":23}}},{"type":"chunk","time":1788880388997,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Use the subagent_fork tool exactly once to delegate this subtask to a forked child agent: 'What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.' The forked child inherits this conversation, so it can answer. After the subagent returns, reply with the single word PARENT_DONE and stop. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"{{message:5}}"}]}} @@ -20,11 +20,12 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":2,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Use the subagent_fork tool exactly once to delegate this subtask to a forked child agent: 'What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.' The forked child inherits this conversation, so it can answer. After the subagent returns, reply with the single word PARENT_DONE and stop. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"{{message:5}}"},"surfaceOp":"append"} -{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to use subagent_fork to delegate a question to a child agent. The child agent inherits this conversation and should be able to answer: the project codeword is MARMALADE. After the subagent returns, I should reply with PARENT_DONE."},{"type":"tool-call","id":"call_00_sAtKUseRzHRBvL4CF7XF1334","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":158,"outputTokens":147,"cacheReadTokens":2816,"reasoningTokens":59},"stream":[{"type":"chunk","time":1788699463219,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788699463219,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," use"," sub","agent","_f","ork"," to"," delegate"," a"," question"," to"," a"," child"," agent","."," The"," child"," agent"," inher","its"," this"," conversation"," and"," should"," be"," able"," to"," answer",":"," the"," project"," cod","ew","ord"," is"," MAR","M","AL","ADE","."," After"," the"," sub","agent"," returns",","," I"," should"," reply"," with"," PAR","ENT","_D","ONE","."]},{"type":"chunk","time":1788699463220,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699463220,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_sAtKUseRzHRBvL4CF7XF1334","name":"subagent_fork","args":["","{","\"","description","\"",": ","\"","Recall"," project"," cod","ew","ord","\"",", ","\"","prom","pt","\"",": ","\"","What"," is"," the"," project"," cod","ew","ord"," mentioned"," earlier"," in"," this"," conversation","?"," Reply"," with"," exactly"," that"," one"," word"," and"," nothing"," else",".","\"","}"]},{"type":"chunk","time":1788699463220,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to use subagent_fork to delegate a question to a child agent. The child agent inherits this conversation and should be able to answer: the project codeword is MARMALADE. After the subagent returns, I should reply with PARENT_DONE."}}},{"type":"chunk","time":1788699463220,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_sAtKUseRzHRBvL4CF7XF1334","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}}},{"type":"chunk","time":1788699463220,"chunk":{"type":"usage","usage":{"inputTokens":158,"outputTokens":147,"cacheReadTokens":2816,"reasoningTokens":59}}},{"type":"chunk","time":1788699463220,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to use subagent_fork to delegate a question to a child agent. The child agent inherits this conversation and should be able to answer: the project codeword is MARMALADE. After the subagent returns, I should reply with PARENT_DONE."},{"type":"tool-call","id":"call_00_sAtKUseRzHRBvL4CF7XF1334","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":158,"outputTokens":147,"cacheReadTokens":2816,"reasoningTokens":59},"stream":[{"type":"chunk","time":1788880389015,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788880389015,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," use"," sub","agent","_f","ork"," to"," delegate"," a"," question"," to"," a"," child"," agent","."," The"," child"," agent"," inher","its"," this"," conversation"," and"," should"," be"," able"," to"," answer",":"," the"," project"," cod","ew","ord"," is"," MAR","M","AL","ADE","."," After"," the"," sub","agent"," returns",","," I"," should"," reply"," with"," PAR","ENT","_D","ONE","."]},{"type":"chunk","time":1788880389015,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880389015,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_sAtKUseRzHRBvL4CF7XF1334","name":"subagent_fork","args":["","{","\"","description","\"",": ","\"","Recall"," project"," cod","ew","ord","\"",", ","\"","prom","pt","\"",": ","\"","What"," is"," the"," project"," cod","ew","ord"," mentioned"," earlier"," in"," this"," conversation","?"," Reply"," with"," exactly"," that"," one"," word"," and"," nothing"," else",".","\"","}"]},{"type":"chunk","time":1788880389015,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to use subagent_fork to delegate a question to a child agent. The child agent inherits this conversation and should be able to answer: the project codeword is MARMALADE. After the subagent returns, I should reply with PARENT_DONE."}}},{"type":"chunk","time":1788880389015,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_sAtKUseRzHRBvL4CF7XF1334","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}}},{"type":"chunk","time":1788880389015,"chunk":{"type":"usage","usage":{"inputTokens":158,"outputTokens":147,"cacheReadTokens":2816,"reasoningTokens":59}}},{"type":"chunk","time":1788880389015,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":2,"step":1,"callId":"call_00_sAtKUseRzHRBvL4CF7XF1334","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788880389023,"mode":"one-shot","label":"Recall project codeword"}} {"type":"tool/result","data":{"turn":2,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_sAtKUseRzHRBvL4CF7XF1334"},"content":[{"type":"tool-result","toolCallId":"call_00_sAtKUseRzHRBvL4CF7XF1334","content":[{"type":"text","text":"MARMALADE"}],"isError":false}],"role":"user","id":"{{message:7}}"}},"sourceEventSeqs":[22],"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"step/start","data":{"turn":2,"step":2}} -{"type":"assistant/message","data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The forked child agent correctly returned \"MARMALADE\". Now I need to reply with \"PARENT_DONE\"."},{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":65,"outputTokens":30,"cacheReadTokens":3072,"reasoningTokens":25},"stream":[{"type":"chunk","time":1788699463283,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788699463283,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," for","ked"," child"," agent"," correctly"," returned"," \"","M","ARM","AL","ADE","\"."," Now"," I"," need"," to"," reply"," with"," \"","PAR","ENT","_D","ONE","\"."]},{"type":"chunk","time":1788699463283,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788699463283,"index":1,"dt":[0,0,0],"texts":["PAR","ENT","_D","ONE"]},{"type":"chunk","time":1788699463283,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The forked child agent correctly returned \"MARMALADE\". Now I need to reply with \"PARENT_DONE\"."}}},{"type":"chunk","time":1788699463283,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788699463283,"chunk":{"type":"usage","usage":{"inputTokens":65,"outputTokens":30,"cacheReadTokens":3072,"reasoningTokens":25}}},{"type":"chunk","time":1788699463283,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The forked child agent correctly returned \"MARMALADE\". Now I need to reply with \"PARENT_DONE\"."},{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":65,"outputTokens":30,"cacheReadTokens":3072,"reasoningTokens":25},"stream":[{"type":"chunk","time":1788880389071,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788880389071,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," for","ked"," child"," agent"," correctly"," returned"," \"","M","ARM","AL","ADE","\"."," Now"," I"," need"," to"," reply"," with"," \"","PAR","ENT","_D","ONE","\"."]},{"type":"chunk","time":1788880389071,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788880389071,"index":1,"dt":[0,0,0],"texts":["PAR","ENT","_D","ONE"]},{"type":"chunk","time":1788880389071,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The forked child agent correctly returned \"MARMALADE\". Now I need to reply with \"PARENT_DONE\"."}}},{"type":"chunk","time":1788880389071,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788880389071,"chunk":{"type":"usage","usage":{"inputTokens":65,"outputTokens":30,"cacheReadTokens":3072,"reasoningTokens":25}}},{"type":"chunk","time":1788880389071,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":2}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}} diff --git a/snapshots/sdk/subagent-list-agents/session.v2.jsonl b/snapshots/sdk/subagent-list-agents/session.v2.jsonl index 685bafd019..bc0a67bfad 100644 --- a/snapshots/sdk/subagent-list-agents/session.v2.jsonl +++ b/snapshots/sdk/subagent-list-agents/session.v2.jsonl @@ -11,13 +11,12 @@ {"type":"session/title","data":{"title":"Call the subagent tool once","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500537942,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500537942,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"]},{"type":"chunk","time":1788500537942,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}},{"type":"chunk","time":1788500537942,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500537942,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788199064253,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788199064253,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"]},{"type":"chunk","time":1788199064253,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}},{"type":"chunk","time":1788199064253,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788199064253,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500537949,"mode":"continuable","label":"Reply with CHILD_OK"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500537988,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500537988,"index":0,"dt":[],"texts":["STARTED"]},{"type":"chunk","time":1788500537988,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"STARTED"}}},{"type":"chunk","time":1788500537988,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500537988,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788199064280,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788199064280,"index":0,"dt":[],"texts":["STARTED"]},{"type":"chunk","time":1788199064280,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"STARTED"}}},{"type":"chunk","time":1788199064280,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788199064280,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:6}}"}]}} @@ -25,7 +24,7 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":2,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:6}}"},"surfaceOp":"append"} -{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500538018,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500538018,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1788500538018,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1788500538018,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500538018,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788199064342,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788199064342,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1788199064342,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1788199064342,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788199064342,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Call list_agents once with scope set to descendants and observe the subagent you started. Then call interrupt_agent once with agent_id set to {{session:2}}. Then reply with the single word DONE. Do not call any other tool."}],"source":{"kind":"user"},"role":"user","id":"{{message:8}}"}]}} @@ -33,11 +32,11 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":3,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Call list_agents once with scope set to descendants and observe the subagent you started. Then call interrupt_agent once with agent_id set to {{session:2}}. Then reply with the single word DONE. Do not call any other tool."}],"source":{"kind":"user"},"role":"user","id":"{{message:8}}"},"surfaceOp":"append"} -{"type":"assistant/message","data":{"turn":3,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_list","name":"list_agents","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500538040,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500538040,"index":0,"dt":[],"id":"call_list","name":"list_agents","args":["{}"]},{"type":"chunk","time":1788500538040,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_list","name":"list_agents","arguments":"{}"}}},{"type":"chunk","time":1788500538040,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500538040,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":3,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_list","name":"list_agents","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788199064368,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788199064368,"index":0,"dt":[],"id":"call_list","name":"list_agents","args":["{}"]},{"type":"chunk","time":1788199064368,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_list","name":"list_agents","arguments":"{}"}}},{"type":"chunk","time":1788199064368,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788199064368,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":3,"step":1,"callId":"call_list","name":"list_agents","arguments":"{}"}} -{"type":"tool/result","data":{"turn":3,"step":1,"message":{"source":{"kind":"tool","callId":"call_list"},"content":[{"type":"tool-result","toolCallId":"call_list","content":[{"type":"text","text":"{{session:2}} [ready] — Reply with CHILD_OK"}],"isError":false}],"role":"user","id":"{{message:10}}"}},"sourceEventSeqs":[35],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":3,"step":1,"message":{"source":{"kind":"tool","callId":"call_list"},"content":[{"type":"tool-result","toolCallId":"call_list","content":[{"type":"text","text":"{{session:2}} [ready] — Reply with CHILD_OK"}],"isError":false}],"role":"user","id":"{{message:10}}"}},"sourceEventSeqs":[34],"surfaceOp":"append"} {"type":"step/end","data":{"turn":3,"step":1}} {"type":"step/start","data":{"turn":3,"step":2}} -{"type":"assistant/message","data":{"turn":3,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788500538061,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500538061,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1788500538061,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1788500538061,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788500538061,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":3,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788199064389,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788199064389,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1788199064389,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1788199064389,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788199064389,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":3,"step":2}} {"type":"turn/end","data":{"turn":3,"reason":{"kind":"completed"}}} diff --git a/snapshots/sdk/subagent-list-agents/session.v3.jsonl b/snapshots/sdk/subagent-list-agents/session.v3.jsonl index 15b957d58d..3721f02b72 100644 --- a/snapshots/sdk/subagent-list-agents/session.v3.jsonl +++ b/snapshots/sdk/subagent-list-agents/session.v3.jsonl @@ -12,12 +12,13 @@ {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Call the subagent tool once","messageSeqs":[8],"source":{"kind":"fallback"}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788699463855,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699463856,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"]},{"type":"chunk","time":1788699463856,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}},{"type":"chunk","time":1788699463856,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788699463856,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788880389574,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880389574,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"]},{"type":"chunk","time":1788880389574,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}},{"type":"chunk","time":1788880389574,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788880389574,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788880389580,"mode":"continuable","label":"Reply with CHILD_OK"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788699463893,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788699463893,"index":0,"dt":[],"texts":["STARTED"]},{"type":"chunk","time":1788699463893,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"STARTED"}}},{"type":"chunk","time":1788699463893,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788699463893,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788880389609,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788880389609,"index":0,"dt":[],"texts":["STARTED"]},{"type":"chunk","time":1788880389609,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"STARTED"}}},{"type":"chunk","time":1788880389609,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788880389609,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:7}}"}]}} @@ -25,7 +26,7 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":2,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:7}}"},"surfaceOp":"append"} -{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788699463937,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788699463937,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1788699463937,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1788699463937,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788699463937,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788880389644,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788880389644,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1788880389644,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1788880389644,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788880389644,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Call list_agents once with scope set to descendants and observe the subagent you started. Then call interrupt_agent once with agent_id set to {{session:2}}. Then reply with the single word DONE. Do not call any other tool."}],"source":{"kind":"user"},"role":"user","id":"{{message:9}}"}]}} @@ -33,11 +34,11 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":3,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Call list_agents once with scope set to descendants and observe the subagent you started. Then call interrupt_agent once with agent_id set to {{session:2}}. Then reply with the single word DONE. Do not call any other tool."}],"source":{"kind":"user"},"role":"user","id":"{{message:9}}"},"surfaceOp":"append"} -{"type":"assistant/message","data":{"turn":3,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_list","name":"list_agents","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788699463959,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699463959,"index":0,"dt":[],"id":"call_list","name":"list_agents","args":["{}"]},{"type":"chunk","time":1788699463959,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_list","name":"list_agents","arguments":"{}"}}},{"type":"chunk","time":1788699463959,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788699463959,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":3,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_list","name":"list_agents","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788880389662,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880389662,"index":0,"dt":[],"id":"call_list","name":"list_agents","args":["{}"]},{"type":"chunk","time":1788880389662,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_list","name":"list_agents","arguments":"{}"}}},{"type":"chunk","time":1788880389662,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788880389662,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":3,"step":1,"callId":"call_list","name":"list_agents","arguments":"{}"}} -{"type":"tool/result","data":{"turn":3,"step":1,"message":{"source":{"kind":"tool","callId":"call_list"},"content":[{"type":"tool-result","toolCallId":"call_list","content":[{"type":"text","text":"{{session:2}} [ready] — Reply with CHILD_OK"}],"isError":false}],"role":"user","id":"{{message:11}}"}},"sourceEventSeqs":[35],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":3,"step":1,"message":{"source":{"kind":"tool","callId":"call_list"},"content":[{"type":"tool-result","toolCallId":"call_list","content":[{"type":"text","text":"{{session:2}} [ready] — Reply with CHILD_OK"}],"isError":false}],"role":"user","id":"{{message:11}}"}},"sourceEventSeqs":[36],"surfaceOp":"append"} {"type":"step/end","data":{"turn":3,"step":1}} {"type":"step/start","data":{"turn":3,"step":2}} -{"type":"assistant/message","data":{"turn":3,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:12}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788699463980,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788699463980,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1788699463980,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1788699463980,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788699463980,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":3,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:12}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788880389684,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788880389684,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1788880389684,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1788880389684,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788880389684,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":3,"step":2}} {"type":"turn/end","data":{"turn":3,"reason":{"kind":"completed"}}} diff --git a/snapshots/sdk/subagent-mixed/session.v2.jsonl b/snapshots/sdk/subagent-mixed/session.v2.jsonl index 312dad21c5..b221888a97 100644 --- a/snapshots/sdk/subagent-mixed/session.v2.jsonl +++ b/snapshots/sdk/subagent-mixed/session.v2.jsonl @@ -11,7 +11,7 @@ {"type":"session/title","data":{"title":"Remember this fact for later:","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to remember a codeword and just reply with \"OK\"."},{"type":"text","text":"OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":2883,"outputTokens":19,"cacheReadTokens":0,"reasoningTokens":17},"stream":[{"type":"chunk","time":1788500538590,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788500538590,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," remember"," a"," cod","ew","ord"," and"," just"," reply"," with"," \"","OK","\"."]},{"type":"chunk","time":1788500538590,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788500538590,"index":1,"dt":[],"texts":["OK"]},{"type":"chunk","time":1788500538590,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to remember a codeword and just reply with \"OK\"."}}},{"type":"chunk","time":1788500538590,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"OK"}}},{"type":"chunk","time":1788500538590,"chunk":{"type":"usage","usage":{"inputTokens":2883,"outputTokens":19,"cacheReadTokens":0,"reasoningTokens":17}}},{"type":"chunk","time":1788500538590,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to remember a codeword and just reply with \"OK\"."},{"type":"text","text":"OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":2883,"outputTokens":19,"cacheReadTokens":0,"reasoningTokens":17},"stream":[{"type":"chunk","time":1788199065455,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788199065455,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," remember"," a"," cod","ew","ord"," and"," just"," reply"," with"," \"","OK","\"."]},{"type":"chunk","time":1788199065455,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788199065455,"index":1,"dt":[],"texts":["OK"]},{"type":"chunk","time":1788199065456,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to remember a codeword and just reply with \"OK\"."}}},{"type":"chunk","time":1788199065456,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"OK"}}},{"type":"chunk","time":1788199065456,"chunk":{"type":"usage","usage":{"inputTokens":2883,"outputTokens":19,"cacheReadTokens":0,"reasoningTokens":17}}},{"type":"chunk","time":1788199065456,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Do these two delegations, once at a time. First, use the subagent tool (fresh child) exactly once: 'Reply with exactly the word ALPHA and nothing else.' Then, after it returns, use the subagent_fork tool (forked child that inherits this conversation) exactly once: 'What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.' After both subagents return, reply with the single word PARENT_DONE and stop. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"{{message:4}}"}]}} @@ -19,18 +19,16 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":2,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Do these two delegations, once at a time. First, use the subagent tool (fresh child) exactly once: 'Reply with exactly the word ALPHA and nothing else.' Then, after it returns, use the subagent_fork tool (forked child that inherits this conversation) exactly once: 'What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.' After both subagents return, reply with the single word PARENT_DONE and stop. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"{{message:4}}"},"surfaceOp":"append"} -{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"Let me do these two delegations one at a time as requested.\n\nFirst, I'll use the subagent tool (fresh child) to reply with \"ALPHA\"."},{"type":"tool-call","id":"call_00_YvHr2bGomk5HhpgDTvE81896","name":"subagent","arguments":"{\"description\": \"Reply ALPHA only\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":185,"outputTokens":110,"cacheReadTokens":2816,"reasoningTokens":35},"stream":[{"type":"chunk","time":1788500538608,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788500538608,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["Let"," me"," do"," these"," two"," deleg","ations"," one"," at"," a"," time"," as"," requested",".\n\n","First",","," I","'ll"," use"," the"," sub","agent"," tool"," (","fresh"," child",")"," to"," reply"," with"," \"","AL","P","HA","\"."]},{"type":"chunk","time":1788500538608,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500538608,"index":1,"dt":[0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_YvHr2bGomk5HhpgDTvE81896","name":"subagent","args":["","{","\"","description","\"",": ","\"","Reply"," AL","P","HA"," only","\"",", ","\"","prom","pt","\"",": ","\"","Reply"," with"," exactly"," the"," word"," AL","P","HA"," and"," nothing"," else",".","\"",", \"run_in_background\":false}"]},{"type":"chunk","time":1788500538609,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Let me do these two delegations one at a time as requested.\n\nFirst, I'll use the subagent tool (fresh child) to reply with \"ALPHA\"."}}},{"type":"chunk","time":1788500538609,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_YvHr2bGomk5HhpgDTvE81896","name":"subagent","arguments":"{\"description\": \"Reply ALPHA only\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788500538609,"chunk":{"type":"usage","usage":{"inputTokens":185,"outputTokens":110,"cacheReadTokens":2816,"reasoningTokens":35}}},{"type":"chunk","time":1788500538609,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"Let me do these two delegations one at a time as requested.\n\nFirst, I'll use the subagent tool (fresh child) to reply with \"ALPHA\"."},{"type":"tool-call","id":"call_00_YvHr2bGomk5HhpgDTvE81896","name":"subagent","arguments":"{\"description\": \"Reply ALPHA only\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":185,"outputTokens":110,"cacheReadTokens":2816,"reasoningTokens":35},"stream":[{"type":"chunk","time":1788199065477,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788199065477,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["Let"," me"," do"," these"," two"," deleg","ations"," one"," at"," a"," time"," as"," requested",".\n\n","First",","," I","'ll"," use"," the"," sub","agent"," tool"," (","fresh"," child",")"," to"," reply"," with"," \"","AL","P","HA","\"."]},{"type":"chunk","time":1788199065477,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788199065477,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_YvHr2bGomk5HhpgDTvE81896","name":"subagent","args":["","{","\"","description","\"",": ","\"","Reply"," AL","P","HA"," only","\"",", ","\"","prom","pt","\"",": ","\"","Reply"," with"," exactly"," the"," word"," AL","P","HA"," and"," nothing"," else",".","\"",", \"run_in_background\":false}"]},{"type":"chunk","time":1788199065477,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Let me do these two delegations one at a time as requested.\n\nFirst, I'll use the subagent tool (fresh child) to reply with \"ALPHA\"."}}},{"type":"chunk","time":1788199065477,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_YvHr2bGomk5HhpgDTvE81896","name":"subagent","arguments":"{\"description\": \"Reply ALPHA only\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788199065477,"chunk":{"type":"usage","usage":{"inputTokens":185,"outputTokens":110,"cacheReadTokens":2816,"reasoningTokens":35}}},{"type":"chunk","time":1788199065477,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":2,"step":1,"callId":"call_00_YvHr2bGomk5HhpgDTvE81896","name":"subagent","arguments":"{\"description\": \"Reply ALPHA only\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500538615,"mode":"one-shot","label":"Reply ALPHA only"}} {"type":"tool/result","data":{"turn":2,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_YvHr2bGomk5HhpgDTvE81896"},"content":[{"type":"tool-result","toolCallId":"call_00_YvHr2bGomk5HhpgDTvE81896","content":[{"type":"text","text":"ALPHA"}],"isError":false}],"role":"user","id":"{{message:6}}"}},"sourceEventSeqs":[21],"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"step/start","data":{"turn":2,"step":2}} -{"type":"assistant/message","data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The first subagent returned \"ALPHA\". Now I need to use the subagent_fork tool (forked child that inherits this conversation) to ask about the project codeword."},{"type":"tool-call","id":"call_00_JSr5rhREq23wSmwSkCP77184","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":54,"outputTokens":128,"cacheReadTokens":3072,"reasoningTokens":40},"stream":[{"type":"chunk","time":1788500538670,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788500538670,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," first"," sub","agent"," returned"," \"","AL","P","HA","\"."," Now"," I"," need"," to"," use"," the"," sub","agent","_f","ork"," tool"," (","fork","ed"," child"," that"," inher","its"," this"," conversation",")"," to"," ask"," about"," the"," project"," cod","ew","ord","."]},{"type":"chunk","time":1788500538670,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500538670,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0],"id":"call_00_JSr5rhREq23wSmwSkCP77184","name":"subagent_fork","args":["","{","\"","description","\"",": ","\"","Recall"," project"," cod","ew","ord","\"",", ","\"","prom","pt","\"",": ","\"","What"," is"," the"," project"," cod","ew","ord"," mentioned"," earlier"," in"," this"," conversation","?"," Reply"," with"," exactly"," that"," one"," word"," and"," nothing"," else",".","\"","}"]},{"type":"chunk","time":1788500538671,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The first subagent returned \"ALPHA\". Now I need to use the subagent_fork tool (forked child that inherits this conversation) to ask about the project codeword."}}},{"type":"chunk","time":1788500538671,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_JSr5rhREq23wSmwSkCP77184","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}}},{"type":"chunk","time":1788500538671,"chunk":{"type":"usage","usage":{"inputTokens":54,"outputTokens":128,"cacheReadTokens":3072,"reasoningTokens":40}}},{"type":"chunk","time":1788500538671,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The first subagent returned \"ALPHA\". Now I need to use the subagent_fork tool (forked child that inherits this conversation) to ask about the project codeword."},{"type":"tool-call","id":"call_00_JSr5rhREq23wSmwSkCP77184","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":54,"outputTokens":128,"cacheReadTokens":3072,"reasoningTokens":40},"stream":[{"type":"chunk","time":1788199065535,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788199065535,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," first"," sub","agent"," returned"," \"","AL","P","HA","\"."," Now"," I"," need"," to"," use"," the"," sub","agent","_f","ork"," tool"," (","fork","ed"," child"," that"," inher","its"," this"," conversation",")"," to"," ask"," about"," the"," project"," cod","ew","ord","."]},{"type":"chunk","time":1788199065535,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788199065535,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_JSr5rhREq23wSmwSkCP77184","name":"subagent_fork","args":["","{","\"","description","\"",": ","\"","Recall"," project"," cod","ew","ord","\"",", ","\"","prom","pt","\"",": ","\"","What"," is"," the"," project"," cod","ew","ord"," mentioned"," earlier"," in"," this"," conversation","?"," Reply"," with"," exactly"," that"," one"," word"," and"," nothing"," else",".","\"","}"]},{"type":"chunk","time":1788199065535,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The first subagent returned \"ALPHA\". Now I need to use the subagent_fork tool (forked child that inherits this conversation) to ask about the project codeword."}}},{"type":"chunk","time":1788199065535,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_JSr5rhREq23wSmwSkCP77184","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}}},{"type":"chunk","time":1788199065535,"chunk":{"type":"usage","usage":{"inputTokens":54,"outputTokens":128,"cacheReadTokens":3072,"reasoningTokens":40}}},{"type":"chunk","time":1788199065535,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":2,"step":2,"callId":"call_00_JSr5rhREq23wSmwSkCP77184","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:3}}","childCreatedAt":1788500538677,"mode":"one-shot","label":"Recall project codeword"}} -{"type":"tool/result","data":{"turn":2,"step":2,"message":{"source":{"kind":"tool","callId":"call_00_JSr5rhREq23wSmwSkCP77184"},"content":[{"type":"tool-result","toolCallId":"call_00_JSr5rhREq23wSmwSkCP77184","content":[{"type":"text","text":"SAFFRON"}],"isError":false}],"role":"user","id":"{{message:8}}"}},"sourceEventSeqs":[27],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":2,"step":2,"message":{"source":{"kind":"tool","callId":"call_00_JSr5rhREq23wSmwSkCP77184"},"content":[{"type":"tool-result","toolCallId":"call_00_JSr5rhREq23wSmwSkCP77184","content":[{"type":"text","text":"SAFFRON"}],"isError":false}],"role":"user","id":"{{message:8}}"}},"sourceEventSeqs":[26],"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":2}} {"type":"step/start","data":{"turn":2,"step":3}} -{"type":"assistant/message","data":{"turn":2,"step":3,"message":{"role":"assistant","content":[{"type":"reasoning","text":"Both subagents returned:\n1. First (fresh child): \"ALPHA\"\n2. Second (forked child): \"SAFFRON\" - correctly inherited the conversation context where I was asked to remember the codeword \"SAFFRON\".\n\nNow I reply with \"PARENT_DONE\" as instructed."},{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":69,"outputTokens":69,"cacheReadTokens":3200,"reasoningTokens":64},"stream":[{"type":"chunk","time":1788500538725,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788500538725,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["Both"," sub","agents"," returned",":\n","1","."," First"," (","fresh"," child","):"," \"","AL","P","HA","\"\n","2","."," Second"," (","fork","ed"," child","):"," \"","SA","FF","RON","\""," -"," correctly"," inherited"," the"," conversation"," context"," where"," I"," was"," asked"," to"," remember"," the"," cod","ew","ord"," \"","SA","FF","RON","\".\n\n","Now"," I"," reply"," with"," \"","PAR","ENT","_D","ONE","\""," as"," instructed","."]},{"type":"chunk","time":1788500538725,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788500538725,"index":1,"dt":[0,0,0],"texts":["PAR","ENT","_D","ONE"]},{"type":"chunk","time":1788500538725,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Both subagents returned:\n1. First (fresh child): \"ALPHA\"\n2. Second (forked child): \"SAFFRON\" - correctly inherited the conversation context where I was asked to remember the codeword \"SAFFRON\".\n\nNow I reply with \"PARENT_DONE\" as instructed."}}},{"type":"chunk","time":1788500538725,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788500538725,"chunk":{"type":"usage","usage":{"inputTokens":69,"outputTokens":69,"cacheReadTokens":3200,"reasoningTokens":64}}},{"type":"chunk","time":1788500538725,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":3,"message":{"role":"assistant","content":[{"type":"reasoning","text":"Both subagents returned:\n1. First (fresh child): \"ALPHA\"\n2. Second (forked child): \"SAFFRON\" - correctly inherited the conversation context where I was asked to remember the codeword \"SAFFRON\".\n\nNow I reply with \"PARENT_DONE\" as instructed."},{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":69,"outputTokens":69,"cacheReadTokens":3200,"reasoningTokens":64},"stream":[{"type":"chunk","time":1788199065598,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788199065598,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["Both"," sub","agents"," returned",":\n","1","."," First"," (","fresh"," child","):"," \"","AL","P","HA","\"\n","2","."," Second"," (","fork","ed"," child","):"," \"","SA","FF","RON","\""," -"," correctly"," inherited"," the"," conversation"," context"," where"," I"," was"," asked"," to"," remember"," the"," cod","ew","ord"," \"","SA","FF","RON","\".\n\n","Now"," I"," reply"," with"," \"","PAR","ENT","_D","ONE","\""," as"," instructed","."]},{"type":"chunk","time":1788199065598,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788199065598,"index":1,"dt":[0,0,0],"texts":["PAR","ENT","_D","ONE"]},{"type":"chunk","time":1788199065598,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Both subagents returned:\n1. First (fresh child): \"ALPHA\"\n2. Second (forked child): \"SAFFRON\" - correctly inherited the conversation context where I was asked to remember the codeword \"SAFFRON\".\n\nNow I reply with \"PARENT_DONE\" as instructed."}}},{"type":"chunk","time":1788199065598,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788199065598,"chunk":{"type":"usage","usage":{"inputTokens":69,"outputTokens":69,"cacheReadTokens":3200,"reasoningTokens":64}}},{"type":"chunk","time":1788199065598,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":3}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}} diff --git a/snapshots/sdk/subagent-mixed/session.v3.jsonl b/snapshots/sdk/subagent-mixed/session.v3.jsonl index 66f18681a3..18238b0b97 100644 --- a/snapshots/sdk/subagent-mixed/session.v3.jsonl +++ b/snapshots/sdk/subagent-mixed/session.v3.jsonl @@ -12,7 +12,7 @@ {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Remember this fact for later:","messageSeqs":[8],"source":{"kind":"fallback"}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to remember a codeword and just reply with \"OK\"."},{"type":"text","text":"OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":2883,"outputTokens":19,"cacheReadTokens":0,"reasoningTokens":17},"stream":[{"type":"chunk","time":1788699464642,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788699464642,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," remember"," a"," cod","ew","ord"," and"," just"," reply"," with"," \"","OK","\"."]},{"type":"chunk","time":1788699464642,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788699464642,"index":1,"dt":[],"texts":["OK"]},{"type":"chunk","time":1788699464642,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to remember a codeword and just reply with \"OK\"."}}},{"type":"chunk","time":1788699464642,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"OK"}}},{"type":"chunk","time":1788699464642,"chunk":{"type":"usage","usage":{"inputTokens":2883,"outputTokens":19,"cacheReadTokens":0,"reasoningTokens":17}}},{"type":"chunk","time":1788699464642,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to remember a codeword and just reply with \"OK\"."},{"type":"text","text":"OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":2883,"outputTokens":19,"cacheReadTokens":0,"reasoningTokens":17},"stream":[{"type":"chunk","time":1788880390184,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788880390184,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," remember"," a"," cod","ew","ord"," and"," just"," reply"," with"," \"","OK","\"."]},{"type":"chunk","time":1788880390184,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788880390184,"index":1,"dt":[],"texts":["OK"]},{"type":"chunk","time":1788880390184,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to remember a codeword and just reply with \"OK\"."}}},{"type":"chunk","time":1788880390184,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"OK"}}},{"type":"chunk","time":1788880390184,"chunk":{"type":"usage","usage":{"inputTokens":2883,"outputTokens":19,"cacheReadTokens":0,"reasoningTokens":17}}},{"type":"chunk","time":1788880390184,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Do these two delegations, once at a time. First, use the subagent tool (fresh child) exactly once: 'Reply with exactly the word ALPHA and nothing else.' Then, after it returns, use the subagent_fork tool (forked child that inherits this conversation) exactly once: 'What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.' After both subagents return, reply with the single word PARENT_DONE and stop. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"{{message:5}}"}]}} @@ -20,16 +20,18 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":2,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Do these two delegations, once at a time. First, use the subagent tool (fresh child) exactly once: 'Reply with exactly the word ALPHA and nothing else.' Then, after it returns, use the subagent_fork tool (forked child that inherits this conversation) exactly once: 'What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.' After both subagents return, reply with the single word PARENT_DONE and stop. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"{{message:5}}"},"surfaceOp":"append"} -{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"Let me do these two delegations one at a time as requested.\n\nFirst, I'll use the subagent tool (fresh child) to reply with \"ALPHA\"."},{"type":"tool-call","id":"call_00_YvHr2bGomk5HhpgDTvE81896","name":"subagent","arguments":"{\"description\": \"Reply ALPHA only\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":185,"outputTokens":110,"cacheReadTokens":2816,"reasoningTokens":35},"stream":[{"type":"chunk","time":1788699464668,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788699464668,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["Let"," me"," do"," these"," two"," deleg","ations"," one"," at"," a"," time"," as"," requested",".\n\n","First",","," I","'ll"," use"," the"," sub","agent"," tool"," (","fresh"," child",")"," to"," reply"," with"," \"","AL","P","HA","\"."]},{"type":"chunk","time":1788699464668,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699464668,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_YvHr2bGomk5HhpgDTvE81896","name":"subagent","args":["","{","\"","description","\"",": ","\"","Reply"," AL","P","HA"," only","\"",", ","\"","prom","pt","\"",": ","\"","Reply"," with"," exactly"," the"," word"," AL","P","HA"," and"," nothing"," else",".","\"",", \"run_in_background\":false}"]},{"type":"chunk","time":1788699464668,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Let me do these two delegations one at a time as requested.\n\nFirst, I'll use the subagent tool (fresh child) to reply with \"ALPHA\"."}}},{"type":"chunk","time":1788699464668,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_YvHr2bGomk5HhpgDTvE81896","name":"subagent","arguments":"{\"description\": \"Reply ALPHA only\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788699464668,"chunk":{"type":"usage","usage":{"inputTokens":185,"outputTokens":110,"cacheReadTokens":2816,"reasoningTokens":35}}},{"type":"chunk","time":1788699464668,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"Let me do these two delegations one at a time as requested.\n\nFirst, I'll use the subagent tool (fresh child) to reply with \"ALPHA\"."},{"type":"tool-call","id":"call_00_YvHr2bGomk5HhpgDTvE81896","name":"subagent","arguments":"{\"description\": \"Reply ALPHA only\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":185,"outputTokens":110,"cacheReadTokens":2816,"reasoningTokens":35},"stream":[{"type":"chunk","time":1788880390203,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788880390203,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["Let"," me"," do"," these"," two"," deleg","ations"," one"," at"," a"," time"," as"," requested",".\n\n","First",","," I","'ll"," use"," the"," sub","agent"," tool"," (","fresh"," child",")"," to"," reply"," with"," \"","AL","P","HA","\"."]},{"type":"chunk","time":1788880390203,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880390203,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_YvHr2bGomk5HhpgDTvE81896","name":"subagent","args":["","{","\"","description","\"",": ","\"","Reply"," AL","P","HA"," only","\"",", ","\"","prom","pt","\"",": ","\"","Reply"," with"," exactly"," the"," word"," AL","P","HA"," and"," nothing"," else",".","\"",", \"run_in_background\":false}"]},{"type":"chunk","time":1788880390203,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Let me do these two delegations one at a time as requested.\n\nFirst, I'll use the subagent tool (fresh child) to reply with \"ALPHA\"."}}},{"type":"chunk","time":1788880390203,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_YvHr2bGomk5HhpgDTvE81896","name":"subagent","arguments":"{\"description\": \"Reply ALPHA only\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788880390203,"chunk":{"type":"usage","usage":{"inputTokens":185,"outputTokens":110,"cacheReadTokens":2816,"reasoningTokens":35}}},{"type":"chunk","time":1788880390203,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":2,"step":1,"callId":"call_00_YvHr2bGomk5HhpgDTvE81896","name":"subagent","arguments":"{\"description\": \"Reply ALPHA only\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788880390209,"mode":"one-shot","label":"Reply ALPHA only"}} {"type":"tool/result","data":{"turn":2,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_YvHr2bGomk5HhpgDTvE81896"},"content":[{"type":"tool-result","toolCallId":"call_00_YvHr2bGomk5HhpgDTvE81896","content":[{"type":"text","text":"ALPHA"}],"isError":false}],"role":"user","id":"{{message:7}}"}},"sourceEventSeqs":[22],"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"step/start","data":{"turn":2,"step":2}} -{"type":"assistant/message","data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The first subagent returned \"ALPHA\". Now I need to use the subagent_fork tool (forked child that inherits this conversation) to ask about the project codeword."},{"type":"tool-call","id":"call_00_JSr5rhREq23wSmwSkCP77184","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":54,"outputTokens":128,"cacheReadTokens":3072,"reasoningTokens":40},"stream":[{"type":"chunk","time":1788699464739,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788699464739,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," first"," sub","agent"," returned"," \"","AL","P","HA","\"."," Now"," I"," need"," to"," use"," the"," sub","agent","_f","ork"," tool"," (","fork","ed"," child"," that"," inher","its"," this"," conversation",")"," to"," ask"," about"," the"," project"," cod","ew","ord","."]},{"type":"chunk","time":1788699464739,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699464739,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_JSr5rhREq23wSmwSkCP77184","name":"subagent_fork","args":["","{","\"","description","\"",": ","\"","Recall"," project"," cod","ew","ord","\"",", ","\"","prom","pt","\"",": ","\"","What"," is"," the"," project"," cod","ew","ord"," mentioned"," earlier"," in"," this"," conversation","?"," Reply"," with"," exactly"," that"," one"," word"," and"," nothing"," else",".","\"","}"]},{"type":"chunk","time":1788699464739,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The first subagent returned \"ALPHA\". Now I need to use the subagent_fork tool (forked child that inherits this conversation) to ask about the project codeword."}}},{"type":"chunk","time":1788699464739,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_JSr5rhREq23wSmwSkCP77184","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}}},{"type":"chunk","time":1788699464739,"chunk":{"type":"usage","usage":{"inputTokens":54,"outputTokens":128,"cacheReadTokens":3072,"reasoningTokens":40}}},{"type":"chunk","time":1788699464740,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The first subagent returned \"ALPHA\". Now I need to use the subagent_fork tool (forked child that inherits this conversation) to ask about the project codeword."},{"type":"tool-call","id":"call_00_JSr5rhREq23wSmwSkCP77184","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":54,"outputTokens":128,"cacheReadTokens":3072,"reasoningTokens":40},"stream":[{"type":"chunk","time":1788880390271,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788880390271,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," first"," sub","agent"," returned"," \"","AL","P","HA","\"."," Now"," I"," need"," to"," use"," the"," sub","agent","_f","ork"," tool"," (","fork","ed"," child"," that"," inher","its"," this"," conversation",")"," to"," ask"," about"," the"," project"," cod","ew","ord","."]},{"type":"chunk","time":1788880390271,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880390271,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_JSr5rhREq23wSmwSkCP77184","name":"subagent_fork","args":["","{","\"","description","\"",": ","\"","Recall"," project"," cod","ew","ord","\"",", ","\"","prom","pt","\"",": ","\"","What"," is"," the"," project"," cod","ew","ord"," mentioned"," earlier"," in"," this"," conversation","?"," Reply"," with"," exactly"," that"," one"," word"," and"," nothing"," else",".","\"","}"]},{"type":"chunk","time":1788880390271,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The first subagent returned \"ALPHA\". Now I need to use the subagent_fork tool (forked child that inherits this conversation) to ask about the project codeword."}}},{"type":"chunk","time":1788880390271,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_JSr5rhREq23wSmwSkCP77184","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}}},{"type":"chunk","time":1788880390271,"chunk":{"type":"usage","usage":{"inputTokens":54,"outputTokens":128,"cacheReadTokens":3072,"reasoningTokens":40}}},{"type":"chunk","time":1788880390271,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":2,"step":2,"callId":"call_00_JSr5rhREq23wSmwSkCP77184","name":"subagent_fork","arguments":"{\"description\": \"Recall project codeword\", \"prompt\": \"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else.\"}"}} -{"type":"tool/result","data":{"turn":2,"step":2,"message":{"source":{"kind":"tool","callId":"call_00_JSr5rhREq23wSmwSkCP77184"},"content":[{"type":"tool-result","toolCallId":"call_00_JSr5rhREq23wSmwSkCP77184","content":[{"type":"text","text":"SAFFRON"}],"isError":false}],"role":"user","id":"{{message:9}}"}},"sourceEventSeqs":[27],"surfaceOp":"append"} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:3}}","childCreatedAt":1788880390281,"mode":"one-shot","label":"Recall project codeword"}} +{"type":"tool/result","data":{"turn":2,"step":2,"message":{"source":{"kind":"tool","callId":"call_00_JSr5rhREq23wSmwSkCP77184"},"content":[{"type":"tool-result","toolCallId":"call_00_JSr5rhREq23wSmwSkCP77184","content":[{"type":"text","text":"SAFFRON"}],"isError":false}],"role":"user","id":"{{message:9}}"}},"sourceEventSeqs":[28],"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":2}} {"type":"step/start","data":{"turn":2,"step":3}} -{"type":"assistant/message","data":{"turn":2,"step":3,"message":{"role":"assistant","content":[{"type":"reasoning","text":"Both subagents returned:\n1. First (fresh child): \"ALPHA\"\n2. Second (forked child): \"SAFFRON\" - correctly inherited the conversation context where I was asked to remember the codeword \"SAFFRON\".\n\nNow I reply with \"PARENT_DONE\" as instructed."},{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":69,"outputTokens":69,"cacheReadTokens":3200,"reasoningTokens":64},"stream":[{"type":"chunk","time":1788699464798,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788699464798,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["Both"," sub","agents"," returned",":\n","1","."," First"," (","fresh"," child","):"," \"","AL","P","HA","\"\n","2","."," Second"," (","fork","ed"," child","):"," \"","SA","FF","RON","\""," -"," correctly"," inherited"," the"," conversation"," context"," where"," I"," was"," asked"," to"," remember"," the"," cod","ew","ord"," \"","SA","FF","RON","\".\n\n","Now"," I"," reply"," with"," \"","PAR","ENT","_D","ONE","\""," as"," instructed","."]},{"type":"chunk","time":1788699464798,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788699464798,"index":1,"dt":[0,0,0],"texts":["PAR","ENT","_D","ONE"]},{"type":"chunk","time":1788699464798,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Both subagents returned:\n1. First (fresh child): \"ALPHA\"\n2. Second (forked child): \"SAFFRON\" - correctly inherited the conversation context where I was asked to remember the codeword \"SAFFRON\".\n\nNow I reply with \"PARENT_DONE\" as instructed."}}},{"type":"chunk","time":1788699464798,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788699464798,"chunk":{"type":"usage","usage":{"inputTokens":69,"outputTokens":69,"cacheReadTokens":3200,"reasoningTokens":64}}},{"type":"chunk","time":1788699464798,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":3,"message":{"role":"assistant","content":[{"type":"reasoning","text":"Both subagents returned:\n1. First (fresh child): \"ALPHA\"\n2. Second (forked child): \"SAFFRON\" - correctly inherited the conversation context where I was asked to remember the codeword \"SAFFRON\".\n\nNow I reply with \"PARENT_DONE\" as instructed."},{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":69,"outputTokens":69,"cacheReadTokens":3200,"reasoningTokens":64},"stream":[{"type":"chunk","time":1788880390328,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788880390328,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["Both"," sub","agents"," returned",":\n","1","."," First"," (","fresh"," child","):"," \"","AL","P","HA","\"\n","2","."," Second"," (","fork","ed"," child","):"," \"","SA","FF","RON","\""," -"," correctly"," inherited"," the"," conversation"," context"," where"," I"," was"," asked"," to"," remember"," the"," cod","ew","ord"," \"","SA","FF","RON","\".\n\n","Now"," I"," reply"," with"," \"","PAR","ENT","_D","ONE","\""," as"," instructed","."]},{"type":"chunk","time":1788880390328,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788880390328,"index":1,"dt":[0,0,0],"texts":["PAR","ENT","_D","ONE"]},{"type":"chunk","time":1788880390328,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Both subagents returned:\n1. First (fresh child): \"ALPHA\"\n2. Second (forked child): \"SAFFRON\" - correctly inherited the conversation context where I was asked to remember the codeword \"SAFFRON\".\n\nNow I reply with \"PARENT_DONE\" as instructed."}}},{"type":"chunk","time":1788880390328,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788880390328,"chunk":{"type":"usage","usage":{"inputTokens":69,"outputTokens":69,"cacheReadTokens":3200,"reasoningTokens":64}}},{"type":"chunk","time":1788880390328,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":3}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}} diff --git a/snapshots/sdk/subagent-send-message/session.v2.jsonl b/snapshots/sdk/subagent-send-message/session.v2.jsonl index 6de97f0e4c..d80daddee2 100644 --- a/snapshots/sdk/subagent-send-message/session.v2.jsonl +++ b/snapshots/sdk/subagent-send-message/session.v2.jsonl @@ -11,13 +11,12 @@ {"type":"session/title","data":{"title":"Follow these steps exactly, then","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Send a finding\", \"prompt\": \"Send exactly CHILD_MESSAGE_OK to your parent with send_message, then stop.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500539308,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500539308,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\": \"Send a finding\", \"prompt\": \"Send exactly CHILD_MESSAGE_OK to your parent with send_message, then stop.\", \"run_in_background\": true}"]},{"type":"chunk","time":1788500539308,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Send a finding\", \"prompt\": \"Send exactly CHILD_MESSAGE_OK to your parent with send_message, then stop.\", \"run_in_background\": true}"}}},{"type":"chunk","time":1788500539308,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500539308,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Send a finding\", \"prompt\": \"Send exactly CHILD_MESSAGE_OK to your parent with send_message, then stop.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788269697328,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788269697328,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\": \"Send a finding\", \"prompt\": \"Send exactly CHILD_MESSAGE_OK to your parent with send_message, then stop.\", \"run_in_background\": true}"]},{"type":"chunk","time":1788269697328,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Send a finding\", \"prompt\": \"Send exactly CHILD_MESSAGE_OK to your parent with send_message, then stop.\", \"run_in_background\": true}"}}},{"type":"chunk","time":1788269697328,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788269697328,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Send a finding\", \"prompt\": \"Send exactly CHILD_MESSAGE_OK to your parent with send_message, then stop.\", \"run_in_background\": true}"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500539315,"mode":"continuable","label":"Send a finding"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500539349,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500539349,"index":0,"dt":[],"texts":["STARTED"]},{"type":"chunk","time":1788500539349,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"STARTED"}}},{"type":"chunk","time":1788500539349,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500539349,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788269697354,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788269697354,"index":0,"dt":[],"texts":["STARTED"]},{"type":"chunk","time":1788269697354,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"STARTED"}}},{"type":"chunk","time":1788269697354,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788269697354,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} {"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Agent {{session:2}} sent a message: "},{"type":"text","text":"CHILD_MESSAGE_OK"}],"source":{"kind":"agent-message","form":"relay","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:6}}"}]}} @@ -28,7 +27,7 @@ {"type":"step/start","data":{"turn":2,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Agent {{session:2}} sent a message: "},{"type":"text","text":"CHILD_MESSAGE_OK"}],"source":{"kind":"agent-message","form":"relay","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:6}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"Message sent."}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:7}}"},"surfaceOp":"append"} -{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500539403,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500539403,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1788500539403,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1788500539403,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500539403,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788269697428,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788269697428,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1788269697428,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1788269697428,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788269697428,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Repeat back, verbatim, the exact message the background subagent sent you. Reply with only that text. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"{{message:9}}"}]}} @@ -36,6 +35,6 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":3,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Repeat back, verbatim, the exact message the background subagent sent you. Reply with only that text. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"{{message:9}}"},"surfaceOp":"append"} -{"type":"assistant/message","data":{"turn":3,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"CHILD_MESSAGE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500539428,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500539428,"index":0,"dt":[],"texts":["CHILD_MESSAGE_OK"]},{"type":"chunk","time":1788500539428,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CHILD_MESSAGE_OK"}}},{"type":"chunk","time":1788500539428,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500539428,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":3,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"CHILD_MESSAGE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788269697454,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788269697454,"index":0,"dt":[],"texts":["CHILD_MESSAGE_OK"]},{"type":"chunk","time":1788269697454,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CHILD_MESSAGE_OK"}}},{"type":"chunk","time":1788269697454,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788269697454,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":3,"step":1}} {"type":"turn/end","data":{"turn":3,"reason":{"kind":"completed"}}} diff --git a/snapshots/sdk/subagent-send-message/session.v3.jsonl b/snapshots/sdk/subagent-send-message/session.v3.jsonl index 3b3f4cf8c4..661e7901da 100644 --- a/snapshots/sdk/subagent-send-message/session.v3.jsonl +++ b/snapshots/sdk/subagent-send-message/session.v3.jsonl @@ -12,12 +12,13 @@ {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Follow these steps exactly, then","messageSeqs":[8],"source":{"kind":"fallback"}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Send a finding\", \"prompt\": \"Send exactly CHILD_MESSAGE_OK to your parent with send_message, then stop.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788699465470,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699465470,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\": \"Send a finding\", \"prompt\": \"Send exactly CHILD_MESSAGE_OK to your parent with send_message, then stop.\", \"run_in_background\": true}"]},{"type":"chunk","time":1788699465470,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Send a finding\", \"prompt\": \"Send exactly CHILD_MESSAGE_OK to your parent with send_message, then stop.\", \"run_in_background\": true}"}}},{"type":"chunk","time":1788699465470,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788699465470,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Send a finding\", \"prompt\": \"Send exactly CHILD_MESSAGE_OK to your parent with send_message, then stop.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788880390890,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880390890,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\": \"Send a finding\", \"prompt\": \"Send exactly CHILD_MESSAGE_OK to your parent with send_message, then stop.\", \"run_in_background\": true}"]},{"type":"chunk","time":1788880390890,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Send a finding\", \"prompt\": \"Send exactly CHILD_MESSAGE_OK to your parent with send_message, then stop.\", \"run_in_background\": true}"}}},{"type":"chunk","time":1788880390890,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788880390890,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Send a finding\", \"prompt\": \"Send exactly CHILD_MESSAGE_OK to your parent with send_message, then stop.\", \"run_in_background\": true}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788880390895,"mode":"continuable","label":"Send a finding"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788699465512,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788699465512,"index":0,"dt":[],"texts":["STARTED"]},{"type":"chunk","time":1788699465512,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"STARTED"}}},{"type":"chunk","time":1788699465512,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788699465512,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788880390923,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788880390923,"index":0,"dt":[],"texts":["STARTED"]},{"type":"chunk","time":1788880390923,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"STARTED"}}},{"type":"chunk","time":1788880390923,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788880390923,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} {"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Agent {{session:2}} sent a message: "},{"type":"text","text":"CHILD_MESSAGE_OK"}],"source":{"kind":"agent-message","form":"relay","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:7}}"}]}} @@ -28,7 +29,7 @@ {"type":"step/start","data":{"turn":2,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Agent {{session:2}} sent a message: "},{"type":"text","text":"CHILD_MESSAGE_OK"}],"source":{"kind":"agent-message","form":"relay","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:7}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"Message sent."}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:8}}"},"surfaceOp":"append"} -{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788699465577,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788699465577,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1788699465577,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1788699465577,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788699465577,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788880390979,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788880390979,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1788880390979,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1788880390979,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788880390979,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Repeat back, verbatim, the exact message the background subagent sent you. Reply with only that text. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"{{message:10}}"}]}} @@ -36,6 +37,6 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":3,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Repeat back, verbatim, the exact message the background subagent sent you. Reply with only that text. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"{{message:10}}"},"surfaceOp":"append"} -{"type":"assistant/message","data":{"turn":3,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"CHILD_MESSAGE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788699465601,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788699465601,"index":0,"dt":[],"texts":["CHILD_MESSAGE_OK"]},{"type":"chunk","time":1788699465601,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CHILD_MESSAGE_OK"}}},{"type":"chunk","time":1788699465601,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788699465601,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":3,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"CHILD_MESSAGE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788880390997,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788880390997,"index":0,"dt":[],"texts":["CHILD_MESSAGE_OK"]},{"type":"chunk","time":1788880390997,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CHILD_MESSAGE_OK"}}},{"type":"chunk","time":1788880390997,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788880390997,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":3,"step":1}} {"type":"turn/end","data":{"turn":3,"reason":{"kind":"completed"}}} diff --git a/snapshots/sdk/subagent-spawn-in-process/notifications.expected.jsonl b/snapshots/sdk/subagent-spawn-in-process/notifications.expected.jsonl index fdb07e6fb1..1a5029d705 100644 --- a/snapshots/sdk/subagent-spawn-in-process/notifications.expected.jsonl +++ b/snapshots/sdk/subagent-spawn-in-process/notifications.expected.jsonl @@ -16,6 +16,7 @@ {"method":"session.status","params":{"sessionId":"{{sessionId}}","status":"running"}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/start","seq":3,"time":0,"data":{"turn":1}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":4,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"subagent/catalog","seq":15,"time":0,"data":{"version":0,"childId":"{{sessionId}}","childCreatedAt":0,"mode":"one-shot","label":"echo probe"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"subagent/descriptor","seq":5,"time":0,"data":{"version":3,"mode":"one-shot","provider":"spawn","label":"echo probe"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}} @@ -29,10 +30,10 @@ {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":15,"time":0,"data":{"turn":1,"reason":{"kind":"error","error":{"message":"llm-replay: script exhausted — session requested model call #1 but its script has only 0; re-record the scenario","code":"UNKNOWN"}}}}}} {"method":"session.status","params":{"sessionId":"{{sessionId}}","status":"idle"}} {"method":"subagent.finished","params":{"provider":"spawn","agentId":"{{sessionId}}","parentSessionId":"{{sessionId}}","childSessionId":"{{sessionId}}","status":"error","stopReason":"error"}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":15,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_oHPNQ1nLoakoaAGXIxCM7404"},"content":[{"type":"tool-result","toolCallId":"call_00_oHPNQ1nLoakoaAGXIxCM7404","content":[{"type":"text","text":"Error: subagent run failed"}],"isError":true}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":16,"time":0,"data":{"turn":1,"step":1}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":17,"time":0,"data":{"turn":1,"step":2}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":18,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The subagent replied with \"child answer 42.\" Now I need to reply with the subagent's final answer verbatim."},{"type":"text","text":"child answer 42."}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":19,"outputTokens":32,"cacheReadTokens":1920,"reasoningTokens":26},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":0,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," sub","agent"," replied"," with"," \"","child"," answer"," ","42",".\""," Now"," I"," need"," to"," reply"," with"," the"," sub","agent","'s"," final"," answer"," verb","atim","."]},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":1,"dt":[0,0,0,0],"texts":["child"," answer"," ","42","."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The subagent replied with \"child answer 42.\" Now I need to reply with the subagent's final answer verbatim."}}},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"child answer 42."}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":19,"outputTokens":32,"cacheReadTokens":1920,"reasoningTokens":26}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":19,"time":0,"data":{"turn":1,"step":2}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":20,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":16,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_oHPNQ1nLoakoaAGXIxCM7404"},"content":[{"type":"tool-result","toolCallId":"call_00_oHPNQ1nLoakoaAGXIxCM7404","content":[{"type":"text","text":"Error: subagent run failed"}],"isError":true}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":17,"time":0,"data":{"turn":1,"step":1}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":18,"time":0,"data":{"turn":1,"step":2}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":19,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The subagent replied with \"child answer 42.\" Now I need to reply with the subagent's final answer verbatim."},{"type":"text","text":"child answer 42."}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":19,"outputTokens":32,"cacheReadTokens":1920,"reasoningTokens":26},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":0,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," sub","agent"," replied"," with"," \"","child"," answer"," ","42",".\""," Now"," I"," need"," to"," reply"," with"," the"," sub","agent","'s"," final"," answer"," verb","atim","."]},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":1,"dt":[0,0,0,0],"texts":["child"," answer"," ","42","."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The subagent replied with \"child answer 42.\" Now I need to reply with the subagent's final answer verbatim."}}},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"child answer 42."}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":19,"outputTokens":32,"cacheReadTokens":1920,"reasoningTokens":26}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":20,"time":0,"data":{"turn":1,"step":2}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":21,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}}} {"method":"session.status","params":{"sessionId":"{{sessionId}}","status":"idle"}} diff --git a/snapshots/sdk/subagent-spawn-in-process/session.v2.jsonl b/snapshots/sdk/subagent-spawn-in-process/session.v2.jsonl index 16cecfc4e6..8b6c5fc4f6 100644 --- a/snapshots/sdk/subagent-spawn-in-process/session.v2.jsonl +++ b/snapshots/sdk/subagent-spawn-in-process/session.v2.jsonl @@ -11,12 +11,11 @@ {"type":"session/title","data":{"title":"Use the subagent tool exactly","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to:\n1. Use the subagent tool exactly once with description 'echo probe' and prompt 'Reply with exactly: child answer 42.'\n2. Then reply with the subagent's final answer verbatim.\n\nLet me do this step by step."},{"type":"tool-call","id":"call_00_oHPNQ1nLoakoaAGXIxCM7404","name":"subagent","arguments":"{\"description\": \"echo probe\", \"prompt\": \"Reply with exactly: child answer 42.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":135,"outputTokens":124,"cacheReadTokens":1664,"reasoningTokens":55},"stream":[{"type":"chunk","time":1788500539940,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788500539940,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to",":\n","1","."," Use"," the"," sub","agent"," tool"," exactly"," once"," with"," description"," '","echo"," probe","'"," and"," prompt"," '","Reply"," with"," exactly",":"," child"," answer"," ","42",".'\n","2","."," Then"," reply"," with"," the"," sub","agent","'s"," final"," answer"," verb","atim",".\n\n","Let"," me"," do"," this"," step"," by"," step","."]},{"type":"chunk","time":1788500539940,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500539940,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_oHPNQ1nLoakoaAGXIxCM7404","name":"subagent","args":["","{","\"","description","\"",": ","\"","echo"," probe","\"",", ","\"","prom","pt","\"",": ","\"","Reply"," with"," exactly",":"," child"," answer"," ","42",".","\"","}"]},{"type":"chunk","time":1788500539940,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to:\n1. Use the subagent tool exactly once with description 'echo probe' and prompt 'Reply with exactly: child answer 42.'\n2. Then reply with the subagent's final answer verbatim.\n\nLet me do this step by step."}}},{"type":"chunk","time":1788500539940,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_oHPNQ1nLoakoaAGXIxCM7404","name":"subagent","arguments":"{\"description\": \"echo probe\", \"prompt\": \"Reply with exactly: child answer 42.\"}"}}},{"type":"chunk","time":1788500539940,"chunk":{"type":"usage","usage":{"inputTokens":135,"outputTokens":124,"cacheReadTokens":1664,"reasoningTokens":55}}},{"type":"chunk","time":1788500539940,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to:\n1. Use the subagent tool exactly once with description 'echo probe' and prompt 'Reply with exactly: child answer 42.'\n2. Then reply with the subagent's final answer verbatim.\n\nLet me do this step by step."},{"type":"tool-call","id":"call_00_oHPNQ1nLoakoaAGXIxCM7404","name":"subagent","arguments":"{\"description\": \"echo probe\", \"prompt\": \"Reply with exactly: child answer 42.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":135,"outputTokens":124,"cacheReadTokens":1664,"reasoningTokens":55},"stream":[{"type":"chunk","time":1788199067994,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788199067994,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to",":\n","1","."," Use"," the"," sub","agent"," tool"," exactly"," once"," with"," description"," '","echo"," probe","'"," and"," prompt"," '","Reply"," with"," exactly",":"," child"," answer"," ","42",".'\n","2","."," Then"," reply"," with"," the"," sub","agent","'s"," final"," answer"," verb","atim",".\n\n","Let"," me"," do"," this"," step"," by"," step","."]},{"type":"chunk","time":1788199067994,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788199067994,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_oHPNQ1nLoakoaAGXIxCM7404","name":"subagent","args":["","{","\"","description","\"",": ","\"","echo"," probe","\"",", ","\"","prom","pt","\"",": ","\"","Reply"," with"," exactly",":"," child"," answer"," ","42",".","\"","}"]},{"type":"chunk","time":1788199067994,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to:\n1. Use the subagent tool exactly once with description 'echo probe' and prompt 'Reply with exactly: child answer 42.'\n2. Then reply with the subagent's final answer verbatim.\n\nLet me do this step by step."}}},{"type":"chunk","time":1788199067994,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_oHPNQ1nLoakoaAGXIxCM7404","name":"subagent","arguments":"{\"description\": \"echo probe\", \"prompt\": \"Reply with exactly: child answer 42.\"}"}}},{"type":"chunk","time":1788199067994,"chunk":{"type":"usage","usage":{"inputTokens":135,"outputTokens":124,"cacheReadTokens":1664,"reasoningTokens":55}}},{"type":"chunk","time":1788199067994,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_00_oHPNQ1nLoakoaAGXIxCM7404","name":"subagent","arguments":"{\"description\": \"echo probe\", \"prompt\": \"Reply with exactly: child answer 42.\"}"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500539946,"mode":"one-shot","label":"echo probe"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_oHPNQ1nLoakoaAGXIxCM7404"},"content":[{"type":"tool-result","toolCallId":"call_00_oHPNQ1nLoakoaAGXIxCM7404","content":[{"type":"text","text":"Error: subagent run failed"}],"isError":true}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The subagent replied with \"child answer 42.\" Now I need to reply with the subagent's final answer verbatim."},{"type":"text","text":"child answer 42."}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":19,"outputTokens":32,"cacheReadTokens":1920,"reasoningTokens":26},"stream":[{"type":"chunk","time":1788500540000,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788500540000,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," sub","agent"," replied"," with"," \"","child"," answer"," ","42",".\""," Now"," I"," need"," to"," reply"," with"," the"," sub","agent","'s"," final"," answer"," verb","atim","."]},{"type":"chunk","time":1788500540000,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788500540000,"index":1,"dt":[0,0,0,0],"texts":["child"," answer"," ","42","."]},{"type":"chunk","time":1788500540000,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The subagent replied with \"child answer 42.\" Now I need to reply with the subagent's final answer verbatim."}}},{"type":"chunk","time":1788500540000,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"child answer 42."}}},{"type":"chunk","time":1788500540000,"chunk":{"type":"usage","usage":{"inputTokens":19,"outputTokens":32,"cacheReadTokens":1920,"reasoningTokens":26}}},{"type":"chunk","time":1788500540000,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The subagent replied with \"child answer 42.\" Now I need to reply with the subagent's final answer verbatim."},{"type":"text","text":"child answer 42."}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":19,"outputTokens":32,"cacheReadTokens":1920,"reasoningTokens":26},"stream":[{"type":"chunk","time":1788199068048,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788199068048,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],"texts":["The"," sub","agent"," replied"," with"," \"","child"," answer"," ","42",".\""," Now"," I"," need"," to"," reply"," with"," the"," sub","agent","'s"," final"," answer"," verb","atim","."]},{"type":"chunk","time":1788199068049,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788199068049,"index":1,"dt":[0,0,0,0],"texts":["child"," answer"," ","42","."]},{"type":"chunk","time":1788199068049,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The subagent replied with \"child answer 42.\" Now I need to reply with the subagent's final answer verbatim."}}},{"type":"chunk","time":1788199068049,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"child answer 42."}}},{"type":"chunk","time":1788199068049,"chunk":{"type":"usage","usage":{"inputTokens":19,"outputTokens":32,"cacheReadTokens":1920,"reasoningTokens":26}}},{"type":"chunk","time":1788199068049,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/sdk/subagent-spawn-in-process/session.v3.jsonl b/snapshots/sdk/subagent-spawn-in-process/session.v3.jsonl index 038317319f..fa5b4d2b96 100644 --- a/snapshots/sdk/subagent-spawn-in-process/session.v3.jsonl +++ b/snapshots/sdk/subagent-spawn-in-process/session.v3.jsonl @@ -12,11 +12,12 @@ {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Use the subagent tool exactly","messageSeqs":[8],"source":{"kind":"fallback"}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to:\n1. Use the subagent tool exactly once with description 'echo probe' and prompt 'Reply with exactly: child answer 42.'\n2. Then reply with the subagent's final answer verbatim.\n\nLet me do this step by step."},{"type":"tool-call","id":"call_00_oHPNQ1nLoakoaAGXIxCM7404","name":"subagent","arguments":"{\"description\": \"echo probe\", \"prompt\": \"Reply with exactly: child answer 42.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":135,"outputTokens":124,"cacheReadTokens":1664,"reasoningTokens":55},"stream":[{"type":"chunk","time":1788699466205,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788699466205,"index":0,"dt":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to",":\n","1","."," Use"," the"," sub","agent"," tool"," exactly"," once"," with"," description"," '","echo"," probe","'"," and"," prompt"," '","Reply"," with"," exactly",":"," child"," answer"," ","42",".'\n","2","."," Then"," reply"," with"," the"," sub","agent","'s"," final"," answer"," verb","atim",".\n\n","Let"," me"," do"," this"," step"," by"," step","."]},{"type":"chunk","time":1788699466206,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699466206,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_oHPNQ1nLoakoaAGXIxCM7404","name":"subagent","args":["","{","\"","description","\"",": ","\"","echo"," probe","\"",", ","\"","prom","pt","\"",": ","\"","Reply"," with"," exactly",":"," child"," answer"," ","42",".","\"","}"]},{"type":"chunk","time":1788699466206,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to:\n1. Use the subagent tool exactly once with description 'echo probe' and prompt 'Reply with exactly: child answer 42.'\n2. Then reply with the subagent's final answer verbatim.\n\nLet me do this step by step."}}},{"type":"chunk","time":1788699466206,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_oHPNQ1nLoakoaAGXIxCM7404","name":"subagent","arguments":"{\"description\": \"echo probe\", \"prompt\": \"Reply with exactly: child answer 42.\"}"}}},{"type":"chunk","time":1788699466206,"chunk":{"type":"usage","usage":{"inputTokens":135,"outputTokens":124,"cacheReadTokens":1664,"reasoningTokens":55}}},{"type":"chunk","time":1788699466206,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to:\n1. Use the subagent tool exactly once with description 'echo probe' and prompt 'Reply with exactly: child answer 42.'\n2. Then reply with the subagent's final answer verbatim.\n\nLet me do this step by step."},{"type":"tool-call","id":"call_00_oHPNQ1nLoakoaAGXIxCM7404","name":"subagent","arguments":"{\"description\": \"echo probe\", \"prompt\": \"Reply with exactly: child answer 42.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":135,"outputTokens":124,"cacheReadTokens":1664,"reasoningTokens":55},"stream":[{"type":"chunk","time":1788880391547,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788880391547,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to",":\n","1","."," Use"," the"," sub","agent"," tool"," exactly"," once"," with"," description"," '","echo"," probe","'"," and"," prompt"," '","Reply"," with"," exactly",":"," child"," answer"," ","42",".'\n","2","."," Then"," reply"," with"," the"," sub","agent","'s"," final"," answer"," verb","atim",".\n\n","Let"," me"," do"," this"," step"," by"," step","."]},{"type":"chunk","time":1788880391548,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880391548,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_oHPNQ1nLoakoaAGXIxCM7404","name":"subagent","args":["","{","\"","description","\"",": ","\"","echo"," probe","\"",", ","\"","prom","pt","\"",": ","\"","Reply"," with"," exactly",":"," child"," answer"," ","42",".","\"","}"]},{"type":"chunk","time":1788880391548,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to:\n1. Use the subagent tool exactly once with description 'echo probe' and prompt 'Reply with exactly: child answer 42.'\n2. Then reply with the subagent's final answer verbatim.\n\nLet me do this step by step."}}},{"type":"chunk","time":1788880391548,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_oHPNQ1nLoakoaAGXIxCM7404","name":"subagent","arguments":"{\"description\": \"echo probe\", \"prompt\": \"Reply with exactly: child answer 42.\"}"}}},{"type":"chunk","time":1788880391548,"chunk":{"type":"usage","usage":{"inputTokens":135,"outputTokens":124,"cacheReadTokens":1664,"reasoningTokens":55}}},{"type":"chunk","time":1788880391548,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_00_oHPNQ1nLoakoaAGXIxCM7404","name":"subagent","arguments":"{\"description\": \"echo probe\", \"prompt\": \"Reply with exactly: child answer 42.\"}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788880391557,"mode":"one-shot","label":"echo probe"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_oHPNQ1nLoakoaAGXIxCM7404"},"content":[{"type":"tool-result","toolCallId":"call_00_oHPNQ1nLoakoaAGXIxCM7404","content":[{"type":"text","text":"Error: subagent run failed"}],"isError":true}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The subagent replied with \"child answer 42.\" Now I need to reply with the subagent's final answer verbatim."},{"type":"text","text":"child answer 42."}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":19,"outputTokens":32,"cacheReadTokens":1920,"reasoningTokens":26},"stream":[{"type":"chunk","time":1788699466269,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788699466269,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," sub","agent"," replied"," with"," \"","child"," answer"," ","42",".\""," Now"," I"," need"," to"," reply"," with"," the"," sub","agent","'s"," final"," answer"," verb","atim","."]},{"type":"chunk","time":1788699466269,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788699466269,"index":1,"dt":[0,0,0,0],"texts":["child"," answer"," ","42","."]},{"type":"chunk","time":1788699466269,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The subagent replied with \"child answer 42.\" Now I need to reply with the subagent's final answer verbatim."}}},{"type":"chunk","time":1788699466269,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"child answer 42."}}},{"type":"chunk","time":1788699466269,"chunk":{"type":"usage","usage":{"inputTokens":19,"outputTokens":32,"cacheReadTokens":1920,"reasoningTokens":26}}},{"type":"chunk","time":1788699466269,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The subagent replied with \"child answer 42.\" Now I need to reply with the subagent's final answer verbatim."},{"type":"text","text":"child answer 42."}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":19,"outputTokens":32,"cacheReadTokens":1920,"reasoningTokens":26},"stream":[{"type":"chunk","time":1788880391635,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788880391635,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," sub","agent"," replied"," with"," \"","child"," answer"," ","42",".\""," Now"," I"," need"," to"," reply"," with"," the"," sub","agent","'s"," final"," answer"," verb","atim","."]},{"type":"chunk","time":1788880391635,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788880391635,"index":1,"dt":[0,0,0,0],"texts":["child"," answer"," ","42","."]},{"type":"chunk","time":1788880391635,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The subagent replied with \"child answer 42.\" Now I need to reply with the subagent's final answer verbatim."}}},{"type":"chunk","time":1788880391635,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"child answer 42."}}},{"type":"chunk","time":1788880391635,"chunk":{"type":"usage","usage":{"inputTokens":19,"outputTokens":32,"cacheReadTokens":1920,"reasoningTokens":26}}},{"type":"chunk","time":1788880391635,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/advanced-toolchain-runtime/session.v2.jsonl b/snapshots/session/advanced-toolchain-runtime/session.v2.jsonl index 31a014ede7..b3772f0156 100644 --- a/snapshots/session/advanced-toolchain-runtime/session.v2.jsonl +++ b/snapshots/session/advanced-toolchain-runtime/session.v2.jsonl @@ -11,32 +11,30 @@ {"type":"session/title","data":{"title":"Run this advanced flow exactly","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788500543707,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500543708,"index":0,"dt":[],"id":"advanced-define","name":"cordis_define","args":["{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"]},{"type":"chunk","time":1788500543708,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}}},{"type":"chunk","time":1788500543708,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788500543708,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233482630,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233482630,"index":0,"dt":[],"id":"advanced-define","name":"cordis_define","args":["{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"]},{"type":"chunk","time":1788233482630,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}}},{"type":"chunk","time":1788233482630,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233482630,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-define"},"content":[{"type":"tool-result","toolCallId":"advanced-define","content":[{"type":"text","text":"Defined snap-1/pkg-1 (Snapshot Marker); it is not running yet. Use cordis_run to activate this Package."}],"isError":false}],"role":"user","id":"{{message:4}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788500543727,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500543727,"index":0,"dt":[],"id":"advanced-direct-child","name":"subagent","args":["{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788500543727,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788500543727,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788500543727,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233482656,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233482656,"index":0,"dt":[],"id":"advanced-direct-child","name":"subagent","args":["{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788233482656,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788233482656,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233482656,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500543732,"mode":"one-shot","label":"Check direct child"}} {"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{message:6}}"}},"sourceEventSeqs":[18],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} -{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788500543788,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500543788,"index":0,"dt":[],"id":"advanced-workflow","name":"workflow","args":["{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"]},{"type":"chunk","time":1788500543788,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"}}},{"type":"chunk","time":1788500543788,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788500543788,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233482737,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233482737,"index":0,"dt":[],"id":"advanced-workflow","name":"workflow","args":["{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"]},{"type":"chunk","time":1788233482737,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"}}},{"type":"chunk","time":1788233482737,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233482737,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":3,"callId":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"}} {"type":"tool-workflow/run-start","data":{"runId":"{{workflow:1}}","name":"advanced-acp-snapshot"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:3}}","childCreatedAt":1788500543873,"mode":"one-shot"}} {"type":"tool-workflow/agent-start","data":{"runId":"{{workflow:1}}","seq":1,"label":"workflow-child","phase":"Delegate","childId":"{{session:3}}"}} {"type":"tool-workflow/agent-end","data":{"runId":"{{workflow:1}}","seq":1,"outcome":"completed"}} {"type":"tool-workflow/run-end","data":{"runId":"{{workflow:1}}","stopReason":"completed"}} -{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-acp-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{message:8}}"}},"sourceEventSeqs":[24],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-acp-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{message:8}}"}},"sourceEventSeqs":[23],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} {"type":"step/start","data":{"turn":1,"step":4}} -{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788500543931,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500543931,"index":0,"dt":[],"id":"advanced-undefine","name":"cordis_undefine","args":["{\"pluginId\":\"snap-1\"}"]},{"type":"chunk","time":1788500543931,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}}},{"type":"chunk","time":1788500543931,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788500543931,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233482842,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233482842,"index":0,"dt":[],"id":"advanced-undefine","name":"cordis_undefine","args":["{\"pluginId\":\"snap-1\"}"]},{"type":"chunk","time":1788233482842,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}}},{"type":"chunk","time":1788233482842,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233482842,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":4,"callId":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}} -{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{message:10}}"}},"sourceEventSeqs":[34],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{message:10}}"}},"sourceEventSeqs":[32],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":4}} {"type":"step/start","data":{"turn":1,"step":5}} -{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_ACP_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788500543947,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500543947,"index":0,"dt":[],"texts":["ADVANCED_ACP_OK"]},{"type":"chunk","time":1788500543947,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_ACP_OK"}}},{"type":"chunk","time":1788500543947,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788500543947,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_ACP_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233482871,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788233482871,"index":0,"dt":[],"texts":["ADVANCED_ACP_OK"]},{"type":"chunk","time":1788233482871,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_ACP_OK"}}},{"type":"chunk","time":1788233482871,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233482871,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":5}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/advanced-toolchain-runtime/session.v3.jsonl b/snapshots/session/advanced-toolchain-runtime/session.v3.jsonl index e59be17f60..aca06668fd 100644 --- a/snapshots/session/advanced-toolchain-runtime/session.v3.jsonl +++ b/snapshots/session/advanced-toolchain-runtime/session.v3.jsonl @@ -12,30 +12,32 @@ {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Run this advanced flow exactly","messageSeqs":[8],"source":{"kind":"fallback"}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788699344497,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699344497,"index":0,"dt":[],"id":"advanced-define","name":"cordis_define","args":["{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"]},{"type":"chunk","time":1788699344497,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}}},{"type":"chunk","time":1788699344497,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788699344497,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788880370600,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880370600,"index":0,"dt":[],"id":"advanced-define","name":"cordis_define","args":["{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"]},{"type":"chunk","time":1788880370600,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}}},{"type":"chunk","time":1788880370600,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788880370600,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-define"},"content":[{"type":"tool-result","toolCallId":"advanced-define","content":[{"type":"text","text":"Defined snap-1/pkg-1 (Snapshot Marker); it is not running yet. Use cordis_run to activate this Package."}],"isError":false}],"role":"user","id":"{{message:5}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788699344517,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699344517,"index":0,"dt":[],"id":"advanced-direct-child","name":"subagent","args":["{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788699344517,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788699344518,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788699344518,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788880370619,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880370619,"index":0,"dt":[],"id":"advanced-direct-child","name":"subagent","args":["{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788880370619,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788880370619,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788880370619,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788880370624,"mode":"one-shot","label":"Check direct child"}} {"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{message:7}}"}},"sourceEventSeqs":[19],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} -{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788699344588,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699344588,"index":0,"dt":[],"id":"advanced-workflow","name":"workflow","args":["{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"]},{"type":"chunk","time":1788699344588,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"}}},{"type":"chunk","time":1788699344588,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788699344588,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788880370690,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880370690,"index":0,"dt":[],"id":"advanced-workflow","name":"workflow","args":["{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"]},{"type":"chunk","time":1788880370690,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"}}},{"type":"chunk","time":1788880370690,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788880370690,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":3,"callId":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"}} {"type":"tool-workflow/run-start","data":{"runId":"{{workflow:1}}","name":"advanced-acp-snapshot"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:3}}","childCreatedAt":1788880370792,"mode":"one-shot"}} {"type":"tool-workflow/agent-start","data":{"runId":"{{workflow:1}}","seq":1,"label":"workflow-child","phase":"Delegate","childId":"{{session:3}}"}} {"type":"tool-workflow/agent-end","data":{"runId":"{{workflow:1}}","seq":1,"outcome":"completed"}} {"type":"tool-workflow/run-end","data":{"runId":"{{workflow:1}}","stopReason":"completed"}} -{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-acp-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{message:9}}"}},"sourceEventSeqs":[24],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-acp-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{message:9}}"}},"sourceEventSeqs":[25],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} {"type":"step/start","data":{"turn":1,"step":4}} -{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788699344769,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699344770,"index":0,"dt":[],"id":"advanced-undefine","name":"cordis_undefine","args":["{\"pluginId\":\"snap-1\"}"]},{"type":"chunk","time":1788699344770,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}}},{"type":"chunk","time":1788699344770,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788699344770,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788880370853,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880370853,"index":0,"dt":[],"id":"advanced-undefine","name":"cordis_undefine","args":["{\"pluginId\":\"snap-1\"}"]},{"type":"chunk","time":1788880370853,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}}},{"type":"chunk","time":1788880370853,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788880370853,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":4,"callId":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}} -{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{message:11}}"}},"sourceEventSeqs":[33],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{message:11}}"}},"sourceEventSeqs":[35],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":4}} {"type":"step/start","data":{"turn":1,"step":5}} -{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_ACP_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:12}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788699344788,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788699344788,"index":0,"dt":[],"texts":["ADVANCED_ACP_OK"]},{"type":"chunk","time":1788699344788,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_ACP_OK"}}},{"type":"chunk","time":1788699344788,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788699344788,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_ACP_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:12}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788880370871,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788880370871,"index":0,"dt":[],"texts":["ADVANCED_ACP_OK"]},{"type":"chunk","time":1788880370871,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_ACP_OK"}}},{"type":"chunk","time":1788880370871,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788880370871,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":5}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/advanced-toolchain/session.v2.jsonl b/snapshots/session/advanced-toolchain/session.v2.jsonl index 8deeb37cc9..35c5b2fbfb 100644 --- a/snapshots/session/advanced-toolchain/session.v2.jsonl +++ b/snapshots/session/advanced-toolchain/session.v2.jsonl @@ -11,12 +11,12 @@ {"type":"session/title","data":{"title":"Run this advanced flow exactly","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788500542227,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500542227,"index":0,"dt":[],"id":"advanced-define","name":"cordis_define","args":["{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"]},{"type":"chunk","time":1788500542227,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}}},{"type":"chunk","time":1788500542227,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788500542227,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233481631,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233481631,"index":0,"dt":[],"id":"advanced-define","name":"cordis_define","args":["{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"]},{"type":"chunk","time":1788233481631,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}}},{"type":"chunk","time":1788233481631,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233481631,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-define"},"content":[{"type":"tool-result","toolCallId":"advanced-define","content":[{"type":"text","text":"Defined snap-1/pkg-1 (Snapshot Marker); it is not running yet. Use cordis_run to activate this Package."}],"isError":false}],"role":"user","id":"{{message:4}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788500542247,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500542247,"index":0,"dt":[],"id":"advanced-code","name":"run_code","args":["{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"]},{"type":"chunk","time":1788500542247,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"}}},{"type":"chunk","time":1788500542247,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788500542247,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233481651,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233481651,"index":0,"dt":[],"id":"advanced-code","name":"run_code","args":["{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"]},{"type":"chunk","time":1788233481651,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"}}},{"type":"chunk","time":1788233481651,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233481651,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"advanced-code","name":"run_code","arguments":"{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"}} {"type":"tool/code-dispatch-start","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"cordis_run","arguments":{"pluginId":"snap-1","packageId":"pkg-1","mode":"run"}}} {"type":"tool/code-dispatch","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"cordis_run","arguments":{"pluginId":"snap-1","packageId":"pkg-1","mode":"run"},"isError":false,"content":[{"type":"text","text":"snap-1/pkg-1 is running (run-1)."}]}} @@ -25,27 +25,25 @@ {"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-code"},"content":[{"type":"tool-result","toolCallId":"advanced-code","content":[{"type":"text","text":"{\n \"run\": {\n \"status\": \"running\",\n \"pluginId\": \"snap-1\",\n \"packageId\": \"pkg-1\",\n \"pluginRunId\": \"run-1\",\n \"currentPackageId\": \"pkg-1\",\n \"host\": {\n \"status\": \"running\",\n \"provides\": [],\n \"waitingFor\": []\n },\n \"client\": {\n \"status\": \"absent\",\n \"waitingFor\": []\n }\n },\n \"inspected\": {\n \"mode\": \"plugin\",\n \"pluginId\": \"snap-1\",\n \"name\": \"Snapshot Marker\",\n \"packageCount\": 1,\n \"state\": \"running\",\n \"currentPackageId\": \"pkg-1\",\n \"activeRun\": {\n \"pluginRunId\": \"run-1\",\n \"packageId\": \"pkg-1\"\n },\n \"packages\": [\n {\n \"packageId\": \"pkg-1\",\n \"name\": \"Snapshot Marker\",\n \"purpose\": \"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\n \"hasHostHalf\": true,\n \"hasClientHalf\": false,\n \"isCurrent\": true,\n \"isNext\": false\n }\n ]\n }\n}"}],"isError":false}],"role":"user","id":"{{message:6}}"}},"sourceEventSeqs":[18],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} -{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788500542326,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500542326,"index":0,"dt":[],"id":"advanced-direct-child","name":"subagent","args":["{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788500542326,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788500542326,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788500542326,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233481726,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233481726,"index":0,"dt":[],"id":"advanced-direct-child","name":"subagent","args":["{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788233481726,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788233481726,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233481726,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":3,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500542331,"mode":"one-shot","label":"Check direct child"}} {"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{message:8}}"}},"sourceEventSeqs":[27],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} {"type":"step/start","data":{"turn":1,"step":4}} -{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788500542391,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500542391,"index":0,"dt":[],"id":"advanced-workflow","name":"workflow","args":["{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"]},{"type":"chunk","time":1788500542391,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"}}},{"type":"chunk","time":1788500542391,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788500542391,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233481803,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233481803,"index":0,"dt":[],"id":"advanced-workflow","name":"workflow","args":["{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"]},{"type":"chunk","time":1788233481803,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"}}},{"type":"chunk","time":1788233481803,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233481803,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":4,"callId":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"}} {"type":"tool-workflow/run-start","data":{"runId":"{{workflow:1}}","name":"advanced-headless-snapshot"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:3}}","childCreatedAt":1788500542517,"mode":"one-shot"}} {"type":"tool-workflow/agent-start","data":{"runId":"{{workflow:1}}","seq":1,"label":"workflow-child","phase":"Delegate","childId":"{{session:3}}"}} {"type":"tool-workflow/agent-end","data":{"runId":"{{workflow:1}}","seq":1,"outcome":"completed"}} {"type":"tool-workflow/run-end","data":{"runId":"{{workflow:1}}","stopReason":"completed"}} -{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-headless-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{message:10}}"}},"sourceEventSeqs":[33],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-headless-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{message:10}}"}},"sourceEventSeqs":[32],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":4}} {"type":"step/start","data":{"turn":1,"step":5}} -{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788500542582,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500542582,"index":0,"dt":[],"id":"advanced-undefine","name":"cordis_undefine","args":["{\"pluginId\":\"snap-1\"}"]},{"type":"chunk","time":1788500542582,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}}},{"type":"chunk","time":1788500542582,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788500542582,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233481908,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233481908,"index":0,"dt":[],"id":"advanced-undefine","name":"cordis_undefine","args":["{\"pluginId\":\"snap-1\"}"]},{"type":"chunk","time":1788233481908,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}}},{"type":"chunk","time":1788233481908,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233481908,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":5,"callId":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}} -{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{message:12}}"}},"sourceEventSeqs":[43],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{message:12}}"}},"sourceEventSeqs":[41],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":5}} {"type":"step/start","data":{"turn":1,"step":6}} -{"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_HEADLESS_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:13}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788500542598,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500542598,"index":0,"dt":[],"texts":["ADVANCED_HEADLESS_OK"]},{"type":"chunk","time":1788500542598,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_HEADLESS_OK"}}},{"type":"chunk","time":1788500542598,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788500542598,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_HEADLESS_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:13}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233481936,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788233481936,"index":0,"dt":[],"texts":["ADVANCED_HEADLESS_OK"]},{"type":"chunk","time":1788233481936,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_HEADLESS_OK"}}},{"type":"chunk","time":1788233481936,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233481936,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":6}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/advanced-toolchain/session.v3.jsonl b/snapshots/session/advanced-toolchain/session.v3.jsonl index faca05c8fd..e8cbdbb286 100644 --- a/snapshots/session/advanced-toolchain/session.v3.jsonl +++ b/snapshots/session/advanced-toolchain/session.v3.jsonl @@ -12,12 +12,12 @@ {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Run this advanced flow exactly","messageSeqs":[8],"source":{"kind":"fallback"}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788699342767,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699342767,"index":0,"dt":[],"id":"advanced-define","name":"cordis_define","args":["{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"]},{"type":"chunk","time":1788699342767,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}}},{"type":"chunk","time":1788699342767,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788699342767,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788880369305,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880369305,"index":0,"dt":[],"id":"advanced-define","name":"cordis_define","args":["{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"]},{"type":"chunk","time":1788880369305,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}}},{"type":"chunk","time":1788880369305,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788880369305,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-define"},"content":[{"type":"tool-result","toolCallId":"advanced-define","content":[{"type":"text","text":"Defined snap-1/pkg-1 (Snapshot Marker); it is not running yet. Use cordis_run to activate this Package."}],"isError":false}],"role":"user","id":"{{message:5}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788699342788,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699342788,"index":0,"dt":[],"id":"advanced-code","name":"run_code","args":["{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"]},{"type":"chunk","time":1788699342788,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"}}},{"type":"chunk","time":1788699342788,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788699342788,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788880369326,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880369326,"index":0,"dt":[],"id":"advanced-code","name":"run_code","args":["{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"]},{"type":"chunk","time":1788880369326,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"}}},{"type":"chunk","time":1788880369326,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788880369326,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"advanced-code","name":"run_code","arguments":"{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"}} {"type":"tool/ptc-dispatch-start","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:ptc:1","name":"cordis_run","arguments":{"pluginId":"snap-1","packageId":"pkg-1","mode":"run"}}} {"type":"tool/ptc-dispatch","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:ptc:1","name":"cordis_run","arguments":{"pluginId":"snap-1","packageId":"pkg-1","mode":"run"},"isError":false,"content":[{"type":"text","text":"snap-1/pkg-1 is running (run-1)."}]}} @@ -26,25 +26,27 @@ {"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-code"},"content":[{"type":"tool-result","toolCallId":"advanced-code","content":[{"type":"text","text":"{\n \"run\": {\n \"status\": \"running\",\n \"pluginId\": \"snap-1\",\n \"packageId\": \"pkg-1\",\n \"pluginRunId\": \"run-1\",\n \"currentPackageId\": \"pkg-1\",\n \"host\": {\n \"status\": \"running\",\n \"provides\": [],\n \"waitingFor\": []\n },\n \"client\": {\n \"status\": \"absent\",\n \"waitingFor\": []\n }\n },\n \"inspected\": {\n \"mode\": \"plugin\",\n \"pluginId\": \"snap-1\",\n \"name\": \"Snapshot Marker\",\n \"packageCount\": 1,\n \"state\": \"running\",\n \"currentPackageId\": \"pkg-1\",\n \"activeRun\": {\n \"pluginRunId\": \"run-1\",\n \"packageId\": \"pkg-1\"\n },\n \"packages\": [\n {\n \"packageId\": \"pkg-1\",\n \"name\": \"Snapshot Marker\",\n \"purpose\": \"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\n \"hasHostHalf\": true,\n \"hasClientHalf\": false,\n \"isCurrent\": true,\n \"isNext\": false\n }\n ]\n }\n}"}],"isError":false}],"role":"user","id":"{{message:7}}"}},"sourceEventSeqs":[19],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} -{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788699342884,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699342884,"index":0,"dt":[],"id":"advanced-direct-child","name":"subagent","args":["{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788699342884,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788699342884,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788699342884,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788880369400,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880369400,"index":0,"dt":[],"id":"advanced-direct-child","name":"subagent","args":["{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788880369400,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788880369400,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788880369400,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":3,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788880369405,"mode":"one-shot","label":"Check direct child"}} {"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{message:9}}"}},"sourceEventSeqs":[28],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} {"type":"step/start","data":{"turn":1,"step":4}} -{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788699342954,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699342954,"index":0,"dt":[],"id":"advanced-workflow","name":"workflow","args":["{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"]},{"type":"chunk","time":1788699342954,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"}}},{"type":"chunk","time":1788699342954,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788699342954,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788880369464,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880369464,"index":0,"dt":[],"id":"advanced-workflow","name":"workflow","args":["{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"]},{"type":"chunk","time":1788880369464,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"}}},{"type":"chunk","time":1788880369464,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788880369464,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":4,"callId":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"}} {"type":"tool-workflow/run-start","data":{"runId":"{{workflow:1}}","name":"advanced-headless-snapshot"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:3}}","childCreatedAt":1788880369552,"mode":"one-shot"}} {"type":"tool-workflow/agent-start","data":{"runId":"{{workflow:1}}","seq":1,"label":"workflow-child","phase":"Delegate","childId":"{{session:3}}"}} {"type":"tool-workflow/agent-end","data":{"runId":"{{workflow:1}}","seq":1,"outcome":"completed"}} {"type":"tool-workflow/run-end","data":{"runId":"{{workflow:1}}","stopReason":"completed"}} -{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-headless-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{message:11}}"}},"sourceEventSeqs":[33],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-headless-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{message:11}}"}},"sourceEventSeqs":[34],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":4}} {"type":"step/start","data":{"turn":1,"step":5}} -{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:12}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788699343171,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699343171,"index":0,"dt":[],"id":"advanced-undefine","name":"cordis_undefine","args":["{\"pluginId\":\"snap-1\"}"]},{"type":"chunk","time":1788699343171,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}}},{"type":"chunk","time":1788699343171,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788699343171,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:12}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788880369609,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880369609,"index":0,"dt":[],"id":"advanced-undefine","name":"cordis_undefine","args":["{\"pluginId\":\"snap-1\"}"]},{"type":"chunk","time":1788880369609,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}}},{"type":"chunk","time":1788880369609,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788880369609,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":5,"callId":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}} -{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{message:13}}"}},"sourceEventSeqs":[42],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{message:13}}"}},"sourceEventSeqs":[44],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":5}} {"type":"step/start","data":{"turn":1,"step":6}} -{"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_HEADLESS_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:14}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788699343189,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788699343189,"index":0,"dt":[],"texts":["ADVANCED_HEADLESS_OK"]},{"type":"chunk","time":1788699343189,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_HEADLESS_OK"}}},{"type":"chunk","time":1788699343189,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788699343189,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_HEADLESS_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:14}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788880369625,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788880369625,"index":0,"dt":[],"texts":["ADVANCED_HEADLESS_OK"]},{"type":"chunk","time":1788880369625,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_HEADLESS_OK"}}},{"type":"chunk","time":1788880369625,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788880369625,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":6}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/headless.snapshot.ts b/snapshots/session/headless.snapshot.ts index 552378d261..e1a55e6470 100644 --- a/snapshots/session/headless.snapshot.ts +++ b/snapshots/session/headless.snapshot.ts @@ -26,7 +26,6 @@ import { normalizedHeaders, normalizedSystemPrompts, normalizedToolSchemas, - omitSubagentCatalogForHistoricalComparison, parseSnapshotManifest, parseToolSchemasSnapshot, redactSessionSnapshotIds, @@ -789,17 +788,17 @@ describe('headless recorded-session snapshots', () => { try { const logs = [ [ - { type: 'session', version: 2, id: 'parent', createdAt: 1 }, + { type: 'session', version: SESSION_FORMAT_VERSION, id: 'parent', createdAt: 1 }, { type: 'subagent/catalog', data: { childId: 'child-z' } }, { type: 'subagent/catalog', data: { childId: 'child-a' } }, ], - [{ type: 'session', version: 2, id: 'child-z', createdAt: firstCreatedAt, parentSession: 'parent' }], - [{ type: 'session', version: 2, id: 'child-a', createdAt: 10, parentSession: 'parent' }], + [{ type: 'session', version: SESSION_FORMAT_VERSION, id: 'child-z', createdAt: firstCreatedAt, parentSession: 'parent' }], + [{ type: 'session', version: SESSION_FORMAT_VERSION, id: 'child-a', createdAt: 10, parentSession: 'parent' }], ].map(rows => rows.map(row => JSON.stringify(row)).join('\n') + '\n') for (const content of logs) { const directory = join(cwd, '.dsh', 'sessions', String(headerOf(content).id)) await mkdir(directory, { recursive: true }) - await writeFile(join(directory, 'session.v2.jsonl'), content) + await writeFile(join(directory, `session.v${SESSION_FORMAT_VERSION}.jsonl`), content) } const actual = await persistedSessions(cwd) expect(actual.map(log => log.header.id)).toEqual(['parent', 'child-z', 'child-a']) @@ -1013,9 +1012,6 @@ describe('headless recorded-session snapshots', () => { expect(await fixtureSessions(scenario), 'historical replay input remains unchanged').toEqual(fixtures) } const actualSnapshots = normalizeSessionSnapshots(actualLogs.map(log => log.content), actualContext) - .map(snapshot => scenario.manifest.sessionFormat === undefined - ? snapshot - : omitSubagentCatalogForHistoricalComparison(snapshot)) const expectedSnapshots = normalizeSessionSnapshots(expected, contextOf(expected)) for (const [index, actual] of actualSnapshots.entries()) { expect(records(actual), `${scenario.name}: session ${index}`).toEqual(records(expectedSnapshots[index] as string)) diff --git a/snapshots/session/ralph-loop/session.v2.jsonl b/snapshots/session/ralph-loop/session.v2.jsonl index aedf01a58c..a7c6797092 100644 --- a/snapshots/session/ralph-loop/session.v2.jsonl +++ b/snapshots/session/ralph-loop/session.v2.jsonl @@ -11,13 +11,11 @@ {"type":"session/title","data":{"title":"Run a two-round fresh-agent Ralph","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_ralph","name":"ralph","arguments":"{\"objective\":\"Prove two fresh Ralph rounds through the shipped headless app.\",\"maxRounds\":2}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":20,"outputTokens":8},"stream":[{"type":"chunk","time":1788500544849,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500544849,"index":0,"dt":[],"id":"call_ralph","name":"ralph","args":["{\"objective\":\"Prove two fresh Ralph rounds through the shipped headless app.\",\"maxRounds\":2}"]},{"type":"chunk","time":1788500544849,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_ralph","name":"ralph","arguments":"{\"objective\":\"Prove two fresh Ralph rounds through the shipped headless app.\",\"maxRounds\":2}"}}},{"type":"chunk","time":1788500544849,"chunk":{"type":"usage","usage":{"inputTokens":20,"outputTokens":8}}},{"type":"chunk","time":1788500544849,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_ralph","name":"ralph","arguments":"{\"objective\":\"Prove two fresh Ralph rounds through the shipped headless app.\",\"maxRounds\":2}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":20,"outputTokens":8},"stream":[{"type":"chunk","time":1788233515711,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233515711,"index":0,"dt":[],"id":"call_ralph","name":"ralph","args":["{\"objective\":\"Prove two fresh Ralph rounds through the shipped headless app.\",\"maxRounds\":2}"]},{"type":"chunk","time":1788233515711,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_ralph","name":"ralph","arguments":"{\"objective\":\"Prove two fresh Ralph rounds through the shipped headless app.\",\"maxRounds\":2}"}}},{"type":"chunk","time":1788233515711,"chunk":{"type":"usage","usage":{"inputTokens":20,"outputTokens":8}}},{"type":"chunk","time":1788233515711,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_ralph","name":"ralph","arguments":"{\"objective\":\"Prove two fresh Ralph rounds through the shipped headless app.\",\"maxRounds\":2}"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500544927,"mode":"one-shot"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:3}}","childCreatedAt":1788500544976,"mode":"one-shot"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_ralph"},"content":[{"type":"tool-result","toolCallId":"call_ralph","content":[{"type":"text","text":"Ralph worker reported completion after 2 rounds.\nFinal report:\n{\n \"status\": \"complete\",\n \"summary\": \"The Ralph snapshot objective is complete.\",\n \"evidence\": [\n \"Two fresh rounds completed through the shipped app.\"\n ],\n \"nextSteps\": [],\n \"blocker\": \"\"\n}"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"RALPH SNAPSHOT COMPLETE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":30,"outputTokens":4},"stream":[{"type":"chunk","time":1788500545054,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500545054,"index":0,"dt":[],"texts":["RALPH SNAPSHOT COMPLETE"]},{"type":"chunk","time":1788500545054,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"RALPH SNAPSHOT COMPLETE"}}},{"type":"chunk","time":1788500545054,"chunk":{"type":"usage","usage":{"inputTokens":30,"outputTokens":4}}},{"type":"chunk","time":1788500545054,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"RALPH SNAPSHOT COMPLETE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":30,"outputTokens":4},"stream":[{"type":"chunk","time":1788233515902,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788233515902,"index":0,"dt":[],"texts":["RALPH SNAPSHOT COMPLETE"]},{"type":"chunk","time":1788233515902,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"RALPH SNAPSHOT COMPLETE"}}},{"type":"chunk","time":1788233515902,"chunk":{"type":"usage","usage":{"inputTokens":30,"outputTokens":4}}},{"type":"chunk","time":1788233515902,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/ralph-loop/session.v3.jsonl b/snapshots/session/ralph-loop/session.v3.jsonl index 3db6677d03..50ac75f121 100644 --- a/snapshots/session/ralph-loop/session.v3.jsonl +++ b/snapshots/session/ralph-loop/session.v3.jsonl @@ -12,11 +12,13 @@ {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Run a two-round fresh-agent Ralph","messageSeqs":[8],"source":{"kind":"fallback"}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_ralph","name":"ralph","arguments":"{\"objective\":\"Prove two fresh Ralph rounds through the shipped headless app.\",\"maxRounds\":2}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":20,"outputTokens":8},"stream":[{"type":"chunk","time":1788699412474,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699412474,"index":0,"dt":[],"id":"call_ralph","name":"ralph","args":["{\"objective\":\"Prove two fresh Ralph rounds through the shipped headless app.\",\"maxRounds\":2}"]},{"type":"chunk","time":1788699412474,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_ralph","name":"ralph","arguments":"{\"objective\":\"Prove two fresh Ralph rounds through the shipped headless app.\",\"maxRounds\":2}"}}},{"type":"chunk","time":1788699412474,"chunk":{"type":"usage","usage":{"inputTokens":20,"outputTokens":8}}},{"type":"chunk","time":1788699412474,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_ralph","name":"ralph","arguments":"{\"objective\":\"Prove two fresh Ralph rounds through the shipped headless app.\",\"maxRounds\":2}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":20,"outputTokens":8},"stream":[{"type":"chunk","time":1788880374865,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880374865,"index":0,"dt":[],"id":"call_ralph","name":"ralph","args":["{\"objective\":\"Prove two fresh Ralph rounds through the shipped headless app.\",\"maxRounds\":2}"]},{"type":"chunk","time":1788880374865,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_ralph","name":"ralph","arguments":"{\"objective\":\"Prove two fresh Ralph rounds through the shipped headless app.\",\"maxRounds\":2}"}}},{"type":"chunk","time":1788880374865,"chunk":{"type":"usage","usage":{"inputTokens":20,"outputTokens":8}}},{"type":"chunk","time":1788880374865,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_ralph","name":"ralph","arguments":"{\"objective\":\"Prove two fresh Ralph rounds through the shipped headless app.\",\"maxRounds\":2}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788880374949,"mode":"one-shot"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:3}}","childCreatedAt":1788880375000,"mode":"one-shot"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_ralph"},"content":[{"type":"tool-result","toolCallId":"call_ralph","content":[{"type":"text","text":"Ralph worker reported completion after 2 rounds.\nFinal report:\n{\n \"status\": \"complete\",\n \"summary\": \"The Ralph snapshot objective is complete.\",\n \"evidence\": [\n \"Two fresh rounds completed through the shipped app.\"\n ],\n \"nextSteps\": [],\n \"blocker\": \"\"\n}"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"RALPH SNAPSHOT COMPLETE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":30,"outputTokens":4},"stream":[{"type":"chunk","time":1788699412801,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788699412801,"index":0,"dt":[],"texts":["RALPH SNAPSHOT COMPLETE"]},{"type":"chunk","time":1788699412801,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"RALPH SNAPSHOT COMPLETE"}}},{"type":"chunk","time":1788699412801,"chunk":{"type":"usage","usage":{"inputTokens":30,"outputTokens":4}}},{"type":"chunk","time":1788699412801,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"RALPH SNAPSHOT COMPLETE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":30,"outputTokens":4},"stream":[{"type":"chunk","time":1788880375062,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788880375062,"index":0,"dt":[],"texts":["RALPH SNAPSHOT COMPLETE"]},{"type":"chunk","time":1788880375062,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"RALPH SNAPSHOT COMPLETE"}}},{"type":"chunk","time":1788880375062,"chunk":{"type":"usage","usage":{"inputTokens":30,"outputTokens":4}}},{"type":"chunk","time":1788880375062,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/subagent-child-question-rejection/session.v2.jsonl b/snapshots/session/subagent-child-question-rejection/session.v2.jsonl index 7d11041678..13ab513d72 100644 --- a/snapshots/session/subagent-child-question-rejection/session.v2.jsonl +++ b/snapshots/session/subagent-child-question-rejection/session.v2.jsonl @@ -11,12 +11,11 @@ {"type":"session/title","data":{"title":"Delegate one question check. Ask","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_question_child","name":"subagent","arguments":"{\"description\":\"Check deployment question\",\"prompt\":\"Call ask_user_question once to ask whether deployment should use the CUDA fallback. If the tool returns an error, include the unresolved question verbatim in your final result.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500547269,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500547269,"index":0,"dt":[],"id":"call_question_child","name":"subagent","args":["{\"description\":\"Check deployment question\",\"prompt\":\"Call ask_user_question once to ask whether deployment should use the CUDA fallback. If the tool returns an error, include the unresolved question verbatim in your final result.\", \"run_in_background\":false}"]},{"type":"chunk","time":1788500547269,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_question_child","name":"subagent","arguments":"{\"description\":\"Check deployment question\",\"prompt\":\"Call ask_user_question once to ask whether deployment should use the CUDA fallback. If the tool returns an error, include the unresolved question verbatim in your final result.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788500547270,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500547270,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_question_child","name":"subagent","arguments":"{\"description\":\"Check deployment question\",\"prompt\":\"Call ask_user_question once to ask whether deployment should use the CUDA fallback. If the tool returns an error, include the unresolved question verbatim in your final result.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788233523819,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233523819,"index":0,"dt":[],"id":"call_question_child","name":"subagent","args":["{\"description\":\"Check deployment question\",\"prompt\":\"Call ask_user_question once to ask whether deployment should use the CUDA fallback. If the tool returns an error, include the unresolved question verbatim in your final result.\", \"run_in_background\":false}"]},{"type":"chunk","time":1788233523819,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_question_child","name":"subagent","arguments":"{\"description\":\"Check deployment question\",\"prompt\":\"Call ask_user_question once to ask whether deployment should use the CUDA fallback. If the tool returns an error, include the unresolved question verbatim in your final result.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788233523819,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788233523819,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_question_child","name":"subagent","arguments":"{\"description\":\"Check deployment question\",\"prompt\":\"Call ask_user_question once to ask whether deployment should use the CUDA fallback. If the tool returns an error, include the unresolved question verbatim in your final result.\", \"run_in_background\":false}"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500547276,"mode":"one-shot","label":"Check deployment question"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_question_child"},"content":[{"type":"tool-result","toolCallId":"call_question_child","content":[{"type":"text","text":"UNRESOLVED: Should deployment use the CUDA fallback?"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_COMPLETED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788500547345,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500547345,"index":0,"dt":[],"texts":["PARENT_COMPLETED"]},{"type":"chunk","time":1788500547345,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_COMPLETED"}}},{"type":"chunk","time":1788500547345,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788500547345,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_COMPLETED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788233523914,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788233523915,"index":0,"dt":[],"texts":["PARENT_COMPLETED"]},{"type":"chunk","time":1788233523915,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_COMPLETED"}}},{"type":"chunk","time":1788233523915,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788233523915,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/subagent-child-question-rejection/session.v3.jsonl b/snapshots/session/subagent-child-question-rejection/session.v3.jsonl index 3efb7c1692..b406f778e8 100644 --- a/snapshots/session/subagent-child-question-rejection/session.v3.jsonl +++ b/snapshots/session/subagent-child-question-rejection/session.v3.jsonl @@ -12,11 +12,12 @@ {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Delegate one question check. Ask","messageSeqs":[8],"source":{"kind":"fallback"}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_question_child","name":"subagent","arguments":"{\"description\":\"Check deployment question\",\"prompt\":\"Call ask_user_question once to ask whether deployment should use the CUDA fallback. If the tool returns an error, include the unresolved question verbatim in your final result.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788699434793,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699434794,"index":0,"dt":[],"id":"call_question_child","name":"subagent","args":["{\"description\":\"Check deployment question\",\"prompt\":\"Call ask_user_question once to ask whether deployment should use the CUDA fallback. If the tool returns an error, include the unresolved question verbatim in your final result.\", \"run_in_background\":false}"]},{"type":"chunk","time":1788699434794,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_question_child","name":"subagent","arguments":"{\"description\":\"Check deployment question\",\"prompt\":\"Call ask_user_question once to ask whether deployment should use the CUDA fallback. If the tool returns an error, include the unresolved question verbatim in your final result.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788699434794,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788699434794,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_question_child","name":"subagent","arguments":"{\"description\":\"Check deployment question\",\"prompt\":\"Call ask_user_question once to ask whether deployment should use the CUDA fallback. If the tool returns an error, include the unresolved question verbatim in your final result.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788880377210,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880377210,"index":0,"dt":[],"id":"call_question_child","name":"subagent","args":["{\"description\":\"Check deployment question\",\"prompt\":\"Call ask_user_question once to ask whether deployment should use the CUDA fallback. If the tool returns an error, include the unresolved question verbatim in your final result.\", \"run_in_background\":false}"]},{"type":"chunk","time":1788880377210,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_question_child","name":"subagent","arguments":"{\"description\":\"Check deployment question\",\"prompt\":\"Call ask_user_question once to ask whether deployment should use the CUDA fallback. If the tool returns an error, include the unresolved question verbatim in your final result.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788880377210,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788880377210,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_question_child","name":"subagent","arguments":"{\"description\":\"Check deployment question\",\"prompt\":\"Call ask_user_question once to ask whether deployment should use the CUDA fallback. If the tool returns an error, include the unresolved question verbatim in your final result.\", \"run_in_background\":false}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788880377217,"mode":"one-shot","label":"Check deployment question"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_question_child"},"content":[{"type":"tool-result","toolCallId":"call_question_child","content":[{"type":"text","text":"UNRESOLVED: Should deployment use the CUDA fallback?"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_COMPLETED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788699434919,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788699434919,"index":0,"dt":[],"texts":["PARENT_COMPLETED"]},{"type":"chunk","time":1788699434919,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_COMPLETED"}}},{"type":"chunk","time":1788699434919,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788699434919,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_COMPLETED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788880377302,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788880377302,"index":0,"dt":[],"texts":["PARENT_COMPLETED"]},{"type":"chunk","time":1788880377302,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_COMPLETED"}}},{"type":"chunk","time":1788880377302,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788880377302,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/subagent-depth-two-rejection/session.1.v2.jsonl b/snapshots/session/subagent-depth-two-rejection/session.1.v2.jsonl index 98334abe6f..459b241c5d 100644 --- a/snapshots/session/subagent-depth-two-rejection/session.1.v2.jsonl +++ b/snapshots/session/subagent-depth-two-rejection/session.1.v2.jsonl @@ -12,12 +12,11 @@ {"type":"session/title","data":{"title":"Call subagent once. Ask that","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_depth_one_child","name":"subagent","arguments":"{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500548365,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500548366,"index":0,"dt":[],"id":"call_depth_one_child","name":"subagent","args":["{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788500548366,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_depth_one_child","name":"subagent","arguments":"{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788500548366,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500548366,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_depth_one_child","name":"subagent","arguments":"{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788233524598,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233524598,"index":0,"dt":[],"id":"call_depth_one_child","name":"subagent","args":["{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788233524598,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_depth_one_child","name":"subagent","arguments":"{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788233524598,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788233524598,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_depth_one_child","name":"subagent","arguments":"{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\",\"run_in_background\":false}"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:3}}","childCreatedAt":1788500548373,"mode":"one-shot","label":"Start depth two"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_depth_one_child"},"content":[{"type":"tool-result","toolCallId":"call_depth_one_child","content":[{"type":"text","text":"DEPTH_REJECTED"}],"isError":false}],"role":"user","id":"{{message:9}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DEPTH_ONE_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788500548476,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500548476,"index":0,"dt":[],"texts":["DEPTH_ONE_DONE"]},{"type":"chunk","time":1788500548477,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DEPTH_ONE_DONE"}}},{"type":"chunk","time":1788500548477,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788500548477,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DEPTH_ONE_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788233524703,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788233524703,"index":0,"dt":[],"texts":["DEPTH_ONE_DONE"]},{"type":"chunk","time":1788233524703,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DEPTH_ONE_DONE"}}},{"type":"chunk","time":1788233524703,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788233524703,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/subagent-depth-two-rejection/session.1.v3.jsonl b/snapshots/session/subagent-depth-two-rejection/session.1.v3.jsonl index 73b435a3af..8e966a7894 100644 --- a/snapshots/session/subagent-depth-two-rejection/session.1.v3.jsonl +++ b/snapshots/session/subagent-depth-two-rejection/session.1.v3.jsonl @@ -13,11 +13,12 @@ {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Call subagent once. Ask that","messageSeqs":[9],"source":{"kind":"fallback"}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_depth_one_child","name":"subagent","arguments":"{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788699436207,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699436207,"index":0,"dt":[],"id":"call_depth_one_child","name":"subagent","args":["{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788699436207,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_depth_one_child","name":"subagent","arguments":"{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788699436207,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788699436207,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_depth_one_child","name":"subagent","arguments":"{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788880378254,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880378255,"index":0,"dt":[],"id":"call_depth_one_child","name":"subagent","args":["{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788880378255,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_depth_one_child","name":"subagent","arguments":"{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788880378255,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788880378255,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_depth_one_child","name":"subagent","arguments":"{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\",\"run_in_background\":false}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:3}}","childCreatedAt":1788880378258,"mode":"one-shot","label":"Start depth two"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_depth_one_child"},"content":[{"type":"tool-result","toolCallId":"call_depth_one_child","content":[{"type":"text","text":"DEPTH_REJECTED"}],"isError":false}],"role":"user","id":"{{message:11}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DEPTH_ONE_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:12}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788699436309,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788699436309,"index":0,"dt":[],"texts":["DEPTH_ONE_DONE"]},{"type":"chunk","time":1788699436309,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DEPTH_ONE_DONE"}}},{"type":"chunk","time":1788699436309,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788699436309,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DEPTH_ONE_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:12}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788880378331,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788880378331,"index":0,"dt":[],"texts":["DEPTH_ONE_DONE"]},{"type":"chunk","time":1788880378331,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DEPTH_ONE_DONE"}}},{"type":"chunk","time":1788880378331,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788880378331,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/subagent-depth-two-rejection/session.v2.jsonl b/snapshots/session/subagent-depth-two-rejection/session.v2.jsonl index e4b17cd756..764ab99c92 100644 --- a/snapshots/session/subagent-depth-two-rejection/session.v2.jsonl +++ b/snapshots/session/subagent-depth-two-rejection/session.v2.jsonl @@ -11,12 +11,11 @@ {"type":"session/title","data":{"title":"Delegate through two child generations.","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_root_child","name":"subagent","arguments":"{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500548303,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500548303,"index":0,"dt":[],"id":"call_root_child","name":"subagent","args":["{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788500548303,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_root_child","name":"subagent","arguments":"{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788500548303,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500548303,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_root_child","name":"subagent","arguments":"{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788233524552,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233524552,"index":0,"dt":[],"id":"call_root_child","name":"subagent","args":["{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788233524552,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_root_child","name":"subagent","arguments":"{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788233524553,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788233524553,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_root_child","name":"subagent","arguments":"{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\",\"run_in_background\":false}"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500548310,"mode":"one-shot","label":"Start depth one"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_root_child"},"content":[{"type":"tool-result","toolCallId":"call_root_child","content":[{"type":"text","text":"DEPTH_ONE_DONE"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"ROOT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788500548498,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500548498,"index":0,"dt":[],"texts":["ROOT_DONE"]},{"type":"chunk","time":1788500548498,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ROOT_DONE"}}},{"type":"chunk","time":1788500548498,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788500548498,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"ROOT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788233524723,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788233524723,"index":0,"dt":[],"texts":["ROOT_DONE"]},{"type":"chunk","time":1788233524723,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ROOT_DONE"}}},{"type":"chunk","time":1788233524723,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788233524723,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/subagent-depth-two-rejection/session.v3.jsonl b/snapshots/session/subagent-depth-two-rejection/session.v3.jsonl index 1bfe219dec..e505d6093e 100644 --- a/snapshots/session/subagent-depth-two-rejection/session.v3.jsonl +++ b/snapshots/session/subagent-depth-two-rejection/session.v3.jsonl @@ -12,11 +12,12 @@ {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Delegate through two child generations.","messageSeqs":[8],"source":{"kind":"fallback"}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_root_child","name":"subagent","arguments":"{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788699436153,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699436153,"index":0,"dt":[],"id":"call_root_child","name":"subagent","args":["{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788699436154,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_root_child","name":"subagent","arguments":"{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788699436154,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788699436154,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_root_child","name":"subagent","arguments":"{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788880378211,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880378211,"index":0,"dt":[],"id":"call_root_child","name":"subagent","args":["{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788880378211,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_root_child","name":"subagent","arguments":"{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788880378211,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788880378211,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_root_child","name":"subagent","arguments":"{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\",\"run_in_background\":false}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788880378217,"mode":"one-shot","label":"Start depth one"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_root_child"},"content":[{"type":"tool-result","toolCallId":"call_root_child","content":[{"type":"text","text":"DEPTH_ONE_DONE"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"ROOT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788699436343,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788699436343,"index":0,"dt":[],"texts":["ROOT_DONE"]},{"type":"chunk","time":1788699436343,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ROOT_DONE"}}},{"type":"chunk","time":1788699436343,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788699436343,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"ROOT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788880378354,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788880378354,"index":0,"dt":[],"texts":["ROOT_DONE"]},{"type":"chunk","time":1788880378354,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ROOT_DONE"}}},{"type":"chunk","time":1788880378354,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788880378354,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/subagent-max-tokens-partial/session.v2.jsonl b/snapshots/session/subagent-max-tokens-partial/session.v2.jsonl index 82120de5f6..fc7b96f216 100644 --- a/snapshots/session/subagent-max-tokens-partial/session.v2.jsonl +++ b/snapshots/session/subagent-max-tokens-partial/session.v2.jsonl @@ -11,12 +11,11 @@ {"type":"session/title","data":{"title":"Use the subagent tool exactly","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_parent_1","name":"subagent","arguments":"{\"description\": \"Truncated child\", \"prompt\": \"Write the words 'partial one', call todo_write once, then keep going until you are cut off.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500549527,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"chunk","time":1788500549527,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_parent_1","name":"subagent","arguments":"{\"description\": \"Truncated child\", \"prompt\": \"Write the words 'partial one', call todo_write once, then keep going until you are cut off.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788500549527,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500549527,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_parent_1","name":"subagent","arguments":"{\"description\": \"Truncated child\", \"prompt\": \"Write the words 'partial one', call todo_write once, then keep going until you are cut off.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788233525350,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"chunk","time":1788233525350,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_parent_1","name":"subagent","arguments":"{\"description\": \"Truncated child\", \"prompt\": \"Write the words 'partial one', call todo_write once, then keep going until you are cut off.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788233525350,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788233525350,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_parent_1","name":"subagent","arguments":"{\"description\": \"Truncated child\", \"prompt\": \"Write the words 'partial one', call todo_write once, then keep going until you are cut off.\", \"run_in_background\":false}"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500549533,"mode":"one-shot","label":"Truncated child"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_parent_1"},"content":[{"type":"tool-result","toolCallId":"call_parent_1","content":[{"type":"text","text":"Error: subagent run hit its token limit before finishing\nPartial output before the run ended:\npartial one"}],"isError":true}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":12,"outputTokens":2},"stream":[{"type":"chunk","time":1788500549606,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":1788500549606,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788500549606,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2}}},{"type":"chunk","time":1788500549606,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":12,"outputTokens":2},"stream":[{"type":"chunk","time":1788233525447,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":1788233525447,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788233525447,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2}}},{"type":"chunk","time":1788233525447,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/subagent-max-tokens-partial/session.v3.jsonl b/snapshots/session/subagent-max-tokens-partial/session.v3.jsonl index 706401d941..89b886a2df 100644 --- a/snapshots/session/subagent-max-tokens-partial/session.v3.jsonl +++ b/snapshots/session/subagent-max-tokens-partial/session.v3.jsonl @@ -12,11 +12,12 @@ {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Use the subagent tool exactly","messageSeqs":[8],"source":{"kind":"fallback"}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_parent_1","name":"subagent","arguments":"{\"description\": \"Truncated child\", \"prompt\": \"Write the words 'partial one', call todo_write once, then keep going until you are cut off.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788699437598,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"chunk","time":1788699437598,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_parent_1","name":"subagent","arguments":"{\"description\": \"Truncated child\", \"prompt\": \"Write the words 'partial one', call todo_write once, then keep going until you are cut off.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788699437598,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788699437598,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_parent_1","name":"subagent","arguments":"{\"description\": \"Truncated child\", \"prompt\": \"Write the words 'partial one', call todo_write once, then keep going until you are cut off.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788880379254,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"chunk","time":1788880379254,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_parent_1","name":"subagent","arguments":"{\"description\": \"Truncated child\", \"prompt\": \"Write the words 'partial one', call todo_write once, then keep going until you are cut off.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788880379254,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788880379254,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_parent_1","name":"subagent","arguments":"{\"description\": \"Truncated child\", \"prompt\": \"Write the words 'partial one', call todo_write once, then keep going until you are cut off.\", \"run_in_background\":false}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788880379260,"mode":"one-shot","label":"Truncated child"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_parent_1"},"content":[{"type":"tool-result","toolCallId":"call_parent_1","content":[{"type":"text","text":"Error: subagent run hit its token limit before finishing\nPartial output before the run ended:\npartial one"}],"isError":true}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":12,"outputTokens":2},"stream":[{"type":"chunk","time":1788699437684,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":1788699437684,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788699437684,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2}}},{"type":"chunk","time":1788699437684,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":12,"outputTokens":2},"stream":[{"type":"chunk","time":1788880379335,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":1788880379335,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788880379335,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2}}},{"type":"chunk","time":1788880379335,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/subagent-multi/writer.expected.jsonl b/snapshots/session/subagent-multi/writer.expected.jsonl index acf0aceae9..2546fb8bd3 100644 --- a/snapshots/session/subagent-multi/writer.expected.jsonl +++ b/snapshots/session/subagent-multi/writer.expected.jsonl @@ -12,16 +12,18 @@ {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Use the subagent tool TWICE,","messageSeqs":[8],"source":{"kind":"fallback"}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to use the subagent tool twice, sequentially (one at a time). First subagent should reply with \"ALPHA\", second with \"BETA\". After both return, I reply with \"PARENT_DONE\"."},{"type":"tool-call","id":"call_00_7zDCCjdsQgrk5LR2bAEQ1010","name":"subagent","arguments":"{\"description\": \"Return ALPHA only\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\": false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":2938,"outputTokens":124,"cacheReadTokens":0,"reasoningTokens":49},"stream":[{"type":"chunk","time":1788699438892,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788699438892,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," use"," the"," sub","agent"," tool"," twice",","," sequentially"," (","one"," at"," a"," time",")."," First"," sub","agent"," should"," reply"," with"," \"","AL","P","HA","\","," second"," with"," \"","B","ETA","\"."," After"," both"," return",","," I"," reply"," with"," \"","PAR","ENT","_D","ONE","\"."]},{"type":"chunk","time":1788699438893,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699438893,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_7zDCCjdsQgrk5LR2bAEQ1010","name":"subagent","args":["","{","\"","description","\"",": ","\"","Return"," AL","P","HA"," only","\"",", ","\"","prom","pt","\"",": ","\"","Reply"," with"," exactly"," the"," word"," AL","P","HA"," and"," nothing"," else",".","\"",", \"run_in_background\": false}"]},{"type":"chunk","time":1788699438893,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to use the subagent tool twice, sequentially (one at a time). First subagent should reply with \"ALPHA\", second with \"BETA\". After both return, I reply with \"PARENT_DONE\"."}}},{"type":"chunk","time":1788699438893,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_7zDCCjdsQgrk5LR2bAEQ1010","name":"subagent","arguments":"{\"description\": \"Return ALPHA only\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\": false}"}}},{"type":"chunk","time":1788699438893,"chunk":{"type":"usage","usage":{"inputTokens":2938,"outputTokens":124,"cacheReadTokens":0,"reasoningTokens":49}}},{"type":"chunk","time":1788699438893,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to use the subagent tool twice, sequentially (one at a time). First subagent should reply with \"ALPHA\", second with \"BETA\". After both return, I reply with \"PARENT_DONE\"."},{"type":"tool-call","id":"call_00_7zDCCjdsQgrk5LR2bAEQ1010","name":"subagent","arguments":"{\"description\": \"Return ALPHA only\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\": false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":2938,"outputTokens":124,"cacheReadTokens":0,"reasoningTokens":49},"stream":[{"type":"chunk","time":1788880380242,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788880380242,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," use"," the"," sub","agent"," tool"," twice",","," sequentially"," (","one"," at"," a"," time",")."," First"," sub","agent"," should"," reply"," with"," \"","AL","P","HA","\","," second"," with"," \"","B","ETA","\"."," After"," both"," return",","," I"," reply"," with"," \"","PAR","ENT","_D","ONE","\"."]},{"type":"chunk","time":1788880380243,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880380243,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_7zDCCjdsQgrk5LR2bAEQ1010","name":"subagent","args":["","{","\"","description","\"",": ","\"","Return"," AL","P","HA"," only","\"",", ","\"","prom","pt","\"",": ","\"","Reply"," with"," exactly"," the"," word"," AL","P","HA"," and"," nothing"," else",".","\"",", \"run_in_background\": false}"]},{"type":"chunk","time":1788880380243,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to use the subagent tool twice, sequentially (one at a time). First subagent should reply with \"ALPHA\", second with \"BETA\". After both return, I reply with \"PARENT_DONE\"."}}},{"type":"chunk","time":1788880380243,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_7zDCCjdsQgrk5LR2bAEQ1010","name":"subagent","arguments":"{\"description\": \"Return ALPHA only\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\": false}"}}},{"type":"chunk","time":1788880380243,"chunk":{"type":"usage","usage":{"inputTokens":2938,"outputTokens":124,"cacheReadTokens":0,"reasoningTokens":49}}},{"type":"chunk","time":1788880380243,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_00_7zDCCjdsQgrk5LR2bAEQ1010","name":"subagent","arguments":"{\"description\": \"Return ALPHA only\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\": false}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788880380249,"mode":"one-shot","label":"Return ALPHA only"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_7zDCCjdsQgrk5LR2bAEQ1010"},"content":[{"type":"tool-result","toolCallId":"call_00_7zDCCjdsQgrk5LR2bAEQ1010","content":[{"type":"text","text":"ALPHA"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"First subagent returned \"ALPHA\". Now I'll call the second subagent to return \"BETA\"."},{"type":"tool-call","id":"call_00_FudNKuJ0fchSptGy3Scw1411","name":"subagent","arguments":"{\"description\": \"Return BETA only\", \"prompt\": \"Reply with exactly the word BETA and nothing else.\", \"run_in_background\": false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":133,"outputTokens":96,"cacheReadTokens":2944,"reasoningTokens":23},"stream":[{"type":"chunk","time":1788699438974,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788699438974,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0],"texts":["First"," sub","agent"," returned"," \"","AL","P","HA","\"."," Now"," I","'ll"," call"," the"," second"," sub","agent"," to"," return"," \"","B","ETA","\"."]},{"type":"chunk","time":1788699438975,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699438975,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_FudNKuJ0fchSptGy3Scw1411","name":"subagent","args":["","{","\"","description","\"",": ","\"","Return"," B","ETA"," only","\"",", ","\"","prom","pt","\"",": ","\"","Reply"," with"," exactly"," the"," word"," B","ETA"," and"," nothing"," else",".","\"",", \"run_in_background\": false}"]},{"type":"chunk","time":1788699438975,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"First subagent returned \"ALPHA\". Now I'll call the second subagent to return \"BETA\"."}}},{"type":"chunk","time":1788699438975,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_FudNKuJ0fchSptGy3Scw1411","name":"subagent","arguments":"{\"description\": \"Return BETA only\", \"prompt\": \"Reply with exactly the word BETA and nothing else.\", \"run_in_background\": false}"}}},{"type":"chunk","time":1788699438975,"chunk":{"type":"usage","usage":{"inputTokens":133,"outputTokens":96,"cacheReadTokens":2944,"reasoningTokens":23}}},{"type":"chunk","time":1788699438975,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"First subagent returned \"ALPHA\". Now I'll call the second subagent to return \"BETA\"."},{"type":"tool-call","id":"call_00_FudNKuJ0fchSptGy3Scw1411","name":"subagent","arguments":"{\"description\": \"Return BETA only\", \"prompt\": \"Reply with exactly the word BETA and nothing else.\", \"run_in_background\": false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":133,"outputTokens":96,"cacheReadTokens":2944,"reasoningTokens":23},"stream":[{"type":"chunk","time":1788880380308,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788880380309,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["First"," sub","agent"," returned"," \"","AL","P","HA","\"."," Now"," I","'ll"," call"," the"," second"," sub","agent"," to"," return"," \"","B","ETA","\"."]},{"type":"chunk","time":1788880380309,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880380309,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_FudNKuJ0fchSptGy3Scw1411","name":"subagent","args":["","{","\"","description","\"",": ","\"","Return"," B","ETA"," only","\"",", ","\"","prom","pt","\"",": ","\"","Reply"," with"," exactly"," the"," word"," B","ETA"," and"," nothing"," else",".","\"",", \"run_in_background\": false}"]},{"type":"chunk","time":1788880380309,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"First subagent returned \"ALPHA\". Now I'll call the second subagent to return \"BETA\"."}}},{"type":"chunk","time":1788880380309,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_FudNKuJ0fchSptGy3Scw1411","name":"subagent","arguments":"{\"description\": \"Return BETA only\", \"prompt\": \"Reply with exactly the word BETA and nothing else.\", \"run_in_background\": false}"}}},{"type":"chunk","time":1788880380309,"chunk":{"type":"usage","usage":{"inputTokens":133,"outputTokens":96,"cacheReadTokens":2944,"reasoningTokens":23}}},{"type":"chunk","time":1788880380309,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"call_00_FudNKuJ0fchSptGy3Scw1411","name":"subagent","arguments":"{\"description\": \"Return BETA only\", \"prompt\": \"Reply with exactly the word BETA and nothing else.\", \"run_in_background\": false}"}} -{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_00_FudNKuJ0fchSptGy3Scw1411"},"content":[{"type":"tool-result","toolCallId":"call_00_FudNKuJ0fchSptGy3Scw1411","content":[{"type":"text","text":"BETA"}],"isError":false}],"role":"user","id":"{{message:7}}"}},"sourceEventSeqs":[19],"surfaceOp":"append"} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:3}}","childCreatedAt":1788880380316,"mode":"one-shot","label":"Return BETA only"}} +{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_00_FudNKuJ0fchSptGy3Scw1411"},"content":[{"type":"tool-result","toolCallId":"call_00_FudNKuJ0fchSptGy3Scw1411","content":[{"type":"text","text":"BETA"}],"isError":false}],"role":"user","id":"{{message:7}}"}},"sourceEventSeqs":[20],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} -{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"reasoning","text":"Both subagents have returned: first with \"ALPHA\", second with \"BETA\". Now I should reply with \"PARENT_DONE\"."},{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":115,"outputTokens":35,"cacheReadTokens":3072,"reasoningTokens":30},"stream":[{"type":"chunk","time":1788699439044,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788699439044,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["Both"," sub","agents"," have"," returned",":"," first"," with"," \"","AL","P","HA","\","," second"," with"," \"","B","ETA","\"."," Now"," I"," should"," reply"," with"," \"","PAR","ENT","_D","ONE","\"."]},{"type":"chunk","time":1788699439044,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788699439044,"index":1,"dt":[0,0,0],"texts":["PAR","ENT","_D","ONE"]},{"type":"chunk","time":1788699439044,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Both subagents have returned: first with \"ALPHA\", second with \"BETA\". Now I should reply with \"PARENT_DONE\"."}}},{"type":"chunk","time":1788699439044,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788699439044,"chunk":{"type":"usage","usage":{"inputTokens":115,"outputTokens":35,"cacheReadTokens":3072,"reasoningTokens":30}}},{"type":"chunk","time":1788699439044,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"reasoning","text":"Both subagents have returned: first with \"ALPHA\", second with \"BETA\". Now I should reply with \"PARENT_DONE\"."},{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":115,"outputTokens":35,"cacheReadTokens":3072,"reasoningTokens":30},"stream":[{"type":"chunk","time":1788880380369,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788880380369,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["Both"," sub","agents"," have"," returned",":"," first"," with"," \"","AL","P","HA","\","," second"," with"," \"","B","ETA","\"."," Now"," I"," should"," reply"," with"," \"","PAR","ENT","_D","ONE","\"."]},{"type":"chunk","time":1788880380369,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788880380369,"index":1,"dt":[0,0,0],"texts":["PAR","ENT","_D","ONE"]},{"type":"chunk","time":1788880380369,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Both subagents have returned: first with \"ALPHA\", second with \"BETA\". Now I should reply with \"PARENT_DONE\"."}}},{"type":"chunk","time":1788880380369,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788880380369,"chunk":{"type":"usage","usage":{"inputTokens":115,"outputTokens":35,"cacheReadTokens":3072,"reasoningTokens":30}}},{"type":"chunk","time":1788880380369,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/subagent-parallel/session.v2.jsonl b/snapshots/session/subagent-parallel/session.v2.jsonl index 2cbca0e0fb..a01d33384e 100644 --- a/snapshots/session/subagent-parallel/session.v2.jsonl +++ b/snapshots/session/subagent-parallel/session.v2.jsonl @@ -11,15 +11,13 @@ {"type":"session/title","data":{"title":"Use the subagent tool TWICE","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_parallel_alpha_1","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"},{"type":"tool-call","id":"call_parallel_alpha_2","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"stream":[{"type":"chunk","time":1788500551334,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"chunk","time":1788500551334,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_parallel_alpha_1","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788500551334,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"chunk","time":1788500551334,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_parallel_alpha_2","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788500551334,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_parallel_alpha_1","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"},{"type":"tool-call","id":"call_parallel_alpha_2","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"stream":[{"type":"chunk","time":1788233526821,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"chunk","time":1788233526821,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_parallel_alpha_1","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788233526821,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"chunk","time":1788233526821,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_parallel_alpha_2","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788233526821,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_parallel_alpha_1","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_parallel_alpha_2","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500551344,"mode":"one-shot","label":"Say the word ALPHA"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:3}}","childCreatedAt":1788500551344,"mode":"one-shot","label":"Say the word ALPHA"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_parallel_alpha_1"},"content":[{"type":"tool-result","toolCallId":"call_parallel_alpha_1","content":[{"type":"text","text":"ALPHA"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_parallel_alpha_2"},"content":[{"type":"tool-result","toolCallId":"call_parallel_alpha_2","content":[{"type":"text","text":"ALPHA"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"stream":[{"type":"chunk","time":1788500551413,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":1788500551413,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788500551413,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"stream":[{"type":"chunk","time":1788233526904,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":1788233526904,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788233526904,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/subagent-parallel/session.v3.jsonl b/snapshots/session/subagent-parallel/session.v3.jsonl index 5a94290323..96f0ff4011 100644 --- a/snapshots/session/subagent-parallel/session.v3.jsonl +++ b/snapshots/session/subagent-parallel/session.v3.jsonl @@ -12,13 +12,15 @@ {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Use the subagent tool TWICE","messageSeqs":[8],"source":{"kind":"fallback"}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_parallel_alpha_1","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"},{"type":"tool-call","id":"call_parallel_alpha_2","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"stream":[{"type":"chunk","time":1788699440331,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"chunk","time":1788699440331,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_parallel_alpha_1","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788699440331,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"chunk","time":1788699440331,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_parallel_alpha_2","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788699440331,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_parallel_alpha_1","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"},{"type":"tool-call","id":"call_parallel_alpha_2","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"stream":[{"type":"chunk","time":1788880381283,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"chunk","time":1788880381283,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_parallel_alpha_1","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788880381283,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"chunk","time":1788880381283,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_parallel_alpha_2","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788880381283,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_parallel_alpha_1","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_parallel_alpha_2","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788880381293,"mode":"one-shot","label":"Say the word ALPHA"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:3}}","childCreatedAt":1788880381293,"mode":"one-shot","label":"Say the word ALPHA"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_parallel_alpha_1"},"content":[{"type":"tool-result","toolCallId":"call_parallel_alpha_1","content":[{"type":"text","text":"ALPHA"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_parallel_alpha_2"},"content":[{"type":"tool-result","toolCallId":"call_parallel_alpha_2","content":[{"type":"text","text":"ALPHA"}],"isError":false}],"role":"user","id":"{{message:6}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"stream":[{"type":"chunk","time":1788699440444,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":1788699440444,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788699440444,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"stream":[{"type":"chunk","time":1788880381374,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":1788880381374,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788880381374,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/subagent-published-run-failure/session.v2.jsonl b/snapshots/session/subagent-published-run-failure/session.v2.jsonl index e723042e93..62755d8d49 100644 --- a/snapshots/session/subagent-published-run-failure/session.v2.jsonl +++ b/snapshots/session/subagent-published-run-failure/session.v2.jsonl @@ -11,12 +11,11 @@ {"type":"session/title","data":{"title":"Delegate one foreground subagent. Its","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_published_failure","name":"subagent","arguments":"{\"description\":\"Fail published run\",\"prompt\":\"This child prompt must never run.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788500552312,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500552312,"index":0,"dt":[],"id":"call_published_failure","name":"subagent","args":["{\"description\":\"Fail published run\",\"prompt\":\"This child prompt must never run.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788500552312,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_published_failure","name":"subagent","arguments":"{\"description\":\"Fail published run\",\"prompt\":\"This child prompt must never run.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788500552312,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788500552312,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_published_failure","name":"subagent","arguments":"{\"description\":\"Fail published run\",\"prompt\":\"This child prompt must never run.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788233527588,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233527588,"index":0,"dt":[],"id":"call_published_failure","name":"subagent","args":["{\"description\":\"Fail published run\",\"prompt\":\"This child prompt must never run.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788233527588,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_published_failure","name":"subagent","arguments":"{\"description\":\"Fail published run\",\"prompt\":\"This child prompt must never run.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788233527588,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788233527588,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_published_failure","name":"subagent","arguments":"{\"description\":\"Fail published run\",\"prompt\":\"This child prompt must never run.\",\"run_in_background\":false}"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500552318,"mode":"one-shot","label":"Fail published run"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_published_failure"},"content":[{"type":"tool-result","toolCallId":"call_published_failure","content":[{"type":"text","text":"Error: subagent run failed: Error: snapshot published run failed; dispose failed: Error: snapshot published handle disposal failed"}],"isError":true}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_OBSERVED_ERROR"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788500552354,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788500552354,"index":0,"dt":[],"texts":["PARENT_OBSERVED_ERROR"]},{"type":"chunk","time":1788500552354,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_OBSERVED_ERROR"}}},{"type":"chunk","time":1788500552354,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788500552354,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_OBSERVED_ERROR"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788233527613,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788233527613,"index":0,"dt":[],"texts":["PARENT_OBSERVED_ERROR"]},{"type":"chunk","time":1788233527613,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_OBSERVED_ERROR"}}},{"type":"chunk","time":1788233527613,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788233527613,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/subagent-published-run-failure/session.v3.jsonl b/snapshots/session/subagent-published-run-failure/session.v3.jsonl index 1f0d7013f9..8a748eda1f 100644 --- a/snapshots/session/subagent-published-run-failure/session.v3.jsonl +++ b/snapshots/session/subagent-published-run-failure/session.v3.jsonl @@ -12,11 +12,12 @@ {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Delegate one foreground subagent. Its","messageSeqs":[8],"source":{"kind":"fallback"}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_published_failure","name":"subagent","arguments":"{\"description\":\"Fail published run\",\"prompt\":\"This child prompt must never run.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788699441712,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699441712,"index":0,"dt":[],"id":"call_published_failure","name":"subagent","args":["{\"description\":\"Fail published run\",\"prompt\":\"This child prompt must never run.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788699441713,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_published_failure","name":"subagent","arguments":"{\"description\":\"Fail published run\",\"prompt\":\"This child prompt must never run.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788699441713,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788699441713,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_published_failure","name":"subagent","arguments":"{\"description\":\"Fail published run\",\"prompt\":\"This child prompt must never run.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788880382297,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880382297,"index":0,"dt":[],"id":"call_published_failure","name":"subagent","args":["{\"description\":\"Fail published run\",\"prompt\":\"This child prompt must never run.\",\"run_in_background\":false}"]},{"type":"chunk","time":1788880382297,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_published_failure","name":"subagent","arguments":"{\"description\":\"Fail published run\",\"prompt\":\"This child prompt must never run.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1788880382297,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788880382297,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_published_failure","name":"subagent","arguments":"{\"description\":\"Fail published run\",\"prompt\":\"This child prompt must never run.\",\"run_in_background\":false}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788880382302,"mode":"one-shot","label":"Fail published run"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_published_failure"},"content":[{"type":"tool-result","toolCallId":"call_published_failure","content":[{"type":"text","text":"Error: subagent run failed: Error: snapshot published run failed; dispose failed: Error: snapshot published handle disposal failed"}],"isError":true}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_OBSERVED_ERROR"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788699441760,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788699441760,"index":0,"dt":[],"texts":["PARENT_OBSERVED_ERROR"]},{"type":"chunk","time":1788699441760,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_OBSERVED_ERROR"}}},{"type":"chunk","time":1788699441760,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788699441760,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_OBSERVED_ERROR"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788880382338,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788880382338,"index":0,"dt":[],"texts":["PARENT_OBSERVED_ERROR"]},{"type":"chunk","time":1788880382338,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_OBSERVED_ERROR"}}},{"type":"chunk","time":1788880382338,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788880382339,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/subagent-spawn-in-process/session.v2.jsonl b/snapshots/session/subagent-spawn-in-process/session.v2.jsonl index a2f3f9c449..81f85f4de9 100644 --- a/snapshots/session/subagent-spawn-in-process/session.v2.jsonl +++ b/snapshots/session/subagent-spawn-in-process/session.v2.jsonl @@ -11,12 +11,11 @@ {"type":"session/title","data":{"title":"Use the subagent tool exactly","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to:\n1. Use the subagent tool exactly once to delegate the task: \"Reply with exactly the word CHILD_OK and nothing else.\"\n2. After the subagent returns, reply with the single word PARENT_DONE and stop.\n3. Do not use the bash tool.\n\nLet me do this."},{"type":"tool-call","id":"call_00_gVbLWC12Qu8JheZpVRRz8749","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":2907,"outputTokens":142,"cacheReadTokens":0,"reasoningTokens":67},"stream":[{"type":"chunk","time":1788500553301,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788500553301,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to",":\n","1","."," Use"," the"," sub","agent"," tool"," exactly"," once"," to"," delegate"," the"," task",":"," \"","Reply"," with"," exactly"," the"," word"," CH","ILD","_OK"," and"," nothing"," else",".\"\n","2","."," After"," the"," sub","agent"," returns",","," reply"," with"," the"," single"," word"," PAR","ENT","_D","ONE"," and"," stop",".\n","3","."," Do"," not"," use"," the"," bash"," tool",".\n\n","Let"," me"," do"," this","."]},{"type":"chunk","time":1788500553302,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500553302,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_gVbLWC12Qu8JheZpVRRz8749","name":"subagent","args":["","{","\"","description","\"",": ","\"","Reply"," with"," CH","ILD","_OK","\"",", ","\"","prom","pt","\"",": ","\"","Reply"," with"," exactly"," the"," word"," CH","ILD","_OK"," and"," nothing"," else",".","\"",", \"run_in_background\":false}"]},{"type":"chunk","time":1788500553302,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to:\n1. Use the subagent tool exactly once to delegate the task: \"Reply with exactly the word CHILD_OK and nothing else.\"\n2. After the subagent returns, reply with the single word PARENT_DONE and stop.\n3. Do not use the bash tool.\n\nLet me do this."}}},{"type":"chunk","time":1788500553302,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_gVbLWC12Qu8JheZpVRRz8749","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788500553302,"chunk":{"type":"usage","usage":{"inputTokens":2907,"outputTokens":142,"cacheReadTokens":0,"reasoningTokens":67}}},{"type":"chunk","time":1788500553302,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to:\n1. Use the subagent tool exactly once to delegate the task: \"Reply with exactly the word CHILD_OK and nothing else.\"\n2. After the subagent returns, reply with the single word PARENT_DONE and stop.\n3. Do not use the bash tool.\n\nLet me do this."},{"type":"tool-call","id":"call_00_gVbLWC12Qu8JheZpVRRz8749","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":2907,"outputTokens":142,"cacheReadTokens":0,"reasoningTokens":67},"stream":[{"type":"chunk","time":1788233528227,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788233528227,"index":0,"dt":[0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to",":\n","1","."," Use"," the"," sub","agent"," tool"," exactly"," once"," to"," delegate"," the"," task",":"," \"","Reply"," with"," exactly"," the"," word"," CH","ILD","_OK"," and"," nothing"," else",".\"\n","2","."," After"," the"," sub","agent"," returns",","," reply"," with"," the"," single"," word"," PAR","ENT","_D","ONE"," and"," stop",".\n","3","."," Do"," not"," use"," the"," bash"," tool",".\n\n","Let"," me"," do"," this","."]},{"type":"chunk","time":1788233528228,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233528228,"index":1,"dt":[0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_gVbLWC12Qu8JheZpVRRz8749","name":"subagent","args":["","{","\"","description","\"",": ","\"","Reply"," with"," CH","ILD","_OK","\"",", ","\"","prom","pt","\"",": ","\"","Reply"," with"," exactly"," the"," word"," CH","ILD","_OK"," and"," nothing"," else",".","\"",", \"run_in_background\":false}"]},{"type":"chunk","time":1788233528229,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to:\n1. Use the subagent tool exactly once to delegate the task: \"Reply with exactly the word CHILD_OK and nothing else.\"\n2. After the subagent returns, reply with the single word PARENT_DONE and stop.\n3. Do not use the bash tool.\n\nLet me do this."}}},{"type":"chunk","time":1788233528229,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_gVbLWC12Qu8JheZpVRRz8749","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788233528229,"chunk":{"type":"usage","usage":{"inputTokens":2907,"outputTokens":142,"cacheReadTokens":0,"reasoningTokens":67}}},{"type":"chunk","time":1788233528229,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_00_gVbLWC12Qu8JheZpVRRz8749","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\":false}"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500553308,"mode":"one-shot","label":"Reply with CHILD_OK"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_gVbLWC12Qu8JheZpVRRz8749"},"content":[{"type":"tool-result","toolCallId":"call_00_gVbLWC12Qu8JheZpVRRz8749","content":[{"type":"text","text":"CHILD_OK"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The subagent returned \"CHILD_OK\" as expected. Now I need to reply with the single word \"PARENT_DONE\" and stop."},{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":120,"outputTokens":35,"cacheReadTokens":2944,"reasoningTokens":30},"stream":[{"type":"chunk","time":1788500553369,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788500553369,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," sub","agent"," returned"," \"","CH","ILD","_OK","\""," as"," expected","."," Now"," I"," need"," to"," reply"," with"," the"," single"," word"," \"","PAR","ENT","_D","ONE","\""," and"," stop","."]},{"type":"chunk","time":1788500553369,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788500553369,"index":1,"dt":[0,0,0],"texts":["PAR","ENT","_D","ONE"]},{"type":"chunk","time":1788500553369,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The subagent returned \"CHILD_OK\" as expected. Now I need to reply with the single word \"PARENT_DONE\" and stop."}}},{"type":"chunk","time":1788500553369,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788500553369,"chunk":{"type":"usage","usage":{"inputTokens":120,"outputTokens":35,"cacheReadTokens":2944,"reasoningTokens":30}}},{"type":"chunk","time":1788500553369,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The subagent returned \"CHILD_OK\" as expected. Now I need to reply with the single word \"PARENT_DONE\" and stop."},{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":120,"outputTokens":35,"cacheReadTokens":2944,"reasoningTokens":30},"stream":[{"type":"chunk","time":1788233528286,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788233528286,"index":0,"dt":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," sub","agent"," returned"," \"","CH","ILD","_OK","\""," as"," expected","."," Now"," I"," need"," to"," reply"," with"," the"," single"," word"," \"","PAR","ENT","_D","ONE","\""," and"," stop","."]},{"type":"chunk","time":1788233528287,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788233528287,"index":1,"dt":[0,0,0],"texts":["PAR","ENT","_D","ONE"]},{"type":"chunk","time":1788233528287,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The subagent returned \"CHILD_OK\" as expected. Now I need to reply with the single word \"PARENT_DONE\" and stop."}}},{"type":"chunk","time":1788233528287,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788233528287,"chunk":{"type":"usage","usage":{"inputTokens":120,"outputTokens":35,"cacheReadTokens":2944,"reasoningTokens":30}}},{"type":"chunk","time":1788233528287,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/subagent-spawn-in-process/session.v3.jsonl b/snapshots/session/subagent-spawn-in-process/session.v3.jsonl index 3548ae55d6..fe1cf0ce58 100644 --- a/snapshots/session/subagent-spawn-in-process/session.v3.jsonl +++ b/snapshots/session/subagent-spawn-in-process/session.v3.jsonl @@ -12,11 +12,12 @@ {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Use the subagent tool exactly","messageSeqs":[8],"source":{"kind":"fallback"}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to:\n1. Use the subagent tool exactly once to delegate the task: \"Reply with exactly the word CHILD_OK and nothing else.\"\n2. After the subagent returns, reply with the single word PARENT_DONE and stop.\n3. Do not use the bash tool.\n\nLet me do this."},{"type":"tool-call","id":"call_00_gVbLWC12Qu8JheZpVRRz8749","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":2907,"outputTokens":142,"cacheReadTokens":0,"reasoningTokens":67},"stream":[{"type":"chunk","time":1788699442981,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788699442981,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to",":\n","1","."," Use"," the"," sub","agent"," tool"," exactly"," once"," to"," delegate"," the"," task",":"," \"","Reply"," with"," exactly"," the"," word"," CH","ILD","_OK"," and"," nothing"," else",".\"\n","2","."," After"," the"," sub","agent"," returns",","," reply"," with"," the"," single"," word"," PAR","ENT","_D","ONE"," and"," stop",".\n","3","."," Do"," not"," use"," the"," bash"," tool",".\n\n","Let"," me"," do"," this","."]},{"type":"chunk","time":1788699442982,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699442982,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_gVbLWC12Qu8JheZpVRRz8749","name":"subagent","args":["","{","\"","description","\"",": ","\"","Reply"," with"," CH","ILD","_OK","\"",", ","\"","prom","pt","\"",": ","\"","Reply"," with"," exactly"," the"," word"," CH","ILD","_OK"," and"," nothing"," else",".","\"",", \"run_in_background\":false}"]},{"type":"chunk","time":1788699442982,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to:\n1. Use the subagent tool exactly once to delegate the task: \"Reply with exactly the word CHILD_OK and nothing else.\"\n2. After the subagent returns, reply with the single word PARENT_DONE and stop.\n3. Do not use the bash tool.\n\nLet me do this."}}},{"type":"chunk","time":1788699442982,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_gVbLWC12Qu8JheZpVRRz8749","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788699442982,"chunk":{"type":"usage","usage":{"inputTokens":2907,"outputTokens":142,"cacheReadTokens":0,"reasoningTokens":67}}},{"type":"chunk","time":1788699442982,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to:\n1. Use the subagent tool exactly once to delegate the task: \"Reply with exactly the word CHILD_OK and nothing else.\"\n2. After the subagent returns, reply with the single word PARENT_DONE and stop.\n3. Do not use the bash tool.\n\nLet me do this."},{"type":"tool-call","id":"call_00_gVbLWC12Qu8JheZpVRRz8749","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":2907,"outputTokens":142,"cacheReadTokens":0,"reasoningTokens":67},"stream":[{"type":"chunk","time":1788880383250,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788880383251,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to",":\n","1","."," Use"," the"," sub","agent"," tool"," exactly"," once"," to"," delegate"," the"," task",":"," \"","Reply"," with"," exactly"," the"," word"," CH","ILD","_OK"," and"," nothing"," else",".\"\n","2","."," After"," the"," sub","agent"," returns",","," reply"," with"," the"," single"," word"," PAR","ENT","_D","ONE"," and"," stop",".\n","3","."," Do"," not"," use"," the"," bash"," tool",".\n\n","Let"," me"," do"," this","."]},{"type":"chunk","time":1788880383251,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880383251,"index":1,"dt":[0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_gVbLWC12Qu8JheZpVRRz8749","name":"subagent","args":["","{","\"","description","\"",": ","\"","Reply"," with"," CH","ILD","_OK","\"",", ","\"","prom","pt","\"",": ","\"","Reply"," with"," exactly"," the"," word"," CH","ILD","_OK"," and"," nothing"," else",".","\"",", \"run_in_background\":false}"]},{"type":"chunk","time":1788880383252,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to:\n1. Use the subagent tool exactly once to delegate the task: \"Reply with exactly the word CHILD_OK and nothing else.\"\n2. After the subagent returns, reply with the single word PARENT_DONE and stop.\n3. Do not use the bash tool.\n\nLet me do this."}}},{"type":"chunk","time":1788880383252,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_gVbLWC12Qu8JheZpVRRz8749","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\":false}"}}},{"type":"chunk","time":1788880383252,"chunk":{"type":"usage","usage":{"inputTokens":2907,"outputTokens":142,"cacheReadTokens":0,"reasoningTokens":67}}},{"type":"chunk","time":1788880383252,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_00_gVbLWC12Qu8JheZpVRRz8749","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\":false}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788880383258,"mode":"one-shot","label":"Reply with CHILD_OK"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_gVbLWC12Qu8JheZpVRRz8749"},"content":[{"type":"tool-result","toolCallId":"call_00_gVbLWC12Qu8JheZpVRRz8749","content":[{"type":"text","text":"CHILD_OK"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The subagent returned \"CHILD_OK\" as expected. Now I need to reply with the single word \"PARENT_DONE\" and stop."},{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":120,"outputTokens":35,"cacheReadTokens":2944,"reasoningTokens":30},"stream":[{"type":"chunk","time":1788699443047,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788699443047,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," sub","agent"," returned"," \"","CH","ILD","_OK","\""," as"," expected","."," Now"," I"," need"," to"," reply"," with"," the"," single"," word"," \"","PAR","ENT","_D","ONE","\""," and"," stop","."]},{"type":"chunk","time":1788699443047,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788699443047,"index":1,"dt":[0,0,0],"texts":["PAR","ENT","_D","ONE"]},{"type":"chunk","time":1788699443047,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The subagent returned \"CHILD_OK\" as expected. Now I need to reply with the single word \"PARENT_DONE\" and stop."}}},{"type":"chunk","time":1788699443047,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788699443047,"chunk":{"type":"usage","usage":{"inputTokens":120,"outputTokens":35,"cacheReadTokens":2944,"reasoningTokens":30}}},{"type":"chunk","time":1788699443047,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The subagent returned \"CHILD_OK\" as expected. Now I need to reply with the single word \"PARENT_DONE\" and stop."},{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":120,"outputTokens":35,"cacheReadTokens":2944,"reasoningTokens":30},"stream":[{"type":"chunk","time":1788880383320,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788880383320,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," sub","agent"," returned"," \"","CH","ILD","_OK","\""," as"," expected","."," Now"," I"," need"," to"," reply"," with"," the"," single"," word"," \"","PAR","ENT","_D","ONE","\""," and"," stop","."]},{"type":"chunk","time":1788880383320,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788880383320,"index":1,"dt":[0,0,0],"texts":["PAR","ENT","_D","ONE"]},{"type":"chunk","time":1788880383320,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The subagent returned \"CHILD_OK\" as expected. Now I need to reply with the single word \"PARENT_DONE\" and stop."}}},{"type":"chunk","time":1788880383320,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"PARENT_DONE"}}},{"type":"chunk","time":1788880383320,"chunk":{"type":"usage","usage":{"inputTokens":120,"outputTokens":35,"cacheReadTokens":2944,"reasoningTokens":30}}},{"type":"chunk","time":1788880383320,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/workflow-run/session.v2.jsonl b/snapshots/session/workflow-run/session.v2.jsonl index 46bd43686a..f9285f97f6 100644 --- a/snapshots/session/workflow-run/session.v2.jsonl +++ b/snapshots/session/workflow-run/session.v2.jsonl @@ -11,16 +11,15 @@ {"type":"session/title","data":{"title":"Use the workflow tool exactly","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to use the workflow tool exactly once with specific parameters. Let me carefully follow the instructions:\n\n1. args omitted (so I don't include it)\n2. meta = { \"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\" }\n3. script = as given verbatim\n4. After it returns, reply with \"WORKFLOW_DONE\"\n\nLet me do exactly that."},{"type":"tool-call","id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","arguments":"{\"meta\": {\"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\"}, \"script\": \"\\nphase('Run')\\nconst reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.')\\nreturn { reply }\\n\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":3174,"outputTokens":191,"cacheReadTokens":0,"reasoningTokens":88},"stream":[{"type":"chunk","time":1788500554310,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788500554310,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," use"," the"," workflow"," tool"," exactly"," once"," with"," specific"," parameters","."," Let"," me"," carefully"," follow"," the"," instructions",":\n\n","1","."," args"," omitted"," (","so"," I"," don","'t"," include"," it",")\n","2","."," meta"," ="," {"," \"","name","\":"," \"","sn","apshot","-flow","\","," \"","description","\":"," \"","one"," child"," for"," the"," snapshot","\""," }\n","3","."," script"," ="," as"," given"," verb","atim","\n","4","."," After"," it"," returns",","," reply"," with"," \"","WORK","FL","OW","_D","ONE","\"\n\n","Let"," me"," do"," exactly"," that","."]},{"type":"chunk","time":1788500554311,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788500554311,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","args":["","{","\"","meta","\"",": ","{\"","name","\":"," \"","sn","apshot","-flow","\","," \"","description","\":"," \"","one"," child"," for"," the"," snapshot","\"","}",", ","\"","script","\"",": ","\"","\\n","phase","('","Run","')\\n","const"," reply"," ="," await"," agent","('","Reply"," with"," exactly"," the"," word"," WF","_CH","ILD","_OK"," and"," nothing"," else",".')\\n","return"," {"," reply"," }\\n","\"","}"]},{"type":"chunk","time":1788500554311,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to use the workflow tool exactly once with specific parameters. Let me carefully follow the instructions:\n\n1. args omitted (so I don't include it)\n2. meta = { \"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\" }\n3. script = as given verbatim\n4. After it returns, reply with \"WORKFLOW_DONE\"\n\nLet me do exactly that."}}},{"type":"chunk","time":1788500554311,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","arguments":"{\"meta\": {\"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\"}, \"script\": \"\\nphase('Run')\\nconst reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.')\\nreturn { reply }\\n\"}"}}},{"type":"chunk","time":1788500554311,"chunk":{"type":"usage","usage":{"inputTokens":3174,"outputTokens":191,"cacheReadTokens":0,"reasoningTokens":88}}},{"type":"chunk","time":1788500554311,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to use the workflow tool exactly once with specific parameters. Let me carefully follow the instructions:\n\n1. args omitted (so I don't include it)\n2. meta = { \"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\" }\n3. script = as given verbatim\n4. After it returns, reply with \"WORKFLOW_DONE\"\n\nLet me do exactly that."},{"type":"tool-call","id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","arguments":"{\"meta\": {\"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\"}, \"script\": \"\\nphase('Run')\\nconst reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.')\\nreturn { reply }\\n\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":3174,"outputTokens":191,"cacheReadTokens":0,"reasoningTokens":88},"stream":[{"type":"chunk","time":1788233532122,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788233532122,"index":0,"dt":[0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," use"," the"," workflow"," tool"," exactly"," once"," with"," specific"," parameters","."," Let"," me"," carefully"," follow"," the"," instructions",":\n\n","1","."," args"," omitted"," (","so"," I"," don","'t"," include"," it",")\n","2","."," meta"," ="," {"," \"","name","\":"," \"","sn","apshot","-flow","\","," \"","description","\":"," \"","one"," child"," for"," the"," snapshot","\""," }\n","3","."," script"," ="," as"," given"," verb","atim","\n","4","."," After"," it"," returns",","," reply"," with"," \"","WORK","FL","OW","_D","ONE","\"\n\n","Let"," me"," do"," exactly"," that","."]},{"type":"chunk","time":1788233532123,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233532123,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","args":["","{","\"","meta","\"",": ","{\"","name","\":"," \"","sn","apshot","-flow","\","," \"","description","\":"," \"","one"," child"," for"," the"," snapshot","\"","}",", ","\"","script","\"",": ","\"","\\n","phase","('","Run","')\\n","const"," reply"," ="," await"," agent","('","Reply"," with"," exactly"," the"," word"," WF","_CH","ILD","_OK"," and"," nothing"," else",".')\\n","return"," {"," reply"," }\\n","\"","}"]},{"type":"chunk","time":1788233532123,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to use the workflow tool exactly once with specific parameters. Let me carefully follow the instructions:\n\n1. args omitted (so I don't include it)\n2. meta = { \"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\" }\n3. script = as given verbatim\n4. After it returns, reply with \"WORKFLOW_DONE\"\n\nLet me do exactly that."}}},{"type":"chunk","time":1788233532124,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","arguments":"{\"meta\": {\"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\"}, \"script\": \"\\nphase('Run')\\nconst reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.')\\nreturn { reply }\\n\"}"}}},{"type":"chunk","time":1788233532124,"chunk":{"type":"usage","usage":{"inputTokens":3174,"outputTokens":191,"cacheReadTokens":0,"reasoningTokens":88}}},{"type":"chunk","time":1788233532124,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","arguments":"{\"meta\": {\"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\"}, \"script\": \"\\nphase('Run')\\nconst reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.')\\nreturn { reply }\\n\"}"}} {"type":"tool-workflow/run-start","data":{"runId":"{{workflow:1}}","name":"snapshot-flow"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788500554400,"mode":"one-shot"}} {"type":"tool-workflow/agent-start","data":{"runId":"{{workflow:1}}","seq":1,"label":"Reply with exactly the word WF_CHILD_OK and not…","phase":"Run","childId":"{{session:2}}"}} {"type":"tool-workflow/agent-end","data":{"runId":"{{workflow:1}}","seq":1,"outcome":"completed"}} {"type":"tool-workflow/run-end","data":{"runId":"{{workflow:1}}","stopReason":"completed"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_dD2BLuNeJCTh2iiYC1QR3449"},"content":[{"type":"tool-result","toolCallId":"call_00_dD2BLuNeJCTh2iiYC1QR3449","content":[{"type":"text","text":"workflow \"snapshot-flow\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WF_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The workflow returned successfully with the reply \"WF_CHILD_OK\". Now I need to reply with exactly \"WORKFLOW_DONE\" and stop."},{"type":"text","text":"WORKFLOW_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":328,"outputTokens":36,"cacheReadTokens":3072,"reasoningTokens":30},"stream":[{"type":"chunk","time":1788500554460,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788500554460,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," workflow"," returned"," successfully"," with"," the"," reply"," \"","WF","_CH","ILD","_OK","\"."," Now"," I"," need"," to"," reply"," with"," exactly"," \"","WORK","FL","OW","_D","ONE","\""," and"," stop","."]},{"type":"chunk","time":1788500554460,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788500554460,"index":1,"dt":[0,0,0,0],"texts":["WORK","FL","OW","_D","ONE"]},{"type":"chunk","time":1788500554460,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The workflow returned successfully with the reply \"WF_CHILD_OK\". Now I need to reply with exactly \"WORKFLOW_DONE\" and stop."}}},{"type":"chunk","time":1788500554460,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"WORKFLOW_DONE"}}},{"type":"chunk","time":1788500554460,"chunk":{"type":"usage","usage":{"inputTokens":328,"outputTokens":36,"cacheReadTokens":3072,"reasoningTokens":30}}},{"type":"chunk","time":1788500554460,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The workflow returned successfully with the reply \"WF_CHILD_OK\". Now I need to reply with exactly \"WORKFLOW_DONE\" and stop."},{"type":"text","text":"WORKFLOW_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":328,"outputTokens":36,"cacheReadTokens":3072,"reasoningTokens":30},"stream":[{"type":"chunk","time":1788233532207,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788233532207,"index":0,"dt":[0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," workflow"," returned"," successfully"," with"," the"," reply"," \"","WF","_CH","ILD","_OK","\"."," Now"," I"," need"," to"," reply"," with"," exactly"," \"","WORK","FL","OW","_D","ONE","\""," and"," stop","."]},{"type":"chunk","time":1788233532208,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788233532208,"index":1,"dt":[0,0,0,0],"texts":["WORK","FL","OW","_D","ONE"]},{"type":"chunk","time":1788233532208,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The workflow returned successfully with the reply \"WF_CHILD_OK\". Now I need to reply with exactly \"WORKFLOW_DONE\" and stop."}}},{"type":"chunk","time":1788233532208,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"WORKFLOW_DONE"}}},{"type":"chunk","time":1788233532208,"chunk":{"type":"usage","usage":{"inputTokens":328,"outputTokens":36,"cacheReadTokens":3072,"reasoningTokens":30}}},{"type":"chunk","time":1788233532208,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/workflow-run/session.v3.jsonl b/snapshots/session/workflow-run/session.v3.jsonl index 5292c8d096..f847d3e588 100644 --- a/snapshots/session/workflow-run/session.v3.jsonl +++ b/snapshots/session/workflow-run/session.v3.jsonl @@ -12,15 +12,16 @@ {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Use the workflow tool exactly","messageSeqs":[8],"source":{"kind":"fallback"}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to use the workflow tool exactly once with specific parameters. Let me carefully follow the instructions:\n\n1. args omitted (so I don't include it)\n2. meta = { \"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\" }\n3. script = as given verbatim\n4. After it returns, reply with \"WORKFLOW_DONE\"\n\nLet me do exactly that."},{"type":"tool-call","id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","arguments":"{\"meta\": {\"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\"}, \"script\": \"\\nphase('Run')\\nconst reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.')\\nreturn { reply }\\n\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":3174,"outputTokens":191,"cacheReadTokens":0,"reasoningTokens":88},"stream":[{"type":"chunk","time":1788699451783,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788699451783,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," use"," the"," workflow"," tool"," exactly"," once"," with"," specific"," parameters","."," Let"," me"," carefully"," follow"," the"," instructions",":\n\n","1","."," args"," omitted"," (","so"," I"," don","'t"," include"," it",")\n","2","."," meta"," ="," {"," \"","name","\":"," \"","sn","apshot","-flow","\","," \"","description","\":"," \"","one"," child"," for"," the"," snapshot","\""," }\n","3","."," script"," ="," as"," given"," verb","atim","\n","4","."," After"," it"," returns",","," reply"," with"," \"","WORK","FL","OW","_D","ONE","\"\n\n","Let"," me"," do"," exactly"," that","."]},{"type":"chunk","time":1788699451784,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699451784,"index":1,"dt":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","args":["","{","\"","meta","\"",": ","{\"","name","\":"," \"","sn","apshot","-flow","\","," \"","description","\":"," \"","one"," child"," for"," the"," snapshot","\"","}",", ","\"","script","\"",": ","\"","\\n","phase","('","Run","')\\n","const"," reply"," ="," await"," agent","('","Reply"," with"," exactly"," the"," word"," WF","_CH","ILD","_OK"," and"," nothing"," else",".')\\n","return"," {"," reply"," }\\n","\"","}"]},{"type":"chunk","time":1788699451785,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to use the workflow tool exactly once with specific parameters. Let me carefully follow the instructions:\n\n1. args omitted (so I don't include it)\n2. meta = { \"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\" }\n3. script = as given verbatim\n4. After it returns, reply with \"WORKFLOW_DONE\"\n\nLet me do exactly that."}}},{"type":"chunk","time":1788699451785,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","arguments":"{\"meta\": {\"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\"}, \"script\": \"\\nphase('Run')\\nconst reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.')\\nreturn { reply }\\n\"}"}}},{"type":"chunk","time":1788699451785,"chunk":{"type":"usage","usage":{"inputTokens":3174,"outputTokens":191,"cacheReadTokens":0,"reasoningTokens":88}}},{"type":"chunk","time":1788699451785,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to use the workflow tool exactly once with specific parameters. Let me carefully follow the instructions:\n\n1. args omitted (so I don't include it)\n2. meta = { \"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\" }\n3. script = as given verbatim\n4. After it returns, reply with \"WORKFLOW_DONE\"\n\nLet me do exactly that."},{"type":"tool-call","id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","arguments":"{\"meta\": {\"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\"}, \"script\": \"\\nphase('Run')\\nconst reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.')\\nreturn { reply }\\n\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":3174,"outputTokens":191,"cacheReadTokens":0,"reasoningTokens":88},"stream":[{"type":"chunk","time":1788880384222,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788880384223,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," use"," the"," workflow"," tool"," exactly"," once"," with"," specific"," parameters","."," Let"," me"," carefully"," follow"," the"," instructions",":\n\n","1","."," args"," omitted"," (","so"," I"," don","'t"," include"," it",")\n","2","."," meta"," ="," {"," \"","name","\":"," \"","sn","apshot","-flow","\","," \"","description","\":"," \"","one"," child"," for"," the"," snapshot","\""," }\n","3","."," script"," ="," as"," given"," verb","atim","\n","4","."," After"," it"," returns",","," reply"," with"," \"","WORK","FL","OW","_D","ONE","\"\n\n","Let"," me"," do"," exactly"," that","."]},{"type":"chunk","time":1788880384224,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788880384224,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","args":["","{","\"","meta","\"",": ","{\"","name","\":"," \"","sn","apshot","-flow","\","," \"","description","\":"," \"","one"," child"," for"," the"," snapshot","\"","}",", ","\"","script","\"",": ","\"","\\n","phase","('","Run","')\\n","const"," reply"," ="," await"," agent","('","Reply"," with"," exactly"," the"," word"," WF","_CH","ILD","_OK"," and"," nothing"," else",".')\\n","return"," {"," reply"," }\\n","\"","}"]},{"type":"chunk","time":1788880384224,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to use the workflow tool exactly once with specific parameters. Let me carefully follow the instructions:\n\n1. args omitted (so I don't include it)\n2. meta = { \"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\" }\n3. script = as given verbatim\n4. After it returns, reply with \"WORKFLOW_DONE\"\n\nLet me do exactly that."}}},{"type":"chunk","time":1788880384224,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","arguments":"{\"meta\": {\"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\"}, \"script\": \"\\nphase('Run')\\nconst reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.')\\nreturn { reply }\\n\"}"}}},{"type":"chunk","time":1788880384224,"chunk":{"type":"usage","usage":{"inputTokens":3174,"outputTokens":191,"cacheReadTokens":0,"reasoningTokens":88}}},{"type":"chunk","time":1788880384224,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","arguments":"{\"meta\": {\"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\"}, \"script\": \"\\nphase('Run')\\nconst reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.')\\nreturn { reply }\\n\"}"}} {"type":"tool-workflow/run-start","data":{"runId":"{{workflow:1}}","name":"snapshot-flow"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1788880384304,"mode":"one-shot"}} {"type":"tool-workflow/agent-start","data":{"runId":"{{workflow:1}}","seq":1,"label":"Reply with exactly the word WF_CHILD_OK and not…","phase":"Run","childId":"{{session:2}}"}} {"type":"tool-workflow/agent-end","data":{"runId":"{{workflow:1}}","seq":1,"outcome":"completed"}} {"type":"tool-workflow/run-end","data":{"runId":"{{workflow:1}}","stopReason":"completed"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_dD2BLuNeJCTh2iiYC1QR3449"},"content":[{"type":"tool-result","toolCallId":"call_00_dD2BLuNeJCTh2iiYC1QR3449","content":[{"type":"text","text":"workflow \"snapshot-flow\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WF_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The workflow returned successfully with the reply \"WF_CHILD_OK\". Now I need to reply with exactly \"WORKFLOW_DONE\" and stop."},{"type":"text","text":"WORKFLOW_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":328,"outputTokens":36,"cacheReadTokens":3072,"reasoningTokens":30},"stream":[{"type":"chunk","time":1788699451981,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788699451981,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," workflow"," returned"," successfully"," with"," the"," reply"," \"","WF","_CH","ILD","_OK","\"."," Now"," I"," need"," to"," reply"," with"," exactly"," \"","WORK","FL","OW","_D","ONE","\""," and"," stop","."]},{"type":"chunk","time":1788699451981,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788699451981,"index":1,"dt":[0,0,0,0],"texts":["WORK","FL","OW","_D","ONE"]},{"type":"chunk","time":1788699451981,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The workflow returned successfully with the reply \"WF_CHILD_OK\". Now I need to reply with exactly \"WORKFLOW_DONE\" and stop."}}},{"type":"chunk","time":1788699451981,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"WORKFLOW_DONE"}}},{"type":"chunk","time":1788699451981,"chunk":{"type":"usage","usage":{"inputTokens":328,"outputTokens":36,"cacheReadTokens":3072,"reasoningTokens":30}}},{"type":"chunk","time":1788699451981,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The workflow returned successfully with the reply \"WF_CHILD_OK\". Now I need to reply with exactly \"WORKFLOW_DONE\" and stop."},{"type":"text","text":"WORKFLOW_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":328,"outputTokens":36,"cacheReadTokens":3072,"reasoningTokens":30},"stream":[{"type":"chunk","time":1788880384369,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788880384369,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," workflow"," returned"," successfully"," with"," the"," reply"," \"","WF","_CH","ILD","_OK","\"."," Now"," I"," need"," to"," reply"," with"," exactly"," \"","WORK","FL","OW","_D","ONE","\""," and"," stop","."]},{"type":"chunk","time":1788880384369,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788880384369,"index":1,"dt":[0,0,0,0],"texts":["WORK","FL","OW","_D","ONE"]},{"type":"chunk","time":1788880384369,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The workflow returned successfully with the reply \"WF_CHILD_OK\". Now I need to reply with exactly \"WORKFLOW_DONE\" and stop."}}},{"type":"chunk","time":1788880384369,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"WORKFLOW_DONE"}}},{"type":"chunk","time":1788880384369,"chunk":{"type":"usage","usage":{"inputTokens":328,"outputTokens":36,"cacheReadTokens":3072,"reasoningTokens":30}}},{"type":"chunk","time":1788880384369,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} From 5ab91467e25d539b244f407718ef878b23585453 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Tue, 8 Sep 2026 23:26:56 +0800 Subject: [PATCH 30/58] test(cli): include catalog facts in profile expectations --- .../parent.expected.jsonl | 1 + .../stream-json.expected.jsonl | 25 ++++++++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/parent.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/parent.expected.jsonl index 7b41f6017e..44d31a36ab 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/parent.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/parent.expected.jsonl @@ -22,6 +22,7 @@ {"type":"session/title","data":{"title":"Tighten this session to read-only.","messageSeqs":[3],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"delegate-write","name":"subagent","arguments":"{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"delegate-write","name":"subagent","args":["{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"delegate-write","name":"subagent","arguments":"{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":2,"step":1,"callId":"delegate-write","name":"subagent","arguments":"{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{sessionId}}","childCreatedAt":0,"mode":"one-shot","label":"Delegated write probe"}} {"type":"tool/result","data":{"turn":2,"step":1,"message":{"source":{"kind":"tool","callId":"delegate-write"},"content":[{"type":"tool-result","toolCallId":"delegate-write","content":[{"type":"text","text":"CHILD_DENIED [sandbox: file access denied under read-only mode]"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[22],"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"step/start","data":{"turn":2,"step":2}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl index c2d9145aaf..b027378207 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl @@ -12,16 +12,17 @@ {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":14,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"start-child","name":"subagent","args":["{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":15,"time":0,"data":{"turn":1,"step":1,"callId":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":16,"time":0,"data":{"title":"Subagent settlement","messageSeqs":[8],"source":{"kind":"provider","provider":"session-title-first-prompt-llm","model":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":17,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"start-child"},"content":[{"type":"tool-result","toolCallId":"start-child","content":[{"type":"text","text":"started subagent {{sessionId}}"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":18,"time":0,"data":{"turn":1,"step":1}}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":19,"time":0,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{sessionId}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_RESULT"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} finished and will do no further work unless you send it more.","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"}]}}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":20,"time":0,"data":{"turn":1,"step":2}}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":21,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["STARTED"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"STARTED"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":22,"time":0,"data":{"turn":1,"step":2}}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":23,"time":0,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":24,"time":0,"data":{"turn":1,"step":3}}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":25,"time":0,"data":{"content":[{"type":"text","text":"Background subagent {{sessionId}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_RESULT"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} finished and will do no further work unless you send it more.","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":26,"time":0,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_RECEIVED_CHILD_RESULT"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["PARENT_RECEIVED_CHILD_RESULT"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_RECEIVED_CHILD_RESULT"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":27,"time":0,"data":{"turn":1,"step":3}}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":28,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"subagent/catalog","seq":17,"time":0,"data":{"version":0,"childId":"{{sessionId}}","childCreatedAt":0,"mode":"continuable","label":"Return child result"}}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":18,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"start-child"},"content":[{"type":"tool-result","toolCallId":"start-child","content":[{"type":"text","text":"started subagent {{sessionId}}"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":19,"time":0,"data":{"turn":1,"step":1}}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":20,"time":0,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{sessionId}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_RESULT"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} finished and will do no further work unless you send it more.","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"}]}}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":21,"time":0,"data":{"turn":1,"step":2}}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":22,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["STARTED"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"STARTED"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":23,"time":0,"data":{"turn":1,"step":2}}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":24,"time":0,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":25,"time":0,"data":{"turn":1,"step":3}}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":26,"time":0,"data":{"content":[{"type":"text","text":"Background subagent {{sessionId}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_RESULT"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} finished and will do no further work unless you send it more.","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":27,"time":0,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_RECEIVED_CHILD_RESULT"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["PARENT_RECEIVED_CHILD_RESULT"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_RECEIVED_CHILD_RESULT"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":28,"time":0,"data":{"turn":1,"step":3}}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":29,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}} {"type":"result","sessionId":"{{sessionId}}","output":"PARENT_RECEIVED_CHILD_RESULT","usage":{"inputTokens":30,"outputTokens":15}} From 3c05dcb537328792cc1cfd6bd2b62a73c3682251 Mon Sep 17 00:00:00 2001 From: fz Date: Tue, 8 Sep 2026 23:29:03 +0800 Subject: [PATCH 31/58] fix(ci): refresh persistent bash fixtures and synchronize LSP lifecycle test --- ...09-03-minimal-profiles-persistent-shell-only.i18n.yaml | 4 ++-- .../2026-09-03-minimal-profiles-persistent-shell-only.md | 4 +++- ...026-09-03-minimal-profiles-persistent-shell-only.zh.md | 4 +++- apps/web/tests/minimal-preset.snapshot.ts | 5 +++-- docs/config-catalog.i18n.yaml | 4 ++-- docs/config-catalog.md | 2 +- docs/config-catalog.zh.md | 2 +- packages/lsp/lsp-stdio/tests/lifecycle.spec.ts | 8 ++++++-- .../sdk/persistent-tools/notifications.expected.jsonl | 6 +++--- snapshots/sdk/persistent-tools/session.v3.jsonl | 6 +++--- snapshots/web/minimal-preset/session.v3.jsonl | 2 +- snapshots/web/minimal-preset/ui.expected.md | 2 +- 12 files changed, 29 insertions(+), 20 deletions(-) diff --git a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml index 3470a11031..b85c97a4b8 100644 --- a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml +++ b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md -2026-09-03-minimal-profiles-persistent-shell-only.md: 1e49006b07645c34f273abf7f707fa0d79cfa14e -2026-09-03-minimal-profiles-persistent-shell-only.zh.md: bd23b1d89665a8093550e6991cd4940cea79a90d +2026-09-03-minimal-profiles-persistent-shell-only.md: d58210cb890e4264b02d591f213045b592ca6bfc +2026-09-03-minimal-profiles-persistent-shell-only.zh.md: 481ef479c2dc01ae503f621df976529aeb6bdbe0 diff --git a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md index 1e49006b07..d58210cb89 100644 --- a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md +++ b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md @@ -16,6 +16,8 @@ The shipped minimal compositions expose exactly one platform-selected persistent The standalone editor package remains available for explicit custom compositions. A trusted user-authored preset or higher profile patch must insert the editor into the Cordis tree with a filesystem provider in the same service scope; the shipped `minimal` and `sdk-minimal` defaults never insert it. The [Python SDK guide](../../../../docs/user/guide/python-sdk.md#opt-in-to-str_replace_editor) provides an executable patch example. +The shared [persistent Bash consumer](../../../../packages/shell/tool-bash-persistent/README.md#model-experience) uses the one-shot shell's command-status wording while retaining its persistent state. Settled commands append `[Command finished with exit code N]`, including success; timeout output includes `[Command timed out or OOM]` and the shell-reset notice. Trailing newlines are removed before the status trailer. Both minimal Bash descriptions state that network access depends on the task environment. Explicit compositions using this consumer share its output behavior; the persistent PowerShell description and output remain unchanged. + Exact composition tests assert the single tool and the absence of a preset-local filesystem service. The `sdk-minimal` bundle test and built config dump assert that its row and dependency allowlists contain neither `fs-local` nor `dsh-tool-str-replace-editor`. Web and packaged-Python model-visible snapshots pin the one-tool schema roster. SDK profile smoke tests execute the guide's editor patch and verify file creation and viewing. This decision partially supersedes the tool selection in [the bare minimal runtime](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md) and the minimal exception in [the base editor decision](2026-09-05-base-default-file-editor.md). Those notes retain authority for prompt ownership, no-compaction behavior, and base-backed file editing. [The application architecture](../../../../docs/architecture.md) owns profile launch and bundle layering. @@ -30,4 +32,4 @@ This decision partially supersedes the tool selection in [the bare minimal runti ## Consequences -Minimal agents inspect and modify files through their persistent shell. Their model requests carry one tool schema, and their compositions own no filesystem service. Full profiles, the editor package, and explicit custom compositions keep their existing behavior. +Minimal agents inspect and modify files through their persistent shell. Their model requests carry one tool schema, and their compositions own no filesystem service. The editor package and explicit editor compositions remain available. Web and SDK replay fixtures pin the persistent Bash status trailers alongside shell state and file effects. diff --git a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md index bd23b1d896..481ef479c2 100644 --- a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md +++ b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md @@ -16,6 +16,8 @@ Status: implemented 独立 editor 包仍可用于显式自定义组合。受信任的用户自定义 preset 或更高优先级的 profile patch 必须将 editor 插入 Cordis tree,并在同一服务作用域内提供文件系统后端;随附的 `minimal` 与 `sdk-minimal` 默认组合不会插入它。[Python SDK 指南](../../../../docs/user/guide/python-sdk.zh.md#opt-in-to-str_replace_editor)提供可执行的 patch 示例。 +共享的[持久 Bash 消费方](../../../../packages/shell/tool-bash-persistent/README.zh.md#model-experience)保留跨调用状态,并采用单次 shell 的命令状态文案。完成的命令追加 `[Command finished with exit code N]`,成功时也包含该标记;超时输出包含 `[Command timed out or OOM]` 和 shell 重置说明。追加状态标记前会移除输出末尾的全部换行。两份极简 Bash 描述都说明网络访问取决于任务环境。使用该消费方的显式组合共享相同的输出行为;持久 PowerShell 的描述与输出保持不变。 + 精确组合测试会断言单工具清单以及 preset 内不存在文件系统服务。`sdk-minimal` bundle 测试与构建后配置转储会断言配置项和依赖 allowlist 都不含 `fs-local` 或 `dsh-tool-str-replace-editor`。Web 与打包 Python 的模型可见快照会固定单工具 schema 清单。SDK profile 冒烟测试会执行指南中的 editor patch,并验证文件创建和查看。 本决策部分取代[极简裸运行时](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md)中的工具选择,以及[base 编辑器决策](2026-09-05-base-default-file-editor.zh.md)中的极简例外。这些 Agent Note 继续负责提示词所有权、无 compaction 行为和基于 base 的文件编辑。[应用架构](../../../../docs/architecture.zh.md)负责 profile 启动与 bundle 分层。 @@ -30,4 +32,4 @@ Status: implemented ## 后果 -极简 agent 通过持久 shell 检查和修改文件。模型请求只携带一个工具 schema,组合不拥有文件系统服务。完整 profile、editor 包与显式自定义组合保持现有行为。 +极简 agent 通过持久 shell 检查和修改文件。模型请求只携带一个工具 schema,组合不拥有文件系统服务。editor 包和显式 editor 组合仍然可用。Web 与 SDK 回放快照会同时固定持久 Bash 的状态标记、shell 状态和文件操作结果。 diff --git a/apps/web/tests/minimal-preset.snapshot.ts b/apps/web/tests/minimal-preset.snapshot.ts index 11707cfaf1..22ab1bc476 100644 --- a/apps/web/tests/minimal-preset.snapshot.ts +++ b/apps/web/tests/minimal-preset.snapshot.ts @@ -118,7 +118,8 @@ describe('minimal agent preset', () => { bash: text(bash), }).toMatchInlineSnapshot(` { - "bash": "PERSISTED:{{cwd}}/persistent-state", + "bash": "PERSISTED:{{cwd}}/persistent-state + [Command finished with exit code 0]", "goalCommand": false, "prompt": "You are a helpful software engineer assistant.", "tools": [ @@ -161,7 +162,7 @@ describe('minimal agent preset', () => { const call = row.locator('xpath=..') await call.getByText('IN', { exact: true }).waitFor() await call.getByText('OUT', { exact: true }).waitFor() - await call.getByText('MINIMAL_BASH_CARD_OK', { exact: true }).waitFor() + await call.getByText('MINIMAL_BASH_CARD_OK\n[Command finished with exit code 0]', { exact: true }).waitFor() await call.getByText(/"command": "printf 'MINIMAL_BASH_CARD_OK/).waitFor() const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd) diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 3957eee85a..23b1b9016f 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-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/config-catalog.md -config-catalog.md: 9099ca894f0cab6fb8030c5ffced74cf7547df77 -config-catalog.zh.md: a9d7502de051857bb947361c1808a48ebcedb2ee +config-catalog.md: 900d8d65632322170e806ea1190072531853260c +config-catalog.zh.md: 07693911ae6b1cf20608d774c55a578874471ca3 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 9099ca894f..900d8d6563 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -2740,7 +2740,7 @@ export interface Config { } ``` -Source: [`packages/shell/tool-bash-persistent/src/index.ts:432`](../packages/shell/tool-bash-persistent/src/index.ts) +Source: [`packages/shell/tool-bash-persistent/src/index.ts:435`](../packages/shell/tool-bash-persistent/src/index.ts) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index a9d7502de0..07693911ae 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -2742,7 +2742,7 @@ export interface Config { } ``` -来源:[`packages/shell/tool-bash-persistent/src/index.ts:432`](../packages/shell/tool-bash-persistent/src/index.ts) +来源:[`packages/shell/tool-bash-persistent/src/index.ts:435`](../packages/shell/tool-bash-persistent/src/index.ts) diff --git a/packages/lsp/lsp-stdio/tests/lifecycle.spec.ts b/packages/lsp/lsp-stdio/tests/lifecycle.spec.ts index 9cedab10d5..551d1ffe05 100644 --- a/packages/lsp/lsp-stdio/tests/lifecycle.spec.ts +++ b/packages/lsp/lsp-stdio/tests/lifecycle.spec.ts @@ -304,6 +304,7 @@ describe('lsp-stdio end to end over a fake server', () => { LSP_FAKE_DEF: 'null', LSP_FAKE_OPEN_MARKER: marker, }, {}, (registered) => { provider = registered }) + const firstReply = Promise.withResolvers() const release = Promise.withResolvers() const request = Object.getOwnPropertyDescriptor(LspConnection.prototype, 'request')?.value as LspConnection['request'] let holdFirst = true @@ -311,14 +312,17 @@ describe('lsp-stdio end to end over a fake server', () => { const hold = method === 'textDocument/definition' && holdFirst if (hold) holdFirst = false const result = await request.call(this, method, params) - if (hold) await release.promise + if (hold) { + firstReply.resolve(undefined) + await release.promise + } return result }) const pending: Promise[] = [] try { const first = ctx.lsp.query(query('goToDefinition')) pending.push(Promise.allSettled([first])) - await waitFor(async () => (await markerLines(marker)).length === 1) + await Promise.race([firstReply.promise, first]) // The changed tail proves the second query entered the provider queue // while the first response is held, before the source rewrite starts. const queues = (provider as unknown as { queues: ReadonlyMap> }).queues diff --git a/snapshots/sdk/persistent-tools/notifications.expected.jsonl b/snapshots/sdk/persistent-tools/notifications.expected.jsonl index ac2aee992b..4139dd054a 100644 --- a/snapshots/sdk/persistent-tools/notifications.expected.jsonl +++ b/snapshots/sdk/persistent-tools/notifications.expected.jsonl @@ -11,17 +11,17 @@ {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"session/title","seq":9,"time":0,"data":{"title":"Prove that bash state persists.","messageSeqs":[5],"source":{"kind":"fallback"}}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":10,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"bash-1","name":"bash","args":["{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":11,"time":0,"data":{"turn":1,"step":1,"callId":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":12,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"bash-1"},"content":[{"type":"tool-result","toolCallId":"bash-1","content":[{"type":"text","text":"COUNT=1 CWD=/tmp"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[11],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":12,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"bash-1"},"content":[{"type":"tool-result","toolCallId":"bash-1","content":[{"type":"text","text":"COUNT=1 CWD=/tmp\n[Command finished with exit code 0]"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[11],"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":13,"time":0,"data":{"turn":1,"step":1}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":14,"time":0,"data":{"turn":1,"step":2}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":15,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"bash-2","name":"bash","args":["{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":16,"time":0,"data":{"turn":1,"step":2,"callId":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":17,"time":0,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"bash-2"},"content":[{"type":"tool-result","toolCallId":"bash-2","content":[{"type":"text","text":"COUNT=2 CWD=/tmp"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[16],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":17,"time":0,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"bash-2"},"content":[{"type":"tool-result","toolCallId":"bash-2","content":[{"type":"text","text":"COUNT=2 CWD=/tmp\n[Command finished with exit code 0]"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[16],"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":18,"time":0,"data":{"turn":1,"step":2}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":19,"time":0,"data":{"turn":1,"step":3}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":20,"time":0,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"bash-delayed-pipeline","name":"bash","args":["{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":21,"time":0,"data":{"turn":1,"step":3,"callId":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":22,"time":0,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"bash-delayed-pipeline"},"content":[{"type":"tool-result","toolCallId":"bash-delayed-pipeline","content":[{"type":"text","text":"delayed"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[21],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":22,"time":0,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"bash-delayed-pipeline"},"content":[{"type":"tool-result","toolCallId":"bash-delayed-pipeline","content":[{"type":"text","text":"delayed\n[Command finished with exit code 0]"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[21],"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":23,"time":0,"data":{"turn":1,"step":3}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":24,"time":0,"data":{"turn":1,"step":4}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":25,"time":0,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"editor-create","name":"str_replace_editor","args":["{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}}} diff --git a/snapshots/sdk/persistent-tools/session.v3.jsonl b/snapshots/sdk/persistent-tools/session.v3.jsonl index 7e8e67db00..fea6a2ba43 100644 --- a/snapshots/sdk/persistent-tools/session.v3.jsonl +++ b/snapshots/sdk/persistent-tools/session.v3.jsonl @@ -11,17 +11,17 @@ {"type":"session/title","data":{"title":"Prove that bash state persists.","messageSeqs":[5],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788699456512,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699456512,"index":0,"dt":[],"id":"bash-1","name":"bash","args":["{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"]},{"type":"chunk","time":1788699456512,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}},{"type":"chunk","time":1788699456512,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788699456512,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}} -{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"bash-1"},"content":[{"type":"tool-result","toolCallId":"bash-1","content":[{"type":"text","text":"COUNT=1 CWD=/tmp"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[11],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"bash-1"},"content":[{"type":"tool-result","toolCallId":"bash-1","content":[{"type":"text","text":"COUNT=1 CWD=/tmp\n[Command finished with exit code 0]"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[11],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788699456882,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699456882,"index":0,"dt":[],"id":"bash-2","name":"bash","args":["{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"]},{"type":"chunk","time":1788699456882,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}},{"type":"chunk","time":1788699456882,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788699456882,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}} -{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"bash-2"},"content":[{"type":"tool-result","toolCallId":"bash-2","content":[{"type":"text","text":"COUNT=2 CWD=/tmp"}],"isError":false}],"role":"user","id":"{{message:7}}"}},"sourceEventSeqs":[16],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"bash-2"},"content":[{"type":"tool-result","toolCallId":"bash-2","content":[{"type":"text","text":"COUNT=2 CWD=/tmp\n[Command finished with exit code 0]"}],"isError":false}],"role":"user","id":"{{message:7}}"}},"sourceEventSeqs":[16],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} {"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788699456983,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699456983,"index":0,"dt":[],"id":"bash-delayed-pipeline","name":"bash","args":["{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"]},{"type":"chunk","time":1788699456983,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}}},{"type":"chunk","time":1788699456983,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788699456983,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":3,"callId":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}} -{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"bash-delayed-pipeline"},"content":[{"type":"tool-result","toolCallId":"bash-delayed-pipeline","content":[{"type":"text","text":"delayed"}],"isError":false}],"role":"user","id":"{{message:9}}"}},"sourceEventSeqs":[21],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"bash-delayed-pipeline"},"content":[{"type":"tool-result","toolCallId":"bash-delayed-pipeline","content":[{"type":"text","text":"delayed\n[Command finished with exit code 0]"}],"isError":false}],"role":"user","id":"{{message:9}}"}},"sourceEventSeqs":[21],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} {"type":"step/start","data":{"turn":1,"step":4}} {"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788699457224,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699457224,"index":0,"dt":[],"id":"editor-create","name":"str_replace_editor","args":["{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"]},{"type":"chunk","time":1788699457225,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}},{"type":"chunk","time":1788699457225,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788699457225,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} diff --git a/snapshots/web/minimal-preset/session.v3.jsonl b/snapshots/web/minimal-preset/session.v3.jsonl index 512a0a78c2..8348820c4d 100644 --- a/snapshots/web/minimal-preset/session.v3.jsonl +++ b/snapshots/web/minimal-preset/session.v3.jsonl @@ -13,7 +13,7 @@ {"type":"session/title","data":{"title":"Use the bash tool to","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"minimal-bash-card","name":"bash","arguments":"{\"command\":\"printf 'MINIMAL_BASH_CARD_OK\\\\n'\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":4},"stream":[{"type":"chunk","time":1788699487232,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699487243,"index":0,"dt":[],"id":"minimal-bash-card","name":"bash","args":["{\"command\":\"printf 'MINIMAL_BASH_CARD_OK\\\\n'\"}"]},{"type":"chunk","time":1788699487254,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"minimal-bash-card","name":"bash","arguments":"{\"command\":\"printf 'MINIMAL_BASH_CARD_OK\\\\n'\"}"}}},{"type":"chunk","time":1788699487265,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":4}}},{"type":"chunk","time":1788699487276,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"minimal-bash-card","name":"bash","arguments":"{\"command\":\"printf 'MINIMAL_BASH_CARD_OK\\\\n'\"}"}} -{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"minimal-bash-card"},"content":[{"type":"tool-result","toolCallId":"minimal-bash-card","content":[{"type":"text","text":"MINIMAL_BASH_CARD_OK"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"minimal-bash-card"},"content":[{"type":"tool-result","toolCallId":"minimal-bash-card","content":[{"type":"text","text":"MINIMAL_BASH_CARD_OK\n[Command finished with exit code 0]"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"MINIMAL_PRESET_REQUEST_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":4},"stream":[{"type":"chunk","time":1788699487708,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788699487719,"index":0,"dt":[],"texts":["MINIMAL_PRESET_REQUEST_OK"]},{"type":"chunk","time":1788699487730,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"MINIMAL_PRESET_REQUEST_OK"}}},{"type":"chunk","time":1788699487741,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":4}}},{"type":"chunk","time":1788699487753,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} diff --git a/snapshots/web/minimal-preset/ui.expected.md b/snapshots/web/minimal-preset/ui.expected.md index 9ad6fdf256..f730fe2b50 100644 --- a/snapshots/web/minimal-preset/ui.expected.md +++ b/snapshots/web/minimal-preset/ui.expected.md @@ -24,7 +24,7 @@ - button "Bash printf 'MINIMAL_BASH_CARD_OK\\n'" [expanded]: - img - text: Bash printf 'MINIMAL_BASH_CARD_OK\n' -- text: "IN { \"command\": \"printf 'MINIMAL_BASH_CARD_OK\\\\n'\" } OUT MINIMAL_BASH_CARD_OK" +- text: "IN { \"command\": \"printf 'MINIMAL_BASH_CARD_OK\\\\n'\" } OUT MINIMAL_BASH_CARD_OK [Command finished with exit code 0]" - button "Inspect" - paragraph: MINIMAL_PRESET_REQUEST_OK - button "Copy": From 4977ccc965b93e7bf6fff0c2c8399014c5c0d198 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Tue, 8 Sep 2026 23:29:58 +0800 Subject: [PATCH 32/58] chore(chunked-list): follow master release version --- packages/util/chunked-list/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/util/chunked-list/package.json b/packages/util/chunked-list/package.json index 130bde0ae2..2a3fc316da 100644 --- a/packages/util/chunked-list/package.json +++ b/packages/util/chunked-list/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-chunked-list", "description": "Persistent append-only chunked lists with bounded copying and JSON checkpoint validation", - "version": "0.1.3-alpha.2", + "version": "0.1.5-alpha.1", "publishConfig": { "access": "public" }, From b42b5b5e9483041f6a596dcff225f44af67a9ef0 Mon Sep 17 00:00:00 2001 From: fz Date: Tue, 8 Sep 2026 23:57:15 +0800 Subject: [PATCH 33/58] fix(ci): align minimal prompt-history expectations with shell-only turns --- scripts/smoke-python-runtime.py | 2 +- .../minimal-in-history/prompt-history.json | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/scripts/smoke-python-runtime.py b/scripts/smoke-python-runtime.py index 47b8d664d2..5004b15efc 100644 --- a/scripts/smoke-python-runtime.py +++ b/scripts/smoke-python-runtime.py @@ -1837,7 +1837,7 @@ def build_in_history_snapshot_files( assert all(event.get("surfaceOp") in (None, "append") for event in result.events) first_tool = next(index for index, event in enumerate(result.events) if event.get("type") == "tool/result") assert result.events.index(systems[1]) > first_tool - assert len(requests) == 4, requests + assert len(requests) == 3, requests request_prompts = [] for index, request in enumerate(requests): messages = request["messages"] diff --git a/scripts/snapshots/python-sdk-single-exe/minimal-in-history/prompt-history.json b/scripts/snapshots/python-sdk-single-exe/minimal-in-history/prompt-history.json index 9fe5365ee8..7faf3de3ee 100644 --- a/scripts/snapshots/python-sdk-single-exe/minimal-in-history/prompt-history.json +++ b/scripts/snapshots/python-sdk-single-exe/minimal-in-history/prompt-history.json @@ -7,10 +7,6 @@ "You are a helpful software engineer assistant.\n\nPython SDK prompt version 1.", "You are a helpful software engineer assistant.\n\nPython SDK prompt version 2." ], - [ - "You are a helpful software engineer assistant.\n\nPython SDK prompt version 1.", - "You are a helpful software engineer assistant.\n\nPython SDK prompt version 2." - ], [ "You are a helpful software engineer assistant.\n\nPython SDK prompt version 1.", "You are a helpful software engineer assistant.\n\nPython SDK prompt version 2." From 581803bf57949c303ee6cbbaab0142e00f64b2c0 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Tue, 8 Sep 2026 17:53:28 +0800 Subject: [PATCH 34/58] feat(client): add global sidebar panels --- .../ui-conversation/src/client/apply.ts | 15 +- .../src/client/contract/slots.ts | 4 +- .../src/client/skeleton/ConversationPanel.tsx | 12 + packages/client/ui-layout/package.json | 1 + .../client/ui-layout/src/client/AppFrame.tsx | 80 ++-- .../ui-layout/src/client/DocumentTitle.tsx | 13 +- packages/client/ui-layout/src/client/index.ts | 74 ++-- .../client/ui-layout/src/client/service.ts | 57 ++- .../client/ui-layout/src/client/stores.ts | 84 ++-- packages/client/ui-layout/tsconfig.json | 3 + .../src/client/contract/slots.ts | 3 + .../ui-sidebar-right/src/client/index.ts | 37 +- .../src/client/shell/RightbarRoot.tsx | 20 + .../src/client/shell/SidebarRight.tsx | 5 +- packages/client/ui-sidebar/package.json | 1 + .../src/client/SidebarRoot.module.css | 72 +++- .../ui-sidebar/src/client/SidebarRoot.tsx | 64 ++- .../ui-sidebar/src/client/contract/slots.ts | 55 ++- .../hello-world/HelloWorldPanel.module.css | 33 ++ .../client/hello-world/HelloWorldPanel.tsx | 29 ++ .../client/ui-sidebar/src/client/index.ts | 78 ++-- .../client/ui-sidebar/src/client/locales.ts | 8 +- packages/client/ui-sidebar/tsconfig.json | 3 + packages/client/ui-workspace/package.json | 2 + .../client/ui-workspace/src/client/index.ts | 10 +- .../ui-workspace/src/client/navigation.ts | 14 +- .../src/client/rows/WorkspaceBrowser.tsx | 23 +- packages/client/ui-workspace/tsconfig.json | 3 + .../src/client/slot-catalog.ts | 369 ++++++++++++++---- pnpm-lock.yaml | 9 + 30 files changed, 884 insertions(+), 297 deletions(-) create mode 100644 packages/client/ui-conversation/src/client/skeleton/ConversationPanel.tsx create mode 100644 packages/client/ui-sidebar-right/src/client/shell/RightbarRoot.tsx create mode 100644 packages/client/ui-sidebar/src/client/hello-world/HelloWorldPanel.module.css create mode 100644 packages/client/ui-sidebar/src/client/hello-world/HelloWorldPanel.tsx diff --git a/packages/client/ui-conversation/src/client/apply.ts b/packages/client/ui-conversation/src/client/apply.ts index 65b8374919..7827186ede 100644 --- a/packages/client/ui-conversation/src/client/apply.ts +++ b/packages/client/ui-conversation/src/client/apply.ts @@ -28,6 +28,7 @@ import { queueDockEntry } from './queue/QueueDock.tsx' import { EnterBehaviorRow } from './settings/EnterBehaviorRow.tsx' import type { EnterBehaviorRowInjected } from './settings/EnterBehaviorRow.tsx' import { ConversationRoot } from './skeleton/ConversationRoot.tsx' +import { ConversationPanel } from './skeleton/ConversationPanel.tsx' import { ConversationSession, ConversationSessionHeader } from './skeleton/ConversationSession.tsx' import { InputBar } from './skeleton/InputBar.tsx' import { todoDockEntry } from './skeleton/TodoPanel.tsx' @@ -84,6 +85,7 @@ const ABSENT_FILE_UPLOADS = { } interface WorkspaceNavigation { + openSession(sessionId: SessionId): void connectWorkspace( workspaceId: Parameters[0], ): Promise @@ -214,7 +216,7 @@ export function apply(ctx: Context, config: Config = Config({})): void { }) const registerConversationRoot = () => slots.register({ - name: 'conversation', + name: 'main.conversation', locale: NS, children: { 'conversation.session': { kind: 'single', scope: 'session' }, @@ -251,7 +253,7 @@ export function apply(ctx: Context, config: Config = Config({})): void { } } } - sessions.open(nextId) + workspaceNavigation.openSession(nextId) }, }), }, ConversationRoot) @@ -284,7 +286,7 @@ export function apply(ctx: Context, config: Config = Config({})): void { store: conversationStore, inject: (sessionId: SessionId, actions: BoundActions): ConversationSessionHeaderInjected => ({ hooks: { conversationViews }, - open: (id) => { sessions.open(id) }, + open: (id) => { workspaceNavigation.openSession(id) }, selectView: (view) => { activateView(sessionId, view) actions.setView(view) @@ -384,7 +386,12 @@ export function apply(ctx: Context, config: Config = Config({})): void { }, }, InputBar) - slots.inject('conversation', function* () { + slots.inject('main', function* () { + yield slots.register({ + name: 'main', + key: 'conversation', + children: { 'main.conversation': { kind: 'single', scope: 'session-maybe' } }, + }, ConversationPanel) yield registerConversationRoot() yield registerConversationSession() yield registerConversationHeader() diff --git a/packages/client/ui-conversation/src/client/contract/slots.ts b/packages/client/ui-conversation/src/client/contract/slots.ts index eb38a0f261..cf76e02b1b 100644 --- a/packages/client/ui-conversation/src/client/contract/slots.ts +++ b/packages/client/ui-conversation/src/client/contract/slots.ts @@ -117,6 +117,8 @@ export type UseConversationViews = SnapshotSelectorHook declare module '@deepseek-ai/dsh-client-ui-slots' { interface SlotMap { + /** Conversation shell beneath its root-scoped main-panel entry. */ + 'main.conversation': { kind: 'single'; scope: 'session-maybe' } /** Strict per-Session Conversation body. */ 'conversation.session': { kind: 'single'; scope: 'session' } /** Strict per-Session title, actions, and View navigation. */ @@ -364,7 +366,7 @@ export interface HeroBrandMarkOwnerProps { /** Full props of the resident optional-Session Conversation shell. */ export type ConversationSlotProps = - PropsRuntime<'conversation'> + PropsRuntime<'main.conversation'> & PropsRenderSlots< | 'conversation.session' | 'conversation.session.header' | 'conversation.composer' | 'conversation.composer.bar' diff --git a/packages/client/ui-conversation/src/client/skeleton/ConversationPanel.tsx b/packages/client/ui-conversation/src/client/skeleton/ConversationPanel.tsx new file mode 100644 index 0000000000..8bd11aac33 --- /dev/null +++ b/packages/client/ui-conversation/src/client/skeleton/ConversationPanel.tsx @@ -0,0 +1,12 @@ +/** Root-scoped main occupant; Session binding belongs to its Conversation child. */ +import type { PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' +import type {} from '../contract/slots.ts' + +/** + * Render the Conversation with optional current-Session binding. + * @param props - main-slot inputs and the declared Conversation renderer. + * @returns the Conversation subtree. + */ +export function ConversationPanel({ renderSlot }: PropsRuntime<'main'> & PropsRenderSlots<'main.conversation'>) { + return renderSlot('main.conversation', {}) +} diff --git a/packages/client/ui-layout/package.json b/packages/client/ui-layout/package.json index 1f10a544ad..a2c025f39d 100644 --- a/packages/client/ui-layout/package.json +++ b/packages/client/ui-layout/package.json @@ -45,6 +45,7 @@ "@deepseek-ai/cordis": "workspace:^" }, "devDependencies": { + "@deepseek-ai/dsh-brand": "workspace:^", "@deepseek-ai/dsh-client-locale": "workspace:^", "@deepseek-ai/dsh-client-store": "workspace:^", "@deepseek-ai/dsh-client-ui-renderer": "workspace:^", diff --git a/packages/client/ui-layout/src/client/AppFrame.tsx b/packages/client/ui-layout/src/client/AppFrame.tsx index 905341f503..05bb4b9494 100644 --- a/packages/client/ui-layout/src/client/AppFrame.tsx +++ b/packages/client/ui-layout/src/client/AppFrame.tsx @@ -3,10 +3,9 @@ * shell renders only 'root'). Owns the grid tracks (sidebar | center | * rightbar), the drag handles (pointer capture + rAF throttle), the column * solve (columns.ts), and the child-slot render decisions: the sidebar slot - * renders HERE with live parameters from that solve, and the session-aware - * occupants render in fixed column positions; the strict right-column entry - * gates itself on current-session availability while the session-maybe - * conversation retains identity. + * receives live parameters from that solve. The root-scoped main slot selects + * the Conversation or a global panel. Each column occupant owns its Session + * binding and reports the geometry it needs. * * The right column is a track, not a box: its occupant draws its panel anchored * to the frame's right edge at the resolved normal width, and the @@ -15,7 +14,7 @@ * track but hides the outer resize handle. Everything arrives through the framework * shares — zero cordis or framework imports, zero self-made hooks. */ -import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import type { ReactNode } from 'react' import type { PropsLocale, PropsRenderSlots, PropsRuntime, PropsStore, @@ -28,7 +27,7 @@ import css from './AppFrame.module.css' /** Full composed props: runtime share + child-slot render share + store share. */ export type AppFrameProps = & PropsRuntime<'root'> - & PropsRenderSlots<'sidebar' | 'conversation' | 'rightbar' | 'shell.overlay'> + & PropsRenderSlots<'sidebar' | 'main' | 'rightbar' | 'shell.overlay'> & PropsStore> & PropsLocale<'common'> @@ -37,6 +36,12 @@ function CenterColumn(props: { children?: ReactNode }) { return
    {props.children}
    } +/** Subscribe to the main key without subscribing the column frame to each panel id. */ +function MainPanel({ usePanelInfo, renderSlot }: Pick, 'usePanelInfo'> & PropsRenderSlots<'main'>) { + const panelId = usePanelInfo(info => info.activePanelId) + return renderSlot('main', {}, { entryKey: panelId ?? 'conversation' }) +} + /** * Right column grid item. Zero-width unless the occupant asked for a track; the * occupant's panel is positioned against the column's right edge, which never @@ -116,18 +121,14 @@ function DragHandle(props: { side: 'sidebar' | 'rightbar'; left: number; onStart export function AppFrame({ useStore, useSessions, + usePanelInfo, actions, renderSlot, - SessionProvider, t, }: AppFrameProps) { - const panels = useStore(s => s) - const documentTitle = useSessions((s) => { - const current = s.current - return current === undefined ? undefined : s.byId[current]?.title - }) + const layoutInfo = useStore(state => state.layoutInfo) const frameRef = useRef(null) - const viewport = panels.viewportWidth + const viewport = layoutInfo.viewportWidth // Track the frame's own box (not the window): rAF-throttled ResizeObserver. useLayoutEffect(() => { @@ -157,15 +158,15 @@ export function AppFrame({ }, [actions]) const narrow = viewport < SIDEBAR_AUTO_COLLAPSE - const sidebarCollapsed = narrow ? !panels.narrowExpanded : panels.sidebar === 0 + const sidebarCollapsed = narrow ? !layoutInfo.narrowExpanded : layoutInfo.sidebar === 0 const sidebarPreference = sidebarCollapsed ? 0 - : panels.sidebar === 0 ? SIDEBAR_DEFAULT : panels.sidebar - const rightbarPreference = panels.rightbar ?? viewport * RIGHTBAR_DEFAULT_RATIO + : layoutInfo.sidebar === 0 ? SIDEBAR_DEFAULT : layoutInfo.sidebar + const rightbarPreference = layoutInfo.rightbar ?? viewport * RIGHTBAR_DEFAULT_RATIO // Opening on a narrow frame collapses the left sidebar. Eligibility must // include that space before the occupant's first shown report arrives. - const normal = computeColumns(viewport, !panels.rightbarShown && narrow ? 0 : sidebarPreference, rightbarPreference) - const cols = computeColumns(viewport, sidebarPreference, panels.rightbarTrack ? rightbarPreference : 0) + const normal = computeColumns(viewport, !layoutInfo.rightbarShown && narrow ? 0 : sidebarPreference, rightbarPreference) + const cols = computeColumns(viewport, sidebarPreference, layoutInfo.rightbarTrack ? rightbarPreference : 0) const colsRef = useRef(cols) colsRef.current = cols const rightbarWidth = useRef(normal.rightbar) @@ -189,6 +190,14 @@ export function AppFrame({ actions.setRightbar(rightbarBase.current - dx) }, [actions]) const productTitle = process.env.DSH_CLIENT_TITLE ?? t('brand.localBuild') + const sidebar = useMemo(() => renderSlot('sidebar', { + collapsed: sidebarCollapsed, + width: cols.sidebar, + }), [renderSlot, sidebarCollapsed, cols.sidebar]) + const main = useMemo(() => ( + + ), [usePanelInfo, renderSlot]) + const overlays = useMemo(() => renderSlot('shell.overlay', {}), [renderSlot]) return (
    - {/* Render-site slot call with live concession output: a closed - sidebar keeps the mounted slot at the compact-rail width, and the - component sees its rendered state as owner params decided here - (collapsed follows the resolved rail, so a derived auto-collapse - renders the rail UI too). */} - {renderSlot('sidebar', { - collapsed: sidebarCollapsed, - width: cols.sidebar, - })} + {sidebar}
    <> - {/* Both column occupants stay at fixed tree positions from first - paint — no loading gate: a bare status line reads worse than - the shell's own pending rendering. The conversation is - session-maybe; SessionProvider withholds the strict right-column - entry while no session is current. */} - {renderSlot('conversation', {})} + {main} - {/* Strict session entry: with no session there is no surface, and the - column is an empty zero-width track. The occupant receives the - panel width it should draw at; the track is the frame's business. */} - - {renderSlot('rightbar', { width: normal.rightbar, viewportWidth: viewport, canShow: normal.rightbar > 0 })} - + {renderSlot('rightbar', { width: normal.rightbar, viewportWidth: viewport, canShow: normal.rightbar > 0 })}
    - {renderSlot('shell.overlay', {})} + {overlays}
    {/* The collapsed rail is fixed-width: no resize handle while closed. */} {!sidebarCollapsed && } - {panels.rightbarShown && !panels.rightbarFullscreen && normal.rightbar > 0 && ( + {layoutInfo.rightbarShown && !layoutInfo.rightbarFullscreen && normal.rightbar > 0 && ( )}
    diff --git a/packages/client/ui-layout/src/client/DocumentTitle.tsx b/packages/client/ui-layout/src/client/DocumentTitle.tsx index 4060a3b187..13663f53d1 100644 --- a/packages/client/ui-layout/src/client/DocumentTitle.tsx +++ b/packages/client/ui-layout/src/client/DocumentTitle.tsx @@ -1,9 +1,9 @@ +/** Browser title selection follows the active main panel without subscribing the frame. */ import { useEffect } from 'react' +import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' /** Props for the browser title projection. */ -export interface DocumentTitleProps { - /** Durable title of the selected session, or undefined for the product title. */ - title?: string +export type DocumentTitleProps = Pick, 'useSessions' | 'usePanelInfo'> & { /** Build-configured or localized product title. */ productTitle: string } @@ -14,7 +14,12 @@ export interface DocumentTitleProps { * @param props - Selected session title projection. * @returns No rendered content. */ -export function DocumentTitle({ title, productTitle }: DocumentTitleProps): null { +export function DocumentTitle({ useSessions, usePanelInfo, productTitle }: DocumentTitleProps): null { + const showSessionTitle = usePanelInfo(info => info.activePanelId === null) + const title = useSessions(state => { + const current = state.current + return !showSessionTitle || current === undefined ? undefined : state.byId[current]?.title + }) useEffect(() => { document.title = title === undefined ? productTitle : `${title} — ${productTitle}` return () => { document.title = productTitle } diff --git a/packages/client/ui-layout/src/client/index.ts b/packages/client/ui-layout/src/client/index.ts index 649215b96d..83439c9d8e 100644 --- a/packages/client/ui-layout/src/client/index.ts +++ b/packages/client/ui-layout/src/client/index.ts @@ -3,8 +3,8 @@ * the runtime's built-in 'root' slot and, in the same breath, declares the * four child slots (declaration = exclusive render authority), seats the * layout store (panel geometry), and wires the panel-action service face. - * ctx.layout is the cross-plugin panel-action contract; navigation state lives - * with the runtime sessions service. A second effect seats the theme + * ctx.layout selects the main panel and controls column geometry; Session + * selection belongs to the Session Controller. A second effect seats the theme * presenter, which projects ctx.theme snapshots onto document.body. */ import type { Context as ClientContext } from '@deepseek-ai/cordis' @@ -12,7 +12,8 @@ import type {} from '@deepseek-ai/dsh-client-locale/client' import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' import type {} from '@deepseek-ai/dsh-client-ui-session/client' import type {} from '@deepseek-ai/dsh-client-ui-theme/client' -import type { PanelActions } from './service.ts' +import type { HostObservable, SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots' +import type { PanelInfo } from './service.ts' import { AppFrame } from './AppFrame.tsx' import { createLayoutStore } from './stores.ts' import { LayoutController } from './service.ts' @@ -24,7 +25,10 @@ import { ThemePresenter } from './theme-presenter.ts' // OwnerShare contracts below are the render-side halves registrants compose // against; the frame components and the store factory are package-internal. export { LayoutController } from './service.ts' -export type { ILayout } from './service.ts' +export type { ILayout, MainPanelId, PanelInfo } from './service.ts' + +/** Selector hook over root-scoped panel selection. */ +export type UsePanelInfo = SnapshotSelectorHook declare module '@deepseek-ai/cordis' { interface Context { @@ -34,6 +38,11 @@ declare module '@deepseek-ai/cordis' { } declare module '@deepseek-ai/dsh-client-ui-slots' { + interface GlobalStandardProps { + /** Subscribe to the selected main panel independently of parent renders. */ + usePanelInfo: UsePanelInfo + } + interface SlotMap { // The 'root' entry itself is the runtime's built-in slot (declared // there); these four are the frame's children, declared by the same @@ -51,18 +60,10 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { */ 'sidebar': { kind: 'single'; scope: 'root'; owner: SidebarOwnerProps } /** - * The whole center column, across both the no-session hero and a live - * conversation. OCCUPIED by ui-conversation's ConversationRoot, which - * declares the session body, composer, and input seats inside it — - * registering here replaces the entire conversation surface (and removes - * every seat it declares) rather than adding to it. - * - * Current-session-optional: the occupant owns both states without - * changing its React identity, so it keeps its own state across a session - * switch. It receives no owner props; session facts arrive through the - * framework hooks of the `session-maybe` scope. + * Central panel selected by sidebar entry id. The reserved `conversation` + * key hosts the Conversation; other keys receive no Session binding. */ - 'conversation': { kind: 'single'; scope: 'session-maybe'; owner: ConvOwnerProps } + 'main': { kind: 'keyed'; scope: 'root' } /** * The right column: a track the centre makes room for, or nothing. OCCUPIED * by the right Sidebar, which uses the resolved column width in normal @@ -73,10 +74,10 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { * occupant's own recorded business — it reports the composition of its * expanded and presentation state through `ctx.layout`, and the frame sizes * the track and places the resize handle from that. The expand control is - * not this column's: it is a button in the conversation header. With no - * current session nothing is mounted here. + * not this column's: it is a button in the conversation header. The root + * occupant decides when to render its Session-bound content. */ - 'rightbar': { kind: 'single'; scope: 'session'; owner: RightbarOwnerProps } + 'rightbar': { kind: 'single'; scope: 'root'; owner: RightbarOwnerProps } /** * Frame-wide floating layer, above every column and outside their scroll * containers. Deliberately generic and unowned by any feature: a badge, a @@ -105,9 +106,6 @@ export interface SidebarOwnerProps { width: number } -/** Conversation owner share: business state and actions belong to the registrant. */ -export interface ConvOwnerProps {} - /** Right column owner share: resolved normal geometry and opening eligibility. */ export interface RightbarOwnerProps { /** Resolved normal panel width in px, not the saved preference; zero if it cannot fit. */ @@ -127,34 +125,42 @@ export const inject = ['slots', 'theme', 'locale'] /** * Client plugin body: provide ctx.layout, then one register() call — AppFrame * into 'root' with the four child-slot declarations, the layout store seat, - * and the inject hook that hands the store's bound actions to the service. + * and the shared root instance supplying commands and the panel-info source. * @param ctx - client root context. */ export function apply(ctx: ClientContext): void { - const layout = new LayoutController() ctx.effect(() => { + const handle = createLayoutStore() + const instance = handle.create() + const store: typeof handle = { ...handle, create: () => instance } + const layout = new LayoutController(instance.actions) + const retainMainPanels = (): void => { + instance.actions.retainMainPanels(ctx.slots.entries('main').flatMap(entry => + entry.options.key === undefined ? [] : [entry.options.key])) + } + const panelInfo: HostObservable = { + getSnapshot: () => instance.getSnapshot().panelInfo, + subscribe: listener => instance.subscribe(listener), + } + const disposePanelInfo = ctx.slots.provideRoot({ hooks: { panelInfo } }) const disposeService = ctx.reflect.provide('layout', layout) const disposeRegistration = ctx.slots.register({ name: 'root', locale: 'common', children: { 'sidebar': { kind: 'single', scope: 'root' }, - 'conversation': { kind: 'single', scope: 'session-maybe' }, - 'rightbar': { kind: 'single', scope: 'session' }, + 'main': { kind: 'keyed', scope: 'root' }, + 'rightbar': { kind: 'single', scope: 'root' }, 'shell.overlay': { kind: 'list', scope: 'root' }, }, - // Exclusive store: the factory itself — the framework instantiates per - // entry and delivers useStore/actions to AppFrame as standard props. - store: createLayoutStore, - // The hook's only side effect connects the root store to ctx.layout; - // conversation business actions belong to their registrants. - inject: (actions: PanelActions) => { - layout.attachPanels(actions) - return {} - }, + store, }, AppFrame) + const disposePanels = ctx.slots.subscribe('main', retainMainPanels) + retainMainPanels() return () => { + disposePanels() disposeRegistration() + disposePanelInfo() // provide()'s disposer settles asynchronously; teardown is synchronous fire-and-forget. void disposeService() } diff --git a/packages/client/ui-layout/src/client/service.ts b/packages/client/ui-layout/src/client/service.ts index 46369f0346..7835f3aa20 100644 --- a/packages/client/ui-layout/src/client/service.ts +++ b/packages/client/ui-layout/src/client/service.ts @@ -1,26 +1,36 @@ /** * LayoutController: the cross-plugin panel-action face behind ctx.layout. - * Panel geometry itself lives in the root entry's layout store (stores.ts); + * Panel geometry and main-panel selection live in the root layout store; * the current-session selection lives with the runtime sessions service, and * the per-session active view dissolved into ui-conversation's session store * (its only consumer). What remains here is the contract other plugins' - * apply worlds reach for panel transitions (sidebar toggle from ui-sidebar, + * apply worlds reach for panel transitions (main-panel selection and sidebar toggle, * right-panel show/hide from ui-sidebar-right) — writes stay inside the - * store's declared action set, delivered as the registration's bound actions. + * store's declared action set, shared with the root registration. */ import type { BoundActions } from '@deepseek-ai/dsh-client-ui-slots' +import type { Branded } from '@deepseek-ai/dsh-brand' import type { createLayoutStore } from './stores.ts' +/** Identity shared by a sidebar panel entry and its main-slot occupant. */ +export type MainPanelId = Branded<'MainPanelId'> + +/** Root-scoped navigation state exposed to panel-aware components. */ +export interface PanelInfo { + /** Selected global panel; null displays the current Conversation. */ + readonly activePanelId: MainPanelId | null +} + /** The layout store's bound action set (framework-baked, draft params peeled). */ export type PanelActions = BoundActions> -/** - * The outward layout face (`ctx.layout`): the panel transitions other - * plugins may trigger — and exactly what a test fake must supply. The - * attachPanels wiring hook stays on the concrete class (root-entry assembly - * only). - */ +/** Panel navigation and geometry actions exposed through ctx.layout. */ export interface ILayout { + /** + * Select a global central panel without changing the current Session. + * @param panelId - registered main key, or null to show the Conversation. + */ + selectPanel(panelId: MainPanelId | null): void /** Toggle the sidebar panel (closed ⟷ contract default width). */ toggleSidebar(): void /** @@ -37,39 +47,26 @@ export interface ILayout { /** Cross-plugin panel-action face (ctx.layout). */ export class LayoutController implements ILayout { - #panels: PanelActions | undefined + /** @param panels - actions of the instance shared with the root entry. */ + constructor(private readonly panels: PanelActions) {} - /** - * Adopt the root entry's bound store actions. Called from the root - * registration's inject hook (a sanctioned assembly side effect), so the - * face is live from the entry's first render; on entry re-register the - * fresh actions overwrite the stale set. - * @param actions - bound actions of the entry's layout store instance. - */ - attachPanels(actions: PanelActions): void { - this.#panels = actions + /** Select a global panel or return to the Conversation. */ + selectPanel(panelId: MainPanelId | null): void { + this.panels.selectPanel(panelId) } /** Toggle the sidebar panel (closed ⟷ contract default width). */ toggleSidebar(): void { - this.#require().toggleSidebar() + this.panels.toggleSidebar() } /** Report the right panel's track and fullscreen presentation. */ openRightbar(track: boolean, fullscreen: boolean): void { - this.#require().openRightbar(track, fullscreen) + this.panels.openRightbar(track, fullscreen) } /** Report the right panel as hidden: no track, no handle. */ closeRightbar(): void { - this.#require().closeRightbar() - } - - #require(): PanelActions { - // Callers are UI gestures, which cannot fire before the root entry - // rendered (the inject hook runs in its first render) — reaching this - // unwired is a boot-order bug, not a race to tolerate. - if (this.#panels === undefined) throw new Error('layout: panel actions not wired (root entry not mounted)') - return this.#panels + this.panels.closeRightbar() } } diff --git a/packages/client/ui-layout/src/client/stores.ts b/packages/client/ui-layout/src/client/stores.ts index 9191745a66..3a53caf4f1 100644 --- a/packages/client/ui-layout/src/client/stores.ts +++ b/packages/client/ui-layout/src/client/stores.ts @@ -3,6 +3,7 @@ * The registration supplies a fresh store and binds its actions to ctx.layout. */ import { defineStore, type EngineStoreHandle } from '@deepseek-ai/dsh-client-store' +import type { MainPanelId } from './service.ts' import { clampWidth, RIGHTBAR_DEFAULT_RATIO, RIGHTBAR_MAX_RATIO, RIGHTBAR_MIN, SIDEBAR_AUTO_COLLAPSE, SIDEBAR_DEFAULT, SIDEBAR_MAX, SIDEBAR_MIN, @@ -13,6 +14,14 @@ import { * the right panel's expanded state belongs to its occupant. */ type LayoutState = { + panelInfo: { + /** Null selects the Conversation; global panels keep the current Session intact. */ + activePanelId: MainPanelId | null + } + layoutInfo: LayoutInfo +} + +type LayoutInfo = { sidebar: number /** Last positive frame measurement; window width bootstraps the first render. */ viewportWidth: number @@ -47,6 +56,8 @@ type LayoutState = { * return type); drift fails assignability at the defineStore call. */ type LayoutActions = { + selectPanel: (draft: LayoutState, panelId: MainPanelId | null) => void + retainMainPanels: (draft: LayoutState, panelIds: readonly string[]) => void setSidebar: (draft: LayoutState, px: number) => void toggleSidebar: (draft: LayoutState) => void setViewportWidth: (draft: LayoutState, width: number) => void @@ -67,56 +78,67 @@ type LayoutActions = { export function createLayoutStore(): EngineStoreHandle { const handle = defineStore({ init: (): LayoutState => ({ - sidebar: SIDEBAR_DEFAULT, - viewportWidth: window.innerWidth, - narrowExpanded: false, - rightbar: null, - rightbarShown: false, - rightbarTrack: false, - rightbarFullscreen: false, - rightbarInstant: false, + panelInfo: { activePanelId: null }, + layoutInfo: { + sidebar: SIDEBAR_DEFAULT, + viewportWidth: window.innerWidth, + narrowExpanded: false, + rightbar: null, + rightbarShown: false, + rightbarTrack: false, + rightbarFullscreen: false, + rightbarInstant: false, + }, }), actions: { + selectPanel: (d, panelId: MainPanelId | null) => { + d.panelInfo.activePanelId = panelId + }, + retainMainPanels: (d, panelIds: readonly string[]) => { + if (d.panelInfo.activePanelId !== null && !panelIds.includes(d.panelInfo.activePanelId)) { + d.panelInfo.activePanelId = null + } + }, setSidebar: (d, px: number) => { - d.rightbarInstant = false - d.sidebar = clampWidth(px, SIDEBAR_MIN, SIDEBAR_MAX) + d.layoutInfo.rightbarInstant = false + d.layoutInfo.sidebar = clampWidth(px, SIDEBAR_MIN, SIDEBAR_MAX) }, // Narrow toggles flip only the override: the width preference survives // untouched, so re-widening restores the pre-squeeze layout. toggleSidebar: (d) => { - d.rightbarInstant = false - if (d.viewportWidth < SIDEBAR_AUTO_COLLAPSE) d.narrowExpanded = !d.narrowExpanded - else d.sidebar = d.sidebar === 0 ? SIDEBAR_DEFAULT : 0 + d.layoutInfo.rightbarInstant = false + if (d.layoutInfo.viewportWidth < SIDEBAR_AUTO_COLLAPSE) d.layoutInfo.narrowExpanded = !d.layoutInfo.narrowExpanded + else d.layoutInfo.sidebar = d.layoutInfo.sidebar === 0 ? SIDEBAR_DEFAULT : 0 }, // Crossing the breakpoint in either direction drops the override: the // narrow default is auto-collapsed, the wide state is the preference. setViewportWidth: (d, width: number) => { - if (d.viewportWidth === width) return - d.rightbarInstant = false - if ((d.viewportWidth < SIDEBAR_AUTO_COLLAPSE) !== (width < SIDEBAR_AUTO_COLLAPSE)) { - d.narrowExpanded = false + if (d.layoutInfo.viewportWidth === width) return + d.layoutInfo.rightbarInstant = false + if ((d.layoutInfo.viewportWidth < SIDEBAR_AUTO_COLLAPSE) !== (width < SIDEBAR_AUTO_COLLAPSE)) { + d.layoutInfo.narrowExpanded = false } - d.viewportWidth = width + d.layoutInfo.viewportWidth = width }, setRightbar: (d, px: number) => { - d.rightbarInstant = false - d.rightbar = clampWidth(px, RIGHTBAR_MIN, Math.max(RIGHTBAR_MIN, d.viewportWidth * RIGHTBAR_MAX_RATIO)) + d.layoutInfo.rightbarInstant = false + d.layoutInfo.rightbar = clampWidth(px, RIGHTBAR_MIN, Math.max(RIGHTBAR_MIN, d.layoutInfo.viewportWidth * RIGHTBAR_MAX_RATIO)) }, openRightbar: (d, track: boolean, fullscreen: boolean) => { - if (!d.rightbarShown || d.rightbarTrack !== track || d.rightbarFullscreen !== fullscreen) { - d.rightbarInstant = d.rightbarFullscreen && !fullscreen + if (!d.layoutInfo.rightbarShown || d.layoutInfo.rightbarTrack !== track || d.layoutInfo.rightbarFullscreen !== fullscreen) { + d.layoutInfo.rightbarInstant = d.layoutInfo.rightbarFullscreen && !fullscreen } - if (!d.rightbarShown && d.viewportWidth < SIDEBAR_AUTO_COLLAPSE) d.narrowExpanded = false - d.rightbar ??= Math.max(RIGHTBAR_MIN, Math.round(d.viewportWidth * RIGHTBAR_DEFAULT_RATIO)) - d.rightbarShown = true - d.rightbarTrack = track - d.rightbarFullscreen = fullscreen + if (!d.layoutInfo.rightbarShown && d.layoutInfo.viewportWidth < SIDEBAR_AUTO_COLLAPSE) d.layoutInfo.narrowExpanded = false + d.layoutInfo.rightbar ??= Math.max(RIGHTBAR_MIN, Math.round(d.layoutInfo.viewportWidth * RIGHTBAR_DEFAULT_RATIO)) + d.layoutInfo.rightbarShown = true + d.layoutInfo.rightbarTrack = track + d.layoutInfo.rightbarFullscreen = fullscreen }, closeRightbar: (d) => { - if (d.rightbarShown) d.rightbarInstant = d.rightbarFullscreen - d.rightbarShown = false - d.rightbarTrack = false - d.rightbarFullscreen = false + if (d.layoutInfo.rightbarShown) d.layoutInfo.rightbarInstant = d.layoutInfo.rightbarFullscreen + d.layoutInfo.rightbarShown = false + d.layoutInfo.rightbarTrack = false + d.layoutInfo.rightbarFullscreen = false }, }, }) diff --git a/packages/client/ui-layout/tsconfig.json b/packages/client/ui-layout/tsconfig.json index 79a7f1ce54..ed6a3944d2 100644 --- a/packages/client/ui-layout/tsconfig.json +++ b/packages/client/ui-layout/tsconfig.json @@ -8,6 +8,9 @@ "src" ], "references": [ + { + "path": "../../util/brand" + }, { "path": "../../../vendor/cordis" }, diff --git a/packages/client/ui-sidebar-right/src/client/contract/slots.ts b/packages/client/ui-sidebar-right/src/client/contract/slots.ts index 49709181c5..f964ee97c6 100644 --- a/packages/client/ui-sidebar-right/src/client/contract/slots.ts +++ b/packages/client/ui-sidebar-right/src/client/contract/slots.ts @@ -21,6 +21,7 @@ * therefore live with their declarer. */ import type {} from '@deepseek-ai/dsh-client-ui-slots' +import type { RightbarOwnerProps } from '@deepseek-ai/dsh-client-ui-layout/client' // The locale plugin's own merge carries the shared `common` vocabulary that the // lookup chain consults after this namespace misses. import type {} from '@deepseek-ai/dsh-client-locale/client' @@ -37,6 +38,8 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { } interface SlotMap { + /** Session content selected by the root-scoped right Sidebar controller. */ + 'rightbar.session': { kind: 'single'; scope: 'session'; owner: RightbarOwnerProps } /** * One tab's body, dispatched with the `id` of the type in force for * `tab.kind`. A tab type registers here under its definition's `id` and diff --git a/packages/client/ui-sidebar-right/src/client/index.ts b/packages/client/ui-sidebar-right/src/client/index.ts index 0971b9d21c..0fc4a4d2e2 100644 --- a/packages/client/ui-sidebar-right/src/client/index.ts +++ b/packages/client/ui-sidebar-right/src/client/index.ts @@ -32,6 +32,7 @@ import type {} from './contract/slots.ts' import { GuideBody, type GuideInjected } from './tabs/guide/GuideBody.tsx' import { ExpandButton } from './shell/ExpandButton.tsx' import { RightbarSeat, type SidebarRightInjected } from './shell/SidebarRight.tsx' +import { RightbarRoot } from './shell/RightbarRoot.tsx' import { createSidebarRightController, type SidebarRightController } from './service.ts' import { SidebarRightTabRegistry } from './tab-registry.ts' import { createSidebarRightStore } from './stores.ts' @@ -143,21 +144,27 @@ export function apply(ctx: ClientContext): void { } const disposeTypes = [tabs.register(guideDefinition(t))] - const disposeSeat = ctx.slots.inject('rightbar', () => ctx.slots.register({ - name: 'rightbar', - locale: NS, - children: { - 'sidebar.right.pane.tab': { kind: 'keyed', scope: 'session', inject: { hooks: { tabInfo: tabInfoFactory } } }, - 'sidebar.right.pane.tab.title': { kind: 'keyed', scope: 'session', inject: { hooks: { tabInfo: tabInfoFactory } } }, - 'sidebar.right.tab.menu.item': { kind: 'list', scope: 'session' }, - }, - store, - inject: (sessionId): SidebarRightInjected => ({ - ...injected, - keyedHooks: { tabNavigation: key => controller.tabDomain.occurrence(sessionId, { id: key as TabId }).navigation }, - occurrence: tab => controller.tabDomain.occurrence(sessionId, tab), - }), - }, RightbarSeat)) + const disposeSeat = ctx.slots.inject('rightbar', function* () { + yield ctx.slots.register({ + name: 'rightbar', + children: { 'rightbar.session': { kind: 'single', scope: 'session' } }, + }, RightbarRoot) + yield ctx.slots.register({ + name: 'rightbar.session', + locale: NS, + children: { + 'sidebar.right.pane.tab': { kind: 'keyed', scope: 'session', inject: { hooks: { tabInfo: tabInfoFactory } } }, + 'sidebar.right.pane.tab.title': { kind: 'keyed', scope: 'session', inject: { hooks: { tabInfo: tabInfoFactory } } }, + 'sidebar.right.tab.menu.item': { kind: 'list', scope: 'session' }, + }, + store, + inject: (sessionId): SidebarRightInjected => ({ + ...injected, + keyedHooks: { tabNavigation: key => controller.tabDomain.occurrence(sessionId, { id: key as TabId }).navigation }, + occurrence: tab => controller.tabDomain.occurrence(sessionId, tab), + }), + }, RightbarSeat) + }) // The expand button shares the panel's store: it only needs to know whether // the panel is expanded, and to ask for it to be. The header's corner seat // is its own place, past the utilities, so showing and hiding it moves diff --git a/packages/client/ui-sidebar-right/src/client/shell/RightbarRoot.tsx b/packages/client/ui-sidebar-right/src/client/shell/RightbarRoot.tsx new file mode 100644 index 0000000000..46420e5970 --- /dev/null +++ b/packages/client/ui-sidebar-right/src/client/shell/RightbarRoot.tsx @@ -0,0 +1,20 @@ +/** Root-scoped controller for the right Sidebar's Session content. */ +import type { PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' +import type {} from '../contract/slots.ts' + +/** + * Render the Session-bound Sidebar only while the Conversation is selected. + * @param props - frame geometry, panel selection, and the authorized Session renderer. + * @returns the current Session's right Sidebar, or no content for a global panel. + */ +export function RightbarRoot({ + usePanelInfo, SessionProvider, renderSlot, width, viewportWidth, canShow, +}: PropsRuntime<'rightbar'> & PropsRenderSlots<'rightbar.session'>) { + const visible = usePanelInfo(info => info.activePanelId === null) + if (!visible) return null + return ( + + {renderSlot('rightbar.session', { width, viewportWidth, canShow })} + + ) +} diff --git a/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.tsx b/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.tsx index ec0c9333e4..008ba4f387 100644 --- a/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.tsx +++ b/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.tsx @@ -33,8 +33,7 @@ import { createPortal } from 'react-dom' import type { HostObservable, InjectFace, PropsLocale, PropsRenderSlots, PropsRuntime, PropsStore, } from '@deepseek-ai/dsh-client-ui-slots' -// The frame declares the `rightbar` seat this component fills. -import type {} from '@deepseek-ai/dsh-client-ui-layout/client' +import type {} from '../contract/slots.ts' import type { DockIntents, DockMode, FloatRect, TabId, TabRecord, TabRenderer } from '@deepseek-ai/dsh-client-ui-dockkit' import { canSplit, dockPaneIds, DockSurface, findPaneContentTab, FloatLayer } from '@deepseek-ai/dsh-client-ui-dockkit' import type { HalvesFit, LayoutState, PaneId } from '@deepseek-ai/dsh-client-ui-dockkit' @@ -108,7 +107,7 @@ export interface SidebarRightInjected { /** The column seat's props: session scope, so the session arrives as a standard prop. */ export type RightbarSeatProps = - & PropsRuntime<'rightbar'> + & PropsRuntime<'rightbar.session'> & Children & Store & PropsLocale<'sidebarRight'> diff --git a/packages/client/ui-sidebar/package.json b/packages/client/ui-sidebar/package.json index a6f1b9d4a3..ffaa44eb90 100644 --- a/packages/client/ui-sidebar/package.json +++ b/packages/client/ui-sidebar/package.json @@ -52,6 +52,7 @@ "devDependencies": { "@deepseek-ai/dsh-api-workspace-controller": "workspace:^", "@deepseek-ai/dsh-client-locale": "workspace:^", + "@deepseek-ai/dsh-client-store": "workspace:^", "@deepseek-ai/dsh-client-test-runtime": "workspace:^", "@deepseek-ai/dsh-client-ui-layout": "workspace:^", "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", diff --git a/packages/client/ui-sidebar/src/client/SidebarRoot.module.css b/packages/client/ui-sidebar/src/client/SidebarRoot.module.css index 4ea10606e1..f899b50c53 100644 --- a/packages/client/ui-sidebar/src/client/SidebarRoot.module.css +++ b/packages/client/ui-sidebar/src/client/SidebarRoot.module.css @@ -58,13 +58,14 @@ from { opacity: 0; } } -/* At the 150ms rail settle, the four upper controls enter from the former +/* At the 150ms rail settle, the upper controls enter from the former rail right edge over the remaining 150ms of the AppFrame track transition. The bottom-pinned settings seat shares their opacity timeline but stays horizontally fixed. Only a live collapse gets .railIn; a cold collapsed render stays static. */ .railIn .iconButton, .railIn .newSession, +.railIn .panelList, .railIn .regionArea { animation: rail-in 150ms var(--ds-ease-in-out) backwards; } @@ -296,6 +297,74 @@ max-width: 0; } +.panelList { + flex: none; + display: flex; + flex-direction: column; + gap: 4px; + margin-bottom: 8px; +} + +.panelRow { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + min-height: 36px; + padding: 7px 8px; + box-sizing: border-box; + border: none; + border-radius: 8px; + background: transparent; + color: var(--dsw-alias-label-secondary); + font: inherit; + line-height: 22px; + text-align: left; + cursor: pointer; +} + +.panelRow:hover { + background: var(--dsw-alias-interactive-bg-hover); +} + +.panelRow.panelActive { + background: var(--dsw-alias-interactive-bg-active); + color: var(--dsw-alias-label-primary); + font-weight: 500; +} + +.panelRow:focus-visible { + outline: 2px solid var(--dsw-alias-label-primary); + outline-offset: -2px; +} + +.panelGlyph { + flex: none; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.panelTitle { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.collapsed .panelList { + gap: 12px; + margin-bottom: 12px; +} + +.collapsed .panelRow { + width: 36px; + height: 36px; + justify-content: center; + padding: 0; + color: var(--dsw-alias-label-primary); +} + /* Region seat: always mounted so the foot never moves. Its trailing margin cancels the wide shell inset so the nested scrollbar can sit at the sidebar edge; the browser restores that inset inside its own rows. */ @@ -351,6 +420,7 @@ .fading > *, .railIn .iconButton, .railIn .newSession, + .railIn .panelList, .railIn .footArea, .railIn .regionArea { transition: none; diff --git a/packages/client/ui-sidebar/src/client/SidebarRoot.tsx b/packages/client/ui-sidebar/src/client/SidebarRoot.tsx index 9dde97bf9e..94232d3043 100644 --- a/packages/client/ui-sidebar/src/client/SidebarRoot.tsx +++ b/packages/client/ui-sidebar/src/client/SidebarRoot.tsx @@ -1,12 +1,13 @@ /** - * Sidebar shell: column geometry only. Collapse is a slide plus crossfade: + * Sidebar shell: column geometry and global panel navigation. + * Collapse is a slide plus crossfade: * content freezes at its expanded width (inline style) and fades out in place * while the sliding column (AppFrame grid tracks) clips it — nothing reflows - * mid-slide. At settle the wide-only content unmounts and the four upper + * mid-slide. At settle the wide-only content unmounts and the upper * controls enter the 56px rail from the same horizontal offset (one icon each, * same top-down order) on one fade that ends with the slide. The bottom-pinned * settings control only fades. The workspace/session browsing region between - * the New Session button and the foot is the `sidebar.workspaces` registrant's, + * global panel rows and the foot is the `sidebar.workspaces` registrant's, * and the foot holds `sidebar.settings` plus `sidebar.footer.action`; the shell * hands them the wide flag (plus an expand request callback for the browser). * @@ -20,7 +21,10 @@ import clsx from 'clsx' import { FishLogo, IconNewChatOutline16, IconPanelLeftOutline16, Tooltip, } from '@deepseek-ai/dsh-client-ui-primitives' -import type { SidebarRootComponentProps } from './contract/slots.ts' +import type { InjectFace, PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' +import type { + SidebarPanelMetadata, SidebarRootComponentProps, SidebarRootInjected, SidebarSectionOwnerProps, +} from './contract/slots.ts' import css from './SidebarRoot.module.css' /** Wide-content unmount delay; matches the 150ms wide-content fade-out. */ @@ -44,6 +48,38 @@ function localBuildVersion(): string | undefined { + (process.env.DSH_CLIENT_GIT_DIRTY === 'true' ? '-dirty' : '') } +type PanelRowProps = + Pick + & Pick + & Pick, 'usePanelInfo'> + & Pick, 'selectPanel'> + & PropsRenderSlots<'sidebar.panellist' | 'sidebar.panellist.title'> + +/** Each panel row subscribes only to its own selection state. */ +function PanelRow({ id, label, wide, usePanelInfo, selectPanel, renderSlot }: PanelRowProps) { + const active = usePanelInfo(info => info.activePanelId === id) + return ( + + + + ) +} + /** * Render the sidebar column shell. * @param props - composed slot props (runtime share + injected callbacks, contract/slots.ts). @@ -54,9 +90,13 @@ export function SidebarRoot({ width, startSession, toggleSidebar, + selectPanel, + usePanels, + usePanelInfo, t, renderSlot, }: SidebarRootComponentProps) { + const panels = usePanels(snapshot => snapshot) // Wide content stays mounted while the collapse animates (fading via // .collapsed .wide), unmounts at settle, and remounts right away on expand. const [settled, setSettled] = useState(collapsed) @@ -199,6 +239,22 @@ export function SidebarRoot({ + {panels.length > 0 && ( + + )} + {/* The browsing region fills the column between the controls and the foot in both states; its rail icon column rides the same slot. */}
    diff --git a/packages/client/ui-sidebar/src/client/contract/slots.ts b/packages/client/ui-sidebar/src/client/contract/slots.ts index 4b4e2e1538..c3bac9a042 100644 --- a/packages/client/ui-sidebar/src/client/contract/slots.ts +++ b/packages/client/ui-sidebar/src/client/contract/slots.ts @@ -1,17 +1,16 @@ /** * Sidebar slot contract: the registrant-side props composition for the * layout-owned `sidebar` slot, plus the holes this shell declares. The shell - * owns column geometry (fold state machine, brand row, New Session); - * everything between the section header and the list bottom is the + * owns column geometry, the brand row, New Session, and global panel rows; + * everything between the workspace section header and the list bottom is the * `sidebar.workspaces` registrant's (ui-workspace), and the foot is the * `sidebar.settings` registrant's (ui-settings), followed by optional footer * actions in `sidebar.footer.action`. */ -import type { PropsLocale, PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' +import type { InjectFace, PropsLocale, PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' +import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store' import type { WorkspaceId } from '@deepseek-ai/dsh-api-workspace-controller/client' -// Type-only: pulls ui-layout's SlotMap merge (the 'sidebar' entry) into every -// program that sees this contract, so PropsRuntime<'sidebar'> resolves. -import type {} from '@deepseek-ai/dsh-client-ui-layout/client' +import type { MainPanelId } from '@deepseek-ai/dsh-client-ui-layout/client' declare module '@deepseek-ai/dsh-client-ui-slots' { interface SlotMap { @@ -26,6 +25,13 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { * package's `sidebar` entry; the shell supplies a generic text fallback. */ 'sidebar.brand.name': { kind: 'single'; scope: 'root'; owner: SidebarBrandNameOwnerProps } + /** + * Global panel icons. Each list id addresses the matching main panel; + * the sidebar owns the button and resolves its label from list metadata. + */ + 'sidebar.panellist': { kind: 'list'; scope: 'root'; owner: SidebarPanelIconOwnerProps } + /** Global panel titles keyed by list id, with the list label as fallback. */ + 'sidebar.panellist.title': { kind: 'keyed'; scope: 'root'; owner: SidebarPanelTitleOwnerProps } /** * The workspace/session browsing region: section header, search, the * grouped/flat session list, and every workspace dialog. Declared by this @@ -59,6 +65,30 @@ export interface SidebarBrandNameOwnerProps { children?: never } +/** Icon presentation supplied by the global panel row. */ +export interface SidebarPanelIconOwnerProps { + /** Requested square edge in pixels. */ + size: number + /** Whether this panel is selected in the main column. */ + active: boolean +} + +/** Title presentation supplied by the global panel row. */ +export interface SidebarPanelTitleOwnerProps { + /** Whether this panel is selected in the main column. */ + active: boolean +} + +/** Serializable metadata for one active global panel list registration. */ +export interface SidebarPanelMetadata { + /** List id and matching main panel key. */ + id: MainPanelId + /** Ascending row order; ties retain registration order. */ + order: number + /** Localized accessible name and fallback title. */ + label: string +} + /** * Owner share of the browser hole — the only facts crossing the shell/region * boundary. Business data and actions arrive through the region's own inject. @@ -87,8 +117,7 @@ export interface SidebarFooterActionOwnerProps { /** * Registrant-private injected share (arrives via the register inject - * factory). The shell keeps only its own controls: starting a Session from - * the New Session button and toggling the column. + * factory). The renderer binds the panel metadata source to usePanels. */ export type SidebarRootInjected = { /** @@ -99,20 +128,26 @@ export type SidebarRootInjected = { startSession: (workspaceId?: WorkspaceId) => void /** Toggle the sidebar column through the layout service. */ toggleSidebar: () => void + /** Select a global panel, or the conversation when null. */ + selectPanel: (id: MainPanelId | null) => void + /** Private reactive sources bound to framework selector hooks. */ + hooks: { panels: ObservableSnapshot } } /** * Full component props: layout owner state/actions plus the declared holes' * render shares, this package's injected callbacks, and the standard locale - * seat. No store is registered. + * seat. Panel metadata arrives through an injected observable. */ export type SidebarRootComponentProps = PropsRuntime<'sidebar'> & PropsRenderSlots< | 'sidebar.brand.mark' | 'sidebar.brand.name' + | 'sidebar.panellist' + | 'sidebar.panellist.title' | 'sidebar.workspaces' | 'sidebar.settings' | 'sidebar.footer.action' > - & SidebarRootInjected & PropsLocale<'sidebar'> + & InjectFace & PropsLocale<'sidebar'> diff --git a/packages/client/ui-sidebar/src/client/hello-world/HelloWorldPanel.module.css b/packages/client/ui-sidebar/src/client/hello-world/HelloWorldPanel.module.css new file mode 100644 index 0000000000..94f03cdb3f --- /dev/null +++ b/packages/client/ui-sidebar/src/client/hello-world/HelloWorldPanel.module.css @@ -0,0 +1,33 @@ +/* Root-scoped panel content; the layout owns its column dimensions. */ +.root { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; + min-height: 100%; + padding: 32px; + box-sizing: border-box; + color: var(--dsw-alias-label-primary); + text-align: center; +} + +.icon { + display: inline-flex; + color: var(--dsw-alias-label-secondary); +} + +.title { + margin: 0; + font-size: 28px; + font-weight: 600; + line-height: 36px; +} + +.description { + max-width: 480px; + margin: 0; + color: var(--dsw-alias-label-secondary); + font-size: 14px; + line-height: 22px; +} diff --git a/packages/client/ui-sidebar/src/client/hello-world/HelloWorldPanel.tsx b/packages/client/ui-sidebar/src/client/hello-world/HelloWorldPanel.tsx new file mode 100644 index 0000000000..1b35fa7ec2 --- /dev/null +++ b/packages/client/ui-sidebar/src/client/hello-world/HelloWorldPanel.tsx @@ -0,0 +1,29 @@ +/** Minimal global panel and its sidebar icon, composed through separate slots. */ +import { IconGlobeOutline14 } from '@deepseek-ai/dsh-client-ui-primitives' +import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' +import type {} from '@deepseek-ai/dsh-client-ui-layout/client' +import css from './HelloWorldPanel.module.css' + +/** + * Render the Hello World navigation icon; the sidebar owns the button and label. + * @param props - framework-composed panel icon props. + * @returns the globe glyph. + */ +export function HelloWorldIcon({ size }: PropsRuntime<'sidebar.panellist'>) { + return +} + +/** + * Render a root-scoped panel independent of the selected Session. + * @param props - main slot props and the sidebar locale seat. + * @returns the global panel content. + */ +export function HelloWorldPanel({ t }: PropsRuntime<'main'> & PropsLocale<'sidebar'>) { + return ( +
    + +

    {t('panel.helloWorld.title')}

    +

    {t('panel.helloWorld.description')}

    +
    + ) +} diff --git a/packages/client/ui-sidebar/src/client/index.ts b/packages/client/ui-sidebar/src/client/index.ts index 2103f133e2..91bfb40818 100644 --- a/packages/client/ui-sidebar/src/client/index.ts +++ b/packages/client/ui-sidebar/src/client/index.ts @@ -1,31 +1,38 @@ -/** Registers the sidebar shell into the layout-owned slot. */ +/** Registers the sidebar shell, global panel navigation, and Hello World panel. */ import type { Context as ClientContext } from '@deepseek-ai/cordis' +import { createSnapshotStore } from '@deepseek-ai/dsh-client-store' +import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots' +import type { MainPanelId } from '@deepseek-ai/dsh-client-ui-layout/client' // Type-only: pulls the locale plugin's Context merge (ctx.locale). import type {} from '@deepseek-ai/dsh-client-locale/client' // Type-only: pulls the SlotRegistry service merge (ctx.slots). import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' // Type-only: pulls the Session root standard-props merge. import type {} from '@deepseek-ai/dsh-client-ui-session/client' -import type { SidebarRootInjected } from './contract/slots.ts' +import type { SidebarPanelMetadata, SidebarRootInjected } from './contract/slots.ts' +import { HelloWorldIcon, HelloWorldPanel } from './hello-world/HelloWorldPanel.tsx' import { SidebarRoot } from './SidebarRoot.tsx' import { en, zh, type SidebarKey } from './locales.ts' export type { SidebarBrandMarkOwnerProps, SidebarBrandNameOwnerProps, SidebarFooterActionOwnerProps, + SidebarPanelIconOwnerProps, SidebarPanelMetadata, SidebarPanelTitleOwnerProps, SidebarRootComponentProps, SidebarRootInjected, SidebarSectionOwnerProps, SidebarSettingsOwnerProps, } from './contract/slots.ts' export type { SidebarKey } from './locales.ts' declare module '@deepseek-ai/dsh-client-ui-slots' { interface LocaleNamespaceMap { - /** Sidebar shell controls copy. */ + /** Sidebar controls and global panel copy. */ sidebar: SidebarKey } } -/** Dictionary namespace owned by this plugin (shell controls copy). */ +/** Dictionary namespace owned by this plugin. */ const NS = 'sidebar' +const HELLO_WORLD_PANEL_ID = 'hello-world' as MainPanelId + interface WorkspaceNavigation { startSession(workspaceId?: Parameters[0]): void } @@ -39,29 +46,56 @@ export const inject = ['slots', 'layout', 'uiWorkspace', 'locale'] export function apply(ctx: ClientContext): void { const workspaceNavigation = ctx.get('uiWorkspace') as unknown as WorkspaceNavigation ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-sidebar: dictionaries') + const t = ctx.locale.bind(NS) + const panels = createSnapshotStore([]) + const syncPanels = (): void => { + const next = ctx.slots.entriesOfSlot('sidebar.panellist').map(({ options }) => { + // The list registration requires an id; StoredEntry erases the slot kind. + const id = options.id as MainPanelId + return { id, order: options.order ?? 0, label: resolveSlotLabel(options.label) ?? id } + }).sort((a, b) => a.order - b.order) + const previous = panels.getSnapshot() + if (previous.length === next.length && previous.every((panel, index) => { + const candidate = next[index]! + return panel.id === candidate.id && panel.order === candidate.order && panel.label === candidate.label + })) return + panels.set(next) + } + ctx.effect(() => ctx.slots.subscribe('sidebar.panellist', syncPanels), 'ui-sidebar: panel entries') + ctx.effect(() => ctx.locale.subscribe(syncPanels), 'ui-sidebar: panel labels') const injectProps = (): SidebarRootInjected => ({ // The shell's New Session button rides the Workspace UI's shared action // (current Session Workspace, then recent Workspace). startSession: (workspaceId) => { workspaceNavigation.startSession(workspaceId) }, toggleSidebar: () => { ctx.layout.toggleSidebar() }, + selectPanel: (id) => { ctx.layout.selectPanel(id) }, + hooks: { panels }, }) - ctx.effect( - () => ctx.slots.register({ - name: 'sidebar', - locale: NS, - // The shell owns geometry; ui-workspace registers the whole browsing - // region (header, search, session list, workspace dialogs), ui-settings - // registers the foot trigger + settings panel. - children: { - 'sidebar.brand.mark': { kind: 'single', scope: 'root' }, - 'sidebar.brand.name': { kind: 'single', scope: 'root' }, - 'sidebar.workspaces': { kind: 'single', scope: 'root' }, - 'sidebar.settings': { kind: 'single', scope: 'root' }, - 'sidebar.footer.action': { kind: 'list', scope: 'root' }, - }, - inject: injectProps, - }, SidebarRoot), - 'ui-sidebar: slot registration', - ) + ctx.slots.inject('sidebar', () => ctx.slots.register({ + name: 'sidebar', + locale: NS, + children: { + 'sidebar.brand.mark': { kind: 'single', scope: 'root' }, + 'sidebar.brand.name': { kind: 'single', scope: 'root' }, + 'sidebar.panellist': { kind: 'list', scope: 'root' }, + 'sidebar.panellist.title': { kind: 'keyed', scope: 'root' }, + 'sidebar.workspaces': { kind: 'single', scope: 'root' }, + 'sidebar.settings': { kind: 'single', scope: 'root' }, + 'sidebar.footer.action': { kind: 'list', scope: 'root' }, + }, + inject: injectProps, + }, SidebarRoot)) + ctx.slots.inject('main', () => ctx.slots.register({ + name: 'main', + key: HELLO_WORLD_PANEL_ID, + locale: NS, + }, HelloWorldPanel)) + ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({ + name: 'sidebar.panellist', + id: HELLO_WORLD_PANEL_ID, + order: 100, + label: () => t('panel.helloWorld.title'), + }, HelloWorldIcon)) + syncPanels() } diff --git a/packages/client/ui-sidebar/src/client/locales.ts b/packages/client/ui-sidebar/src/client/locales.ts index 8cf5ac6d7b..e0df89de4c 100644 --- a/packages/client/ui-sidebar/src/client/locales.ts +++ b/packages/client/ui-sidebar/src/client/locales.ts @@ -1,4 +1,4 @@ -/** `sidebar` namespace dictionaries: shell controls (brand row, New Session, fold toggle). */ +/** `sidebar` namespace dictionaries for shell controls and global panels. */ /** Simplified Chinese dictionary (the key-set source of truth). */ export const zh = { @@ -6,6 +6,9 @@ export const zh = { 'session.new.label': '新建会话', 'toggle.open': '打开侧边栏', 'toggle.collapse': '收起侧边栏', + 'panels.label': '全局面板', + 'panel.helloWorld.title': 'Hello World', + 'panel.helloWorld.description': '这是一个全局面板,不属于任何会话。', } satisfies Record /** The sidebar namespace key union. */ @@ -17,4 +20,7 @@ export const en = { 'session.new.label': 'New session', 'toggle.open': 'Open sidebar', 'toggle.collapse': 'Collapse sidebar', + 'panels.label': 'Global panels', + 'panel.helloWorld.title': 'Hello World', + 'panel.helloWorld.description': 'This global panel is independent of any session.', } satisfies Record diff --git a/packages/client/ui-sidebar/tsconfig.json b/packages/client/ui-sidebar/tsconfig.json index ed840c93a4..5d20bd9741 100644 --- a/packages/client/ui-sidebar/tsconfig.json +++ b/packages/client/ui-sidebar/tsconfig.json @@ -17,6 +17,9 @@ { "path": "../ui-slots" }, + { + "path": "../store" + }, { "path": "../ui-primitives" }, diff --git a/packages/client/ui-workspace/package.json b/packages/client/ui-workspace/package.json index 72876b6d9e..36a6565a1d 100644 --- a/packages/client/ui-workspace/package.json +++ b/packages/client/ui-workspace/package.json @@ -34,6 +34,7 @@ "@deepseek-ai/dsh-client-connection", "@deepseek-ai/dsh-client-locale", "@deepseek-ai/dsh-client-ui-conversation", + "@deepseek-ai/dsh-client-ui-layout", "@deepseek-ai/dsh-client-ui-renderer", "@deepseek-ai/dsh-client-ui-session", "@deepseek-ai/dsh-client-ui-sidebar" @@ -61,6 +62,7 @@ "@deepseek-ai/dsh-client-store": "workspace:^", "@deepseek-ai/dsh-client-test-runtime": "workspace:^", "@deepseek-ai/dsh-client-ui-conversation": "workspace:^", + "@deepseek-ai/dsh-client-ui-layout": "workspace:^", "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", "@deepseek-ai/dsh-client-ui-renderer": "workspace:^", "@deepseek-ai/dsh-client-ui-session": "workspace:^", diff --git a/packages/client/ui-workspace/src/client/index.ts b/packages/client/ui-workspace/src/client/index.ts index 7c8e1108dc..0633e96b85 100644 --- a/packages/client/ui-workspace/src/client/index.ts +++ b/packages/client/ui-workspace/src/client/index.ts @@ -20,6 +20,7 @@ import type {} from '@deepseek-ai/dsh-api-workspace-controller/client' import type {} from '@deepseek-ai/dsh-client-locale/client' // Type-only: pulls the SlotRegistry service merge (ctx.slots). import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' +import type {} from '@deepseek-ai/dsh-client-ui-layout/client' // Type-only: pulls the Session root standard-hook merge. import type {} from '@deepseek-ai/dsh-client-ui-session/client' import type { WorkspaceBrowserInjected, WorkspacePickerInjected } from './contract/slots.ts' @@ -60,7 +61,7 @@ const NS = 'workspace' * declaration through `slots.inject()` instead of assuming order. */ export const inject = [ - 'slots', 'sessions', 'workspaces', 'locale', 'remote', 'remote.directoryPicker', + 'slots', 'sessions', 'workspaces', 'locale', 'remote', 'remote.directoryPicker', 'layout', ] /** @@ -95,11 +96,14 @@ export function apply(ctx: Context): void { subscribe: listener => ctx.on('connection/reset', listener), } const pickerFlowSource = flowSource('conversation.hero.workspace.directoryFlow') + const openSession: WorkspaceBrowserInjected['open'] = (sessionId) => { + uiWorkspace.openSession(sessionId) + } const browserInjected = (): WorkspaceBrowserInjected => ({ // Explicit group actions keep their target; unscoped New Session inherits // the current Session Workspace before the recent-Workspace fallback. startSession: (workspaceId) => { uiWorkspace.startSession(workspaceId) }, - open: (sessionId) => { sessions.open(sessionId) }, + open: openSession, searchSessions, searchResultLimit: sessions.searchResultLimit, renameSession: async (sessionId, title) => { @@ -112,7 +116,7 @@ export function apply(ctx: Context): void { }, forkSession: (sessionId) => { sessions.fork({ sessionId, increaseTitle: true }) - .then((childId) => { sessions.open(childId) }) + .then(openSession) .catch(() => { // Fork or child-rename failure keeps the current selection. }) diff --git a/packages/client/ui-workspace/src/client/navigation.ts b/packages/client/ui-workspace/src/client/navigation.ts index a460afe23a..c9dcc5e984 100644 --- a/packages/client/ui-workspace/src/client/navigation.ts +++ b/packages/client/ui-workspace/src/client/navigation.ts @@ -10,9 +10,15 @@ import type { IWorkspaces, WorkspaceId, WorkspaceView, } from '@deepseek-ai/dsh-api-workspace-controller/client' import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type {} from '@deepseek-ai/dsh-client-ui-layout/client' /** Workspace archive and directory operations consumed by Client UI domains. */ export interface UiWorkspace { + /** + * Select a Session and show its Conversation as one UI navigation action. + * @param sessionId - listed or retained Session to display. + */ + openSession(sessionId: SessionId): void /** * Resolve the reusable or newly created blank Session for a Workspace. * @param workspaceId - target Workspace. @@ -111,6 +117,11 @@ class UiWorkspaceService extends Service implements UiWorkspace { return attempt } + openSession(sessionId: SessionId): void { + this.sessions.open(sessionId) + this.ctx.layout.selectPanel(null) + } + startSession(workspaceId?: WorkspaceId): void { const workspace = this.workspaces.list.getSnapshot() const sessions = this.sessions.list.getSnapshot() @@ -124,10 +135,11 @@ class UiWorkspaceService extends Service implements UiWorkspace { const target = workspaceId ?? currentWorkspaceId ?? recent if (target === undefined) { this.sessions.clear() + this.ctx.layout.selectPanel(null) return } void this.connectWorkspace(target).then( - (sessionId) => { this.sessions.open(sessionId) }, + (sessionId) => { this.openSession(sessionId) }, (reason: unknown) => { console.warn('new session failed:', reason) }, ) } diff --git a/packages/client/ui-workspace/src/client/rows/WorkspaceBrowser.tsx b/packages/client/ui-workspace/src/client/rows/WorkspaceBrowser.tsx index 0ef21ddb82..585e9c8766 100644 --- a/packages/client/ui-workspace/src/client/rows/WorkspaceBrowser.tsx +++ b/packages/client/ui-workspace/src/client/rows/WorkspaceBrowser.tsx @@ -235,7 +235,7 @@ function workspaceGroupHalf(e: { clientY: number; currentTarget: HTMLElement }): type SessionTreeProps = Pick< WorkspaceBrowserProps, 'useSessions' | 'useSessionPendingInteraction' | 'startSession' | 'open' | 'forkSession' - | 'insertWorkspaceBefore' | 'insertSessionBefore' | 't' + | 'insertWorkspaceBefore' | 'insertSessionBefore' | 't' | 'usePanelInfo' > & { /** Host account home for POSIX hover-path abbreviation. */ home?: string | undefined @@ -275,16 +275,17 @@ type SessionTreeProps = Pick< /** The scrolling session tree; unmounting drops the sessions subscription and expand-all state. */ function SessionTree({ useSessions, useSessionPendingInteraction, startSession, open, forkSession, workspaces, archivedSessionIds, - workspaceReady, + workspaceReady, usePanelInfo, onRenameRequest, onDeleteRequest, onSessionRename, onSessionArchive, insertWorkspaceBefore, insertSessionBefore, orderBy, groupExpansion, setGroupExpanded, sessionOrderByAccount, sessionUpdatedAtByAccount, syncSessionOrderAccount, setSessionOrder, home, t, revealSessionId, onSessionRevealed, }: SessionTreeProps) { + const panelActive = usePanelInfo(info => info.activePanelId !== null) const list = useSessions(s => s) const pendingInteractions = useSessionPendingInteraction(s => s) - const current = list.current + const current = panelActive ? undefined : list.current const revealGroup = revealSessionId === undefined || !workspaceReady ? undefined : owningGroupKey(workspaces, revealSessionId) @@ -619,7 +620,7 @@ function SessionTree({ /** The flat "In one list" body: every session is one draggable top-level row. */ function FlatList({ useSessions, useSessionPendingInteraction, open, forkSession, onSessionRename, onSessionArchive, - archivedSessionIds, + archivedSessionIds, usePanelInfo, orderBy, sessionOrderByAccount, sessionUpdatedAtByAccount, syncSessionOrderAccount, setSessionOrder, revealSessionId, onSessionRevealed, t, }: Pick< @@ -631,6 +632,7 @@ function FlatList({ | 'onSessionRename' | 'onSessionArchive' | 'archivedSessionIds' + | 'usePanelInfo' | 'orderBy' | 'sessionOrderByAccount' | 'sessionUpdatedAtByAccount' @@ -640,6 +642,7 @@ function FlatList({ | 'onSessionRevealed' | 't' >) { + const panelActive = usePanelInfo(info => info.activePanelId !== null) const list = useSessions(s => s) const pendingInteractions = useSessionPendingInteraction(s => s) const baseRows = useMemo( @@ -706,7 +709,7 @@ function FlatList({ & { +}: Pick & { workspaces: readonly WorkspaceView[] archivedSessionIds: readonly SessionNode['id'][] query: string remote: RemoteSearchState resultLimit: number }) { + const panelActive = usePanelInfo(info => info.activePanelId !== null) const list = useSessions(s => s) const pendingInteractions = useSessionPendingInteraction(s => s) const currentRemote = remote.query === query @@ -798,7 +803,7 @@ function SearchResults({ @@ -833,6 +838,7 @@ function SearchResults({ */ export function WorkspaceBrowser({ wide, + usePanelInfo, expandSidebar, useSessions, useSessionPendingInteraction, @@ -1251,6 +1257,7 @@ export function WorkspaceBrowser({ {wide && (normalizedQuery !== '' ? ( ', - 'useSessions: UseSessions', - 'useSessionPendingInteraction: UseSessionPendingInteraction', - 'useWorkspaces: SnapshotSelectorHook', - 'useConversation: MaybeSnapshotSelectorHook', - 'useInput: MaybeSnapshotSelectorHook', - 'inputActions: InputActions | undefined', - 'useSession: MaybeSnapshotSelectorHook', - 'sessionId: SessionId | undefined', - 'useProjection: UseProjection', - ], - keyDomain: '', - hookContext: '', - slotInject: '', - declaredBy: 'an entry in \'root\' (client-ui-layout), so it exists while that entry is mounted', - occupants: [ - 'client-ui-conversation ConversationRoot', - ], - replaceRisk: 'shadows-shipped-ui', - example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation\', () => ctx.slots.register(\n { name: \'conversation\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-layout/src/client/index.ts:65', - }, { key: 'conversation.approval.detail', kind: 'single', @@ -128,6 +93,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -186,6 +152,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -234,6 +201,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -283,6 +251,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -345,6 +314,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -393,6 +363,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -408,7 +379,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ keyDomain: '', hookContext: '', slotInject: '', - declaredBy: 'an entry in \'conversation\' (client-ui-conversation), so it exists while that entry is mounted', + declaredBy: 'an entry in \'main.conversation\' (client-ui-conversation), so it exists while that entry is mounted', occupants: [ 'client-ui-approval ApprovalPanel', 'client-ui-subagent SubagentReadOnlyComposer', @@ -416,7 +387,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.composer\', () => ctx.slots.register(\n { name: \'conversation.composer\', select: owner => null },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-conversation/src/client/contract/slots.ts:157', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:159', }, { key: 'conversation.composer.bar', @@ -434,6 +405,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -447,13 +419,13 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ keyDomain: '', hookContext: '', slotInject: '', - declaredBy: 'an entry in \'conversation\' (client-ui-conversation), so it exists while that entry is mounted', + declaredBy: 'an entry in \'main.conversation\' (client-ui-conversation), so it exists while that entry is mounted', occupants: [ 'client-ui-conversation InputBar', ], replaceRisk: 'shadows-shipped-ui', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.composer.bar\', () => ctx.slots.register(\n { name: \'conversation.composer.bar\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-conversation/src/client/contract/slots.ts:175', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:177', }, { key: 'conversation.composer.dock', @@ -486,6 +458,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -507,7 +480,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.composer.dock\', () => ctx.slots.register(\n { name: \'conversation.composer.dock\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-conversation/src/client/contract/slots.ts:169', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:171', }, { key: 'conversation.hero.agentPreset', @@ -523,6 +496,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -530,13 +504,13 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ keyDomain: '', hookContext: '', slotInject: '', - declaredBy: 'an entry in \'conversation\' (client-ui-conversation), so it exists while that entry is mounted', + declaredBy: 'an entry in \'main.conversation\' (client-ui-conversation), so it exists while that entry is mounted', occupants: [ 'client-ui-agent-preset AgentPresetSeat', ], replaceRisk: 'shadows-shipped-ui', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.hero.agentPreset\', () => ctx.slots.register(\n { name: \'conversation.hero.agentPreset\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-conversation/src/client/contract/slots.ts:163', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:165', }, { key: 'conversation.hero.brand.mark', @@ -552,6 +526,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -559,11 +534,11 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ keyDomain: '', hookContext: '', slotInject: '', - declaredBy: 'an entry in \'conversation\' (client-ui-conversation), so it exists while that entry is mounted', + declaredBy: 'an entry in \'main.conversation\' (client-ui-conversation), so it exists while that entry is mounted', occupants: [], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.hero.brand.mark\', () => ctx.slots.register(\n { name: \'conversation.hero.brand.mark\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-conversation/src/client/contract/slots.ts:161', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:163', }, { key: 'conversation.hero.workspace', @@ -581,6 +556,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -588,13 +564,13 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ keyDomain: '', hookContext: '', slotInject: '', - declaredBy: 'an entry in \'conversation\' (client-ui-conversation), so it exists while that entry is mounted', + declaredBy: 'an entry in \'main.conversation\' (client-ui-conversation), so it exists while that entry is mounted', occupants: [ 'client-ui-workspace WorkspacePicker', ], replaceRisk: 'shadows-shipped-ui', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.hero.workspace\', () => ctx.slots.register(\n { name: \'conversation.hero.workspace\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-conversation/src/client/contract/slots.ts:159', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:161', }, { key: 'conversation.hero.workspace.directoryFlow', @@ -610,6 +586,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -644,6 +621,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -663,7 +641,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'shadows-shipped-ui', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.input.attachments\', () => ctx.slots.register(\n { name: \'conversation.input.attachments\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-conversation/src/client/contract/slots.ts:177', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:179', }, { key: 'conversation.input.dock', @@ -701,6 +679,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -716,7 +695,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ keyDomain: '', hookContext: '', slotInject: '', - declaredBy: 'an entry in \'conversation\' (client-ui-conversation), so it exists while that entry is mounted', + declaredBy: 'an entry in \'main.conversation\' (client-ui-conversation), so it exists while that entry is mounted', occupants: [ 'client-ui-conversation QueueDock id \'queue\'', 'client-ui-conversation TodoDock id \'todo\'', @@ -724,7 +703,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.input.dock\', () => ctx.slots.register(\n { name: \'conversation.input.dock\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-conversation/src/client/contract/slots.ts:165', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:167', }, { key: 'conversation.input.left', @@ -757,6 +736,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -776,7 +756,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ occupants: [], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.input.left\', () => ctx.slots.register(\n { name: \'conversation.input.left\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-conversation/src/client/contract/slots.ts:171', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:173', }, { key: 'conversation.input.model', @@ -792,6 +772,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -813,7 +794,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'shadows-shipped-ui', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.input.model\', () => ctx.slots.register(\n { name: \'conversation.input.model\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-conversation/src/client/contract/slots.ts:185', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:187', }, { key: 'conversation.input.overlay', @@ -846,6 +827,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -868,7 +850,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.input.overlay\', () => ctx.slots.register(\n { name: \'conversation.input.overlay\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-conversation/src/client/contract/slots.ts:167', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:169', }, { key: 'conversation.input.plan', @@ -884,6 +866,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -905,7 +888,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'shadows-shipped-ui', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.input.plan\', () => ctx.slots.register(\n { name: \'conversation.input.plan\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-conversation/src/client/contract/slots.ts:183', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:185', }, { key: 'conversation.input.right', @@ -938,6 +921,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -957,7 +941,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ occupants: [], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.input.right\', () => ctx.slots.register(\n { name: \'conversation.input.right\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-conversation/src/client/contract/slots.ts:173', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:175', }, { key: 'conversation.message.images', @@ -977,6 +961,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -1012,6 +997,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -1027,13 +1013,13 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ keyDomain: '', hookContext: '', slotInject: '', - declaredBy: 'an entry in \'conversation\' (client-ui-conversation), so it exists while that entry is mounted', + declaredBy: 'an entry in \'main.conversation\' (client-ui-conversation), so it exists while that entry is mounted', occupants: [ 'client-ui-conversation ConversationSession', ], replaceRisk: 'shadows-shipped-ui', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.session\', () => ctx.slots.register(\n { name: \'conversation.session\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-conversation/src/client/contract/slots.ts:121', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:123', }, { key: 'conversation.session.header', @@ -1047,6 +1033,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -1062,13 +1049,13 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ keyDomain: '', hookContext: '', slotInject: '', - declaredBy: 'an entry in \'conversation\' (client-ui-conversation), so it exists while that entry is mounted', + declaredBy: 'an entry in \'main.conversation\' (client-ui-conversation), so it exists while that entry is mounted', occupants: [ 'client-ui-conversation ConversationSessionHeader', ], replaceRisk: 'shadows-shipped-ui', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.session.header\', () => ctx.slots.register(\n { name: \'conversation.session.header\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-conversation/src/client/contract/slots.ts:123', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:125', }, { key: 'conversation.session.header.actions', @@ -1103,6 +1090,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -1127,7 +1115,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.session.header.actions\', () => ctx.slots.register(\n { name: \'conversation.session.header.actions\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-conversation/src/client/contract/slots.ts:131', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:133', }, { key: 'conversation.session.header.corner', @@ -1143,6 +1131,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -1164,7 +1153,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'shadows-shipped-ui', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.session.header.corner\', () => ctx.slots.register(\n { name: \'conversation.session.header.corner\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-conversation/src/client/contract/slots.ts:149', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:151', }, { key: 'conversation.session.header.lineage', @@ -1182,6 +1171,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -1203,7 +1193,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'shadows-shipped-ui', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.session.header.lineage\', () => ctx.slots.register(\n { name: \'conversation.session.header.lineage\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-conversation/src/client/contract/slots.ts:125', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:127', }, { key: 'conversation.session.header.utilities', @@ -1238,6 +1228,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -1260,7 +1251,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.session.header.utilities\', () => ctx.slots.register(\n { name: \'conversation.session.header.utilities\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-conversation/src/client/contract/slots.ts:137', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:139', }, { key: 'conversation.trajectory.images', @@ -1280,6 +1271,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -1338,6 +1330,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -1360,14 +1353,84 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.view\', () => ctx.slots.register(\n { name: \'conversation.view\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-conversation/src/client/contract/slots.ts:155', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:157', + }, + { + key: 'main', + kind: 'keyed', + scope: 'root', + summary: 'Central panel selected by sidebar entry id.', + doc: 'Central panel selected by sidebar entry id. The reserved `conversation`\nkey hosts the Conversation; other keys receive no Session binding.', + registerOptions: [ + { + name: 'key', + requirement: 'required', + type: 'string', + doc: 'Your cell key: the entry renders where the owner dispatches this exact key. Registering an already-occupied key replaces that occupant.', + }, + ], + ownerProps: [], + ownerPropsReferences: [], + standardProps: [ + 'useResource: UseResource', + 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', + 'useSessions: UseSessions', + 'useSessionPendingInteraction: UseSessionPendingInteraction', + 'useWorkspaces: SnapshotSelectorHook', + ], + keyDomain: 'open: any string the owner dispatches (no compile-time key set), already taken: conversation', + hookContext: '', + slotInject: '', + declaredBy: 'an entry in \'root\' (client-ui-layout), so it exists while that entry is mounted', + occupants: [ + 'client-ui-conversation ConversationPanel key \'conversation\'', + 'client-ui-sidebar HelloWorldPanel', + ], + replaceRisk: 'shadows-shipped-ui', + example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'main\', () => ctx.slots.register(\n { name: \'main\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', + source: 'packages/client/ui-layout/src/client/index.ts:66', + }, + { + key: 'main.conversation', + kind: 'single', + scope: 'session-maybe', + summary: 'Conversation shell beneath its root-scoped main-panel entry.', + doc: 'Conversation shell beneath its root-scoped main-panel entry.', + registerOptions: [], + ownerProps: [], + ownerPropsReferences: [], + standardProps: [ + 'useResource: UseResource', + 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', + 'useSessions: UseSessions', + 'useSessionPendingInteraction: UseSessionPendingInteraction', + 'useWorkspaces: SnapshotSelectorHook', + 'useConversation: MaybeSnapshotSelectorHook', + 'useInput: MaybeSnapshotSelectorHook', + 'inputActions: InputActions | undefined', + 'useSession: MaybeSnapshotSelectorHook', + 'sessionId: SessionId | undefined', + 'useProjection: UseProjection', + ], + keyDomain: '', + hookContext: '', + slotInject: '', + declaredBy: 'an entry in \'main\' (client-ui-conversation), so it exists while that entry is mounted', + occupants: [ + 'client-ui-conversation ConversationRoot', + ], + replaceRisk: 'shadows-shipped-ui', + example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'main.conversation\', () => ctx.slots.register(\n { name: \'main.conversation\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:121', }, { key: 'rightbar', kind: 'single', - scope: 'session', + scope: 'root', summary: 'The right column: a track the centre makes room for, or nothing.', - doc: 'The right column: a track the centre makes room for, or nothing. OCCUPIED\nby the right Sidebar, which uses the resolved column width in normal\nmode and covers the viewport in fullscreen, retaining the wide-screen\ncolumn reservation underneath.\n\nWhether the panel is shown, and whether it takes a track, is the\noccupant\'s own recorded business — it reports the composition of its\nexpanded and presentation state through `ctx.layout`, and the frame sizes\nthe track and places the resize handle from that. The expand control is\nnot this column\'s: it is a button in the conversation header. With no\ncurrent session nothing is mounted here.', + doc: 'The right column: a track the centre makes room for, or nothing. OCCUPIED\nby the right Sidebar, which uses the resolved column width in normal\nmode and covers the viewport in fullscreen, retaining the wide-screen\ncolumn reservation underneath.\n\nWhether the panel is shown, and whether it takes a track, is the\noccupant\'s own recorded business — it reports the composition of its\nexpanded and presentation state through `ctx.layout`, and the frame sizes\nthe track and places the resize handle from that. The expand control is\nnot this column\'s: it is a button in the conversation header. The root\noccupant decides when to render its Session-bound content.', registerOptions: [], ownerProps: [ '/** Right column owner share: resolved normal geometry and opening eligibility. */\nexport interface RightbarOwnerProps {\n /** Resolved normal panel width in px, not the saved preference; zero if it cannot fit. */\n width: number\n /** Current frame width in px. */\n viewportWidth: number\n /**\n * Whether a normal right panel can retain 300px beside a 400px center.\n * Before a narrow opening, includes the space from collapsing the left sidebar.\n */\n canShow: boolean\n}', @@ -1376,6 +1439,37 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', + 'useSessions: UseSessions', + 'useSessionPendingInteraction: UseSessionPendingInteraction', + 'useWorkspaces: SnapshotSelectorHook', + ], + keyDomain: '', + hookContext: '', + slotInject: '', + declaredBy: 'an entry in \'root\' (client-ui-layout), so it exists while that entry is mounted', + occupants: [ + 'client-ui-sidebar-right RightbarRoot', + ], + replaceRisk: 'shadows-shipped-ui', + example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'rightbar\', () => ctx.slots.register(\n { name: \'rightbar\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', + source: 'packages/client/ui-layout/src/client/index.ts:80', + }, + { + key: 'rightbar.session', + kind: 'single', + scope: 'session', + summary: 'Session content selected by the root-scoped right Sidebar controller.', + doc: 'Session content selected by the root-scoped right Sidebar controller.', + registerOptions: [], + ownerProps: [ + '/** Right column owner share: resolved normal geometry and opening eligibility. */\nexport interface RightbarOwnerProps {\n /** Resolved normal panel width in px, not the saved preference; zero if it cannot fit. */\n width: number\n /** Current frame width in px. */\n viewportWidth: number\n /**\n * Whether a normal right panel can retain 300px beside a 400px center.\n * Before a narrow opening, includes the space from collapsing the left sidebar.\n */\n canShow: boolean\n}', + ], + ownerPropsReferences: [], + standardProps: [ + 'useResource: UseResource', + 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -1391,13 +1485,13 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ keyDomain: '', hookContext: '', slotInject: '', - declaredBy: 'an entry in \'root\' (client-ui-layout), so it exists while that entry is mounted', + declaredBy: 'an entry in \'rightbar\' (client-ui-sidebar-right), so it exists while that entry is mounted', occupants: [ 'client-ui-sidebar-right RightbarSeat', ], replaceRisk: 'shadows-shipped-ui', - example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'rightbar\', () => ctx.slots.register(\n { name: \'rightbar\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-layout/src/client/index.ts:79', + example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'rightbar.session\', () => ctx.slots.register(\n { name: \'rightbar.session\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', + source: 'packages/client/ui-sidebar-right/src/client/contract/slots.ts:42', }, { key: 'root', @@ -1413,6 +1507,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -1461,6 +1556,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -1490,6 +1586,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -1538,6 +1635,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -1572,6 +1670,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -1620,6 +1719,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -1656,6 +1756,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -1702,6 +1803,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -1739,6 +1841,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -1790,6 +1893,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -1839,6 +1943,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -1871,6 +1976,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -1917,6 +2023,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -1928,7 +2035,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ occupants: [], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'shell.overlay\', () => ctx.slots.register(\n { name: \'shell.overlay\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-layout/src/client/index.ts:90', + source: 'packages/client/ui-layout/src/client/index.ts:91', }, { key: 'sidebar', @@ -1944,6 +2051,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -1957,7 +2065,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'shadows-shipped-ui', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar\', () => ctx.slots.register(\n { name: \'sidebar\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-layout/src/client/index.ts:52', + source: 'packages/client/ui-layout/src/client/index.ts:61', }, { key: 'sidebar.brand.mark', @@ -1973,6 +2081,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -1986,7 +2095,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'shadows-shipped-ui', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.brand.mark\', () => ctx.slots.register(\n { name: \'sidebar.brand.mark\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:23', + source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:22', }, { key: 'sidebar.brand.name', @@ -2002,6 +2111,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -2015,7 +2125,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'shadows-shipped-ui', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.brand.name\', () => ctx.slots.register(\n { name: \'sidebar.brand.name\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:28', + source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:27', }, { key: 'sidebar.footer.action', @@ -2050,6 +2160,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -2063,7 +2174,91 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.footer.action\', () => ctx.slots.register(\n { name: \'sidebar.footer.action\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:46', + source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:52', + }, + { + key: 'sidebar.panellist', + kind: 'list', + scope: 'root', + summary: 'Global panel icons.', + doc: 'Global panel icons. Each list id addresses the matching main panel;\nthe sidebar owns the button and resolves its label from list metadata.', + registerOptions: [ + { + name: 'id', + requirement: 'required', + type: 'string', + doc: 'Your cell key. Use an id of your own: a fresh id is added beside the shipped entries, while reusing a shipped id puts you in THAT cell and replaces it. Owners that filter by id address you by it.', + }, + { + name: 'order', + requirement: 'optional', + type: 'number', + doc: 'Position among the entries, ascending (default 0).', + }, + { + name: 'label', + requirement: 'optional', + type: 'string | (() => string)', + doc: 'Display text where the owner projects one (nav rows, tabs). A thunk is re-read on every projection, so localized text follows the active locale without re-registering.', + }, + ], + ownerProps: [ + '/** Icon presentation supplied by the global panel row. */\nexport interface SidebarPanelIconOwnerProps {\n /** Requested square edge in pixels. */\n size: number\n /** Whether this panel is selected in the main column. */\n active: boolean\n}', + ], + ownerPropsReferences: [], + standardProps: [ + 'useResource: UseResource', + 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', + 'useSessions: UseSessions', + 'useSessionPendingInteraction: UseSessionPendingInteraction', + 'useWorkspaces: SnapshotSelectorHook', + ], + keyDomain: '', + hookContext: '', + slotInject: '', + declaredBy: 'an entry in \'sidebar\' (client-ui-sidebar), so it exists while that entry is mounted', + occupants: [ + 'client-ui-sidebar HelloWorldIcon', + ], + replaceRisk: 'none', + example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.panellist\', () => ctx.slots.register(\n { name: \'sidebar.panellist\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', + source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:32', + }, + { + key: 'sidebar.panellist.title', + kind: 'keyed', + scope: 'root', + summary: 'Global panel titles keyed by list id, with the list label as fallback.', + doc: 'Global panel titles keyed by list id, with the list label as fallback.', + registerOptions: [ + { + name: 'key', + requirement: 'required', + type: 'string', + doc: 'Your cell key: the entry renders where the owner dispatches this exact key. Registering an already-occupied key replaces that occupant.', + }, + ], + ownerProps: [ + '/** Title presentation supplied by the global panel row. */\nexport interface SidebarPanelTitleOwnerProps {\n /** Whether this panel is selected in the main column. */\n active: boolean\n}', + ], + ownerPropsReferences: [], + standardProps: [ + 'useResource: UseResource', + 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', + 'useSessions: UseSessions', + 'useSessionPendingInteraction: UseSessionPendingInteraction', + 'useWorkspaces: SnapshotSelectorHook', + ], + keyDomain: 'open: any string the owner dispatches (no compile-time key set), none are taken yet', + hookContext: '', + slotInject: '', + declaredBy: 'an entry in \'sidebar\' (client-ui-sidebar), so it exists while that entry is mounted', + occupants: [], + replaceRisk: 'none', + example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.panellist.title\', () => ctx.slots.register(\n { name: \'sidebar.panellist.title\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', + source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:34', }, { key: 'sidebar.right.pane.tab', @@ -2084,6 +2279,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -2099,7 +2295,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ keyDomain: 'open: any string the owner dispatches (no compile-time key set), none are taken yet', hookContext: 'TabHookContext', slotInject: 'SidebarRightTabInjected', - declaredBy: 'an entry in \'rightbar\' (client-ui-sidebar-right), so it exists while that entry is mounted', + declaredBy: 'an entry in \'rightbar.session\' (client-ui-sidebar-right), so it exists while that entry is mounted', occupants: [ 'client-ui-sidebar-files FilesBody', 'client-ui-sidebar-right GuideBody', @@ -2107,7 +2303,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.right.pane.tab\', () => ctx.slots.register(\n { name: \'sidebar.right.pane.tab\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-sidebar-right/src/client/contract/slots.ts:47', + source: 'packages/client/ui-sidebar-right/src/client/contract/slots.ts:50', }, { key: 'sidebar.right.pane.tab.title', @@ -2128,6 +2324,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -2143,11 +2340,11 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ keyDomain: 'open: any string the owner dispatches (no compile-time key set), none are taken yet', hookContext: 'TabHookContext', slotInject: 'SidebarRightTabInjected', - declaredBy: 'an entry in \'rightbar\' (client-ui-sidebar-right), so it exists while that entry is mounted', + declaredBy: 'an entry in \'rightbar.session\' (client-ui-sidebar-right), so it exists while that entry is mounted', occupants: [], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.right.pane.tab.title\', () => ctx.slots.register(\n { name: \'sidebar.right.pane.tab.title\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-sidebar-right/src/client/contract/slots.ts:61', + source: 'packages/client/ui-sidebar-right/src/client/contract/slots.ts:64', }, { key: 'sidebar.right.tab.guide', @@ -2168,6 +2365,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -2187,7 +2385,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ occupants: [], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.right.tab.guide\', () => ctx.slots.register(\n { name: \'sidebar.right.tab.guide\', select: owner => null },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-sidebar-right/src/client/contract/slots.ts:72', + source: 'packages/client/ui-sidebar-right/src/client/contract/slots.ts:75', }, { key: 'sidebar.right.tab.menu.item', @@ -2224,6 +2422,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -2239,11 +2438,11 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ keyDomain: '', hookContext: '', slotInject: '', - declaredBy: 'an entry in \'rightbar\' (client-ui-sidebar-right), so it exists while that entry is mounted', + declaredBy: 'an entry in \'rightbar.session\' (client-ui-sidebar-right), so it exists while that entry is mounted', occupants: [], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.right.tab.menu.item\', () => ctx.slots.register(\n { name: \'sidebar.right.tab.menu.item\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-sidebar-right/src/client/contract/slots.ts:83', + source: 'packages/client/ui-sidebar-right/src/client/contract/slots.ts:86', }, { key: 'sidebar.settings', @@ -2259,6 +2458,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -2272,7 +2472,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'shadows-shipped-ui', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.settings\', () => ctx.slots.register(\n { name: \'sidebar.settings\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:41', + source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:47', }, { key: 'sidebar.workspaces', @@ -2288,6 +2488,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -2301,7 +2502,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'shadows-shipped-ui', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.workspaces\', () => ctx.slots.register(\n { name: \'sidebar.workspaces\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:35', + source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:41', }, { key: 'sidebar.workspaces.directoryFlow', @@ -2317,6 +2518,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -2350,6 +2552,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -2398,6 +2601,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', @@ -2461,6 +2665,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ standardProps: [ 'useResource: UseResource', 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', 'useSessions: UseSessions', 'useSessionPendingInteraction: UseSessionPendingInteraction', 'useWorkspaces: SnapshotSelectorHook', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c3122497b..8858ea63f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2975,6 +2975,9 @@ importers: '@deepseek-ai/cordis': specifier: workspace:^ version: link:../../../vendor/cordis + '@deepseek-ai/dsh-brand': + specifier: workspace:^ + version: link:../../util/brand '@deepseek-ai/dsh-client-locale': specifier: workspace:^ version: link:../locale @@ -3716,6 +3719,9 @@ importers: '@deepseek-ai/dsh-client-locale': specifier: workspace:^ version: link:../locale + '@deepseek-ai/dsh-client-store': + specifier: workspace:^ + version: link:../store '@deepseek-ai/dsh-client-test-runtime': specifier: workspace:^ version: link:../../test-support/client-runtime @@ -4423,6 +4429,9 @@ importers: '@deepseek-ai/dsh-client-ui-conversation': specifier: workspace:^ version: link:../ui-conversation + '@deepseek-ai/dsh-client-ui-layout': + specifier: workspace:^ + version: link:../ui-layout '@deepseek-ai/dsh-client-ui-primitives': specifier: workspace:^ version: link:../ui-primitives From fe460f7fd1fc5e101583f65986e61b5f8a7863da Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Tue, 8 Sep 2026 17:54:23 +0800 Subject: [PATCH 35/58] test(client): support panel selection in slot runtime --- .../test-support/client-runtime/package.json | 1 + .../test-support/client-runtime/src/index.ts | 34 ++++++++++++------ .../tests/helpers.client.spec.tsx | 36 +++++++++++++++++++ .../test-support/client-runtime/tsconfig.json | 3 ++ pnpm-lock.yaml | 3 ++ 5 files changed, 67 insertions(+), 10 deletions(-) diff --git a/packages/test-support/client-runtime/package.json b/packages/test-support/client-runtime/package.json index 8d9984b6bb..88298cd72b 100644 --- a/packages/test-support/client-runtime/package.json +++ b/packages/test-support/client-runtime/package.json @@ -54,6 +54,7 @@ "@deepseek-ai/dsh-client-store": "workspace:^", "@deepseek-ai/dsh-client-ui-chat": "workspace:^", "@deepseek-ai/dsh-client-ui-conversation": "workspace:^", + "@deepseek-ai/dsh-client-ui-layout": "workspace:^", "@deepseek-ai/dsh-client-ui-renderer": "workspace:^", "@deepseek-ai/dsh-client-ui-session": "workspace:^", "@deepseek-ai/dsh-client-ui-settings": "workspace:^", diff --git a/packages/test-support/client-runtime/src/index.ts b/packages/test-support/client-runtime/src/index.ts index 7d0d37c9cb..caad7ac5d1 100644 --- a/packages/test-support/client-runtime/src/index.ts +++ b/packages/test-support/client-runtime/src/index.ts @@ -29,8 +29,10 @@ import { apply as applyUiSession, inject as uiSessionInject, } from '@deepseek-ai/dsh-client-ui-session/client' import type { SessionId } from '@deepseek-ai/dsh-session/types' +import { createSnapshotStore } from '@deepseek-ai/dsh-client-store' +import type { PanelInfo } from '@deepseek-ai/dsh-client-ui-layout/client' import type { - ChildrenDecl, ComposedProps, HostObservable, OwnerOf, SlotComponent, SlotMap, SlotRenderer, + ChildrenDecl, ComposedProps, HostObservable, OwnerOf, RenderOpts, SlotComponent, SlotMap, SlotRenderer, SlotRendererHost, SnapshotSelectorHook, StoreInstanceLike, } from '@deepseek-ai/dsh-client-ui-slots' import { registerDomSnapshotSerializer } from './snapshot.ts' @@ -125,7 +127,7 @@ export interface TestFileUpload { * {@link SlotView.update} drive React through the standard uSES boundary. */ class OwnerPropsCell { - private readonly owners = new Map() + private readonly owners = new Map() private readonly listeners = new Set<() => void>() private version = 0 @@ -147,15 +149,16 @@ class OwnerPropsCell { * caller wraps in act). * @param key - slot key. * @param owner - owner props share. + * @param opts - explicit keyed or list selection for the render site. */ - set(key: string, owner: object): void { - this.owners.set(key, owner) + set(key: string, owner: object, opts?: RenderOpts): void { + this.owners.set(key, { owner, opts }) this.version += 1 for (const fn of [...this.listeners]) fn() } /** Keys with supplied owner props, in first-supply order. */ - entries(): readonly (readonly [string, object])[] { + entries(): readonly (readonly [string, { owner: object; opts: RenderOpts | undefined }])[] { return [...this.owners.entries()] } } @@ -217,6 +220,8 @@ export class SlotTestRuntime { readonly sessions: TestSessions /** Workspaces double (list observable, recorded intent actions). */ readonly workspaces: TestWorkspaces + /** Test-owned panel selection used by the framework's usePanelInfo hook. */ + readonly panelInfo = createSnapshotStore({ activePanelId: null }) /** Mutable file-upload stub; replace `upload` in suites that exercise the capability. */ readonly fileUpload: TestFileUpload @@ -233,6 +238,7 @@ export class SlotTestRuntime { private readonly autoDeclared = new Set() private autoRootView: RenderResult | undefined private readonly disposeWorkspaceSource: () => void + private readonly disposePanelInfoSource: () => void private constructor(ctx: Context, slots: SlotRegistry) { this.ctx = ctx @@ -248,6 +254,7 @@ export class SlotTestRuntime { ctx.provide('workspaces', this.workspaces) ctx.provide('fileUpload', this.fileUpload as never) this.disposeWorkspaceSource = slots.provideRoot({ hooks: { workspaces: this.workspaces.list } }) + this.disposePanelInfoSource = slots.provideRoot({ hooks: { panelInfo: this.panelInfo } }) // Capturing install: the production renderer does the rendering; the // wrapper only takes the host face for storeOf (no machinery copied). const renderer = createSlotRenderer() @@ -309,6 +316,11 @@ export class SlotTestRuntime { this.disposeWorkspaceSource() } + /** Release the default panel hook before mounting the production Layout owner. */ + releasePanelInfoSource(): void { + this.disposePanelInfoSource() + } + /** * Render the root slot tree through the ctx-level entry (the shell's own * entry point): `ctx.slots.renderSlot('root', {})` under Testing Library. @@ -333,13 +345,13 @@ export class SlotTestRuntime { async declare(children: ChildrenDecl): Promise { for (const key of Object.keys(children)) this.autoDeclared.add(key) const cell = this.ownerCell - const AutoFrame = (props: { renderSlot: (key: string, owner: object) => ReactNode }) => { + const AutoFrame = (props: { renderSlot: (key: string, owner: object, opts?: RenderOpts) => ReactNode }) => { useSyncExternalStore(cell.subscribe, cell.getVersion) // Keyed Fragments only: the renderer's outlet anchor is the one // `[data-slot]` element — the frame adding its own would nest // duplicate anchors under the same key. - return createElement(Fragment, null, cell.entries().map(([key, owner]) => - createElement(Fragment, { key }, props.renderSlot(key, owner)))) + return createElement(Fragment, null, cell.entries().map(([key, { owner, opts }]) => + createElement(Fragment, { key }, props.renderSlot(key, owner, opts)))) } await this.root.declare(children as never, AutoFrame as never) } @@ -352,16 +364,17 @@ export class SlotTestRuntime { * slot of the same tree. * @param key - a key declared through {@link SlotTestRuntime.declare}. * @param owner - owner props share for the render site. + * @param opts - explicit keyed or list selection; retained by view updates. * @returns the slot-local view (snapshot container, scoped queries, owner updates). */ - renderSlot(key: K, owner: OwnerOf): SlotView { + renderSlot(key: K, owner: OwnerOf, opts?: RenderOpts): SlotView { if (!this.autoDeclared.has(key)) { throw new Error(`renderSlot('${key}') without declare() — declare the key first (or use root.declare for a custom frame)`) } const install = (next: object): void => { // Synchronous cell write inside act: the frame re-renders through uSES. act(() => { - this.ownerCell.set(key, next) + this.ownerCell.set(key, next, opts) }) } install(owner) @@ -421,6 +434,7 @@ export class SlotTestRuntime { for (const view of this.views.splice(0)) view.unmount() for (const handle of this.handles.splice(0)) await handle.dispose() this.root.release() + this.disposePanelInfoSource() await this.sessions.disposeScopes() localStorage.clear() } diff --git a/packages/test-support/client-runtime/tests/helpers.client.spec.tsx b/packages/test-support/client-runtime/tests/helpers.client.spec.tsx index 484fa8cc01..e5efdc20f4 100644 --- a/packages/test-support/client-runtime/tests/helpers.client.spec.tsx +++ b/packages/test-support/client-runtime/tests/helpers.client.spec.tsx @@ -5,6 +5,8 @@ import { createSnapshotStore } from '@deepseek-ai/dsh-client-store' import { EMPTY_CHAT_SNAPSHOT } from '@deepseek-ai/dsh-client-ui-chat/client' import { EMPTY_CONVERSATION_SNAPSHOT } from '@deepseek-ai/dsh-client-ui-conversation/client' import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type { MainPanelId, PanelInfo } from '@deepseek-ai/dsh-client-ui-layout/client' +import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import { afterAll, afterEach, describe, expect, it, vi } from 'vitest' import { bindSnapshotSelector, @@ -17,6 +19,12 @@ import { const originalLanguages = [...navigator.languages] const originalLanguage = navigator.language +declare module '@deepseek-ai/dsh-client-ui-slots' { + interface SlotMap { + 'trt.panel-info': { kind: 'keyed'; scope: 'root'; owner: { label: string } } + } +} + usePinnedBrowserLanguages('zh-CN', 'en-US') afterEach(cleanup) afterAll(() => { @@ -38,6 +46,34 @@ function entry(seq: number): SessionLiveEventEntry { } describe('fixture helpers', () => { + it('drives panel hooks, retains keyed selection on owner updates, and releases the default source', async () => { + const runtime = await SlotTestRuntime.create() + try { + await runtime.declare({ 'trt.panel-info': { kind: 'keyed', scope: 'root' } }) + runtime.slots.register({ name: 'trt.panel-info', key: 'probe' }, + ({ usePanelInfo, label }: PropsRuntime<'trt.panel-info'>) => ( + {label}:{usePanelInfo(info => info.activePanelId) ?? 'conversation'} + )) + const view = runtime.renderSlot('trt.panel-info', { label: 'first' }, { entryKey: 'probe' }) + expect(view.container.textContent).toBe('first:conversation') + act(() => { runtime.panelInfo.set({ activePanelId: 'custom' as MainPanelId }) }) + expect(view.container.textContent).toBe('first:custom') + view.update({ label: 'next' }) + expect(view.container.textContent).toBe('next:custom') + const replacement = createSnapshotStore({ activePanelId: null }) + await act(async () => { + runtime.releasePanelInfoSource() + await runtime.mount({ + inject: ['slots'], + apply(ctx) { ctx.slots.provideRoot({ hooks: { panelInfo: replacement } }) }, + }) + }) + expect(view.container.textContent).toBe('next:conversation') + } finally { + await runtime.dispose() + } + }) + it('rejects an upload until a suite replaces the default stub', async () => { const runtime = await SlotTestRuntime.create() expect(runtime.fileUpload.available).toBe(false) diff --git a/packages/test-support/client-runtime/tsconfig.json b/packages/test-support/client-runtime/tsconfig.json index f9abd79351..bc8fbf0688 100644 --- a/packages/test-support/client-runtime/tsconfig.json +++ b/packages/test-support/client-runtime/tsconfig.json @@ -38,6 +38,9 @@ { "path": "../../client/ui-conversation" }, + { + "path": "../../client/ui-layout" + }, { "path": "../../client/ui-slots" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8858ea63f1..06b92cd460 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9900,6 +9900,9 @@ importers: '@deepseek-ai/dsh-client-ui-conversation': specifier: workspace:^ version: link:../../client/ui-conversation + '@deepseek-ai/dsh-client-ui-layout': + specifier: workspace:^ + version: link:../../client/ui-layout '@deepseek-ai/dsh-client-ui-renderer': specifier: workspace:^ version: link:../../client/ui-renderer From f95e781f3d419747283b3cc2b57752b2dac2c839 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Tue, 8 Sep 2026 17:54:24 +0800 Subject: [PATCH 36/58] test(client): migrate panel navigation fixtures and snapshots --- .../locale/tests/language-row.client.spec.tsx | 3 +- .../tests/message-image.client.spec.tsx | 3 +- .../tests/apply-inject.client.spec.tsx | 3 +- .../ui-chat/tests/chat-apply.client.spec.tsx | 6 +- .../ui-chat/tests/chat-view.client.spec.tsx | 1 + .../tests/transcript-view-row.client.spec.tsx | 1 + .../tests/apply-inject.client.spec.tsx | 11 +- .../tests/apply-wiring.client.spec.tsx | 25 ++- .../tests/assembly-surfaces.client.spec.tsx | 26 ++- .../tests/enter-behavior-row.client.spec.tsx | 1 + .../tests/input-bar.client.spec.tsx | 1 + .../tests/input-matrix.client.spec.tsx | 1 + .../tests/input-scenarios.client.spec.tsx | 1 + .../tests/queue-dock.client.spec.tsx | 2 + .../tests/skeleton.client.spec.tsx | 4 + .../ui-layout/tests/app-frame.client.spec.tsx | 110 +++++++----- .../ui-layout/tests/apply.client.spec.ts | 99 +++++++++-- .../tests/document-title.client.spec.tsx | 74 ++++++-- .../tests/layout-store.client.spec.ts | 164 ++++++++++++------ .../ui-layout/tests/service.client.spec.ts | 52 +++--- .../permission-presets-row.client.spec.tsx | 3 +- .../tests/components.client.spec.tsx | 3 +- .../tests/settings-root.client.spec.tsx | 3 +- .../tests/onboarding-dialog.client.spec.tsx | 3 +- .../tests/welcome-notice.client.spec.tsx | 3 +- .../tests/apply.client.spec.ts | 29 ++-- .../tests/seat.client.spec.tsx | 18 +- .../sidebar-snapshot.client.spec.tsx.snap | 107 ++++++++++++ .../ui-sidebar/tests/apply.client.spec.tsx | 36 +++- .../tests/pointer-scrollbars.client.spec.tsx | 3 +- .../tests/sidebar-root.client.spec.tsx | 9 +- .../tests/appearance-row.client.spec.tsx | 3 +- .../tests/font-size-row.client.spec.tsx | 3 +- .../tests/assembly-surfaces.client.spec.tsx | 7 +- .../tests/toolview-slot.client.spec.tsx | 8 +- .../ui-trajectory/tests/views.client.spec.tsx | 5 +- .../tests/plan-review-panel.client.spec.tsx | 3 +- .../user-questions-composer.client.spec.tsx | 3 +- .../tests/workflow-run.client.spec.tsx | 3 +- .../ui-workspace/tests/apply.client.spec.ts | 6 +- .../tests/host-home-staleness.client.spec.tsx | 1 + .../tests/rename-assembly.client.spec.tsx | 1 + .../tests/workspace-browser.client.spec.tsx | 3 +- .../tests/workspace-picker.client.spec.tsx | 7 +- .../tests/workspaces-service.client.spec.ts | 23 ++- 45 files changed, 657 insertions(+), 224 deletions(-) diff --git a/packages/client/locale/tests/language-row.client.spec.tsx b/packages/client/locale/tests/language-row.client.spec.tsx index f10adf664e..d65a1c82f0 100644 --- a/packages/client/locale/tests/language-row.client.spec.tsx +++ b/packages/client/locale/tests/language-row.client.spec.tsx @@ -12,6 +12,7 @@ import { createLanguageRowStore } from '../src/client/settings-store.ts' // Every fixture carries the resource hook the resources plugin merges into GlobalStandardProps. const useResource = (() => ({ status: 'none' as const, value: undefined, failure: undefined, reload: () => {} })) as GlobalStandardProps['useResource'] +const usePanelInfo: GlobalStandardProps['usePanelInfo'] = selector => selector({ activePanelId: null }) afterEach(cleanup) @@ -41,7 +42,7 @@ function mount(active = 'en') { const props: LanguageRowComponentProps = { useSessions: emptySessions(), useSessionPendingInteraction, - useResource, + usePanelInfo, useResource, useWorkspaces: emptyWorkspaces(), useStore: bindSnapshotSelector(store), actions: store.actions, diff --git a/packages/client/ui-attachment/tests/message-image.client.spec.tsx b/packages/client/ui-attachment/tests/message-image.client.spec.tsx index 7da3a878e9..3e86094c74 100644 --- a/packages/client/ui-attachment/tests/message-image.client.spec.tsx +++ b/packages/client/ui-attachment/tests/message-image.client.spec.tsx @@ -12,6 +12,7 @@ import { MessageImages } from '../src/client/MessageImages.tsx' // Every session-scope fixture carries the resource hook the resources plugin merges into GlobalStandardProps. const useResource = (() => ({ status: 'none' as const, value: undefined, failure: undefined, reload: () => {} })) as GlobalStandardProps['useResource'] +const usePanelInfo: GlobalStandardProps['usePanelInfo'] = selector => selector({ activePanelId: null }) afterEach(cleanup) @@ -272,7 +273,7 @@ describe('ImageGallery', () => { sessionId: 'message-images-test' as MessageImagesProps['sessionId'], useSession, useSessions, - useResource, + usePanelInfo, useResource, useSessionPendingInteraction, useWorkspaces, useProjection: () => undefined, diff --git a/packages/client/ui-chat/tests/apply-inject.client.spec.tsx b/packages/client/ui-chat/tests/apply-inject.client.spec.tsx index 590cc5602c..91c78811e0 100644 --- a/packages/client/ui-chat/tests/apply-inject.client.spec.tsx +++ b/packages/client/ui-chat/tests/apply-inject.client.spec.tsx @@ -58,6 +58,7 @@ async function bench() { new TestRemote(runtime.ctx, { session: { openWorkspacePath } }) runtime.ctx.provide('uiWorkspace', { connectWorkspace: vi.fn(async () => ROOT), + openSession: (id: SessionId) => { runtime.sessions.open(id) }, } as never) const session = sessionFakeFor() await runtime.sessions.add({ @@ -69,7 +70,7 @@ async function bench() { runtime.ctx.provide('locale', locale) runtime.slots.installLocale(locale) await runtime.root.declare({ - 'conversation': { kind: 'single', scope: 'session-maybe' }, + 'main': { kind: 'keyed', scope: 'root' }, }, (_props: { renderSlot?: unknown }) => null) await runtime.mount({ inject: [...injectConversation], apply: applyConversation }) await runtime.mount({ inject: [...injectChat], apply: applyChat }) diff --git a/packages/client/ui-chat/tests/chat-apply.client.spec.tsx b/packages/client/ui-chat/tests/chat-apply.client.spec.tsx index fc1cf43bc5..6c1ab9c6a0 100644 --- a/packages/client/ui-chat/tests/chat-apply.client.spec.tsx +++ b/packages/client/ui-chat/tests/chat-apply.client.spec.tsx @@ -45,6 +45,7 @@ async function bench() { runtime.ctx.provide('sidebarRight', { openResource: vi.fn() } as never) runtime.ctx.provide('uiWorkspace', { connectWorkspace: vi.fn(async () => SID), + openSession: (id: SessionId) => { runtime.sessions.open(id) }, } as never) new TestRemote(runtime.ctx, { session: { openWorkspacePath: vi.fn(async () => ({ ok: true, value: { opened: true } })) }, @@ -53,7 +54,7 @@ async function bench() { runtime.ctx.provide('locale', locale) runtime.slots.installLocale(locale) await runtime.root.declare({ - 'conversation': { kind: 'single', scope: 'session-maybe' }, + 'main': { kind: 'keyed', scope: 'root' }, 'conversation.approval.detail': { kind: 'single', scope: 'session' }, 'settings.general.item': { kind: 'list', scope: 'root' }, }, (_props: { renderSlot?: unknown }) => null) @@ -120,7 +121,8 @@ describe('Chat apply wiring', () => { await b.chat.dispose() expect(b.runtime.slots.entries('conversation.view')).toHaveLength(0) expect(b.runtime.slots.spec('conversation.chat.node')).toBeUndefined() - expect(b.runtime.slots.entries('conversation')).toHaveLength(1) + expect(b.runtime.slots.entries('main').map(row => row.options.key)).toEqual(['conversation']) + expect(b.runtime.slots.entries('main.conversation')).toHaveLength(1) expect(b.runtime.ctx.get('uiConversation')).toBeDefined() await b.runtime.dispose() }) diff --git a/packages/client/ui-chat/tests/chat-view.client.spec.tsx b/packages/client/ui-chat/tests/chat-view.client.spec.tsx index 7b72d280dc..fc8fd9c15e 100644 --- a/packages/client/ui-chat/tests/chat-view.client.spec.tsx +++ b/packages/client/ui-chat/tests/chat-view.client.spec.tsx @@ -364,6 +364,7 @@ function makeHarness( // ChatView never invokes it (pass-through stub). const SessionProviderStub: ChatViewSlotProps['SessionProvider'] = ({ children }) => <>{children} const props: ChatViewSlotProps = { + usePanelInfo: selector => selector({ activePanelId: null }), sessionId: SID, useSession: bindSnapshotSelector(session.source), useChat: bindSnapshotSelector(chatSource.source), diff --git a/packages/client/ui-chat/tests/transcript-view-row.client.spec.tsx b/packages/client/ui-chat/tests/transcript-view-row.client.spec.tsx index 5aa8e31047..7004e75a37 100644 --- a/packages/client/ui-chat/tests/transcript-view-row.client.spec.tsx +++ b/packages/client/ui-chat/tests/transcript-view-row.client.spec.tsx @@ -35,6 +35,7 @@ function mount(mode: 'normal' | 'compact' = 'compact') { const source = createSnapshotStore(mode) const setTranscriptView = vi.fn((next: 'normal' | 'compact') => { source.set(next) }) const props: TranscriptViewRowProps = { + usePanelInfo: selector => selector({ activePanelId: null }), useSessions: emptySessions(), useSessionPendingInteraction: noPendingInteraction(), useWorkspaces: emptyWorkspaces(), diff --git a/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx b/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx index 2dde9ce7b3..aa50a19ba2 100644 --- a/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx +++ b/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx @@ -49,7 +49,10 @@ async function bench() { } runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) const connectWorkspace = vi.fn(async () => ROOT) - runtime.ctx.provide('uiWorkspace', { connectWorkspace } as never) + runtime.ctx.provide('uiWorkspace', { + connectWorkspace, + openSession: (id: SessionId) => { runtime.sessions.open(id) }, + } as never) const sessionFake = sessionFakeFor() await runtime.sessions.add({ id: ROOT, @@ -60,12 +63,12 @@ async function bench() { runtime.ctx.provide('locale', locale) runtime.slots.installLocale(locale) await runtime.root.declare({ - 'conversation': { kind: 'single', scope: 'session-maybe' }, + 'main': { kind: 'keyed', scope: 'root' }, }, (_props: { renderSlot?: unknown }) => null) const feature = await runtime.mount({ inject: [...inject], apply }) runtime.renderRoot() - const entryOf = (key: 'conversation' | 'conversation.session' | 'conversation.session.header' | 'conversation.composer.bar') => + const entryOf = (key: 'main.conversation' | 'conversation.session' | 'conversation.session.header' | 'conversation.composer.bar') => runtime.slots.entries(key)[0]! const conversationApi = (id: SessionId) => { const entry = entryOf('conversation.session') @@ -77,7 +80,7 @@ async function bench() { return { instance, injected } } const residentApi = (id: SessionId | undefined) => { - const entry = entryOf('conversation') + const entry = entryOf('main.conversation') return (entry.inject as unknown as (sessionId: SessionId | undefined) => ConversationInjected)(id) } const headerApi = (id: SessionId) => { diff --git a/packages/client/ui-conversation/tests/apply-wiring.client.spec.tsx b/packages/client/ui-conversation/tests/apply-wiring.client.spec.tsx index eccf4b87a0..a71103fb32 100644 --- a/packages/client/ui-conversation/tests/apply-wiring.client.spec.tsx +++ b/packages/client/ui-conversation/tests/apply-wiring.client.spec.tsx @@ -14,14 +14,17 @@ const SID = 'session-1' as SessionId async function bench(options: { declareConversation?: boolean } = {}) { const runtime = await SlotTestRuntime.create() - runtime.ctx.provide('uiWorkspace', { connectWorkspace: vi.fn(async () => SID) } as never) + runtime.ctx.provide('uiWorkspace', { + connectWorkspace: vi.fn(async () => SID), + openSession: (id: SessionId) => { runtime.sessions.open(id) }, + } as never) runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) const locale = new LocaleRuntime(runtime.ctx) runtime.ctx.provide('locale', locale) runtime.slots.installLocale(locale) if (options.declareConversation !== false) { await runtime.root.declare({ - 'conversation': { kind: 'single', scope: 'session-maybe' }, + 'main': { kind: 'keyed', scope: 'root' }, 'settings.general.item': { kind: 'list', scope: 'root' }, }, (_props: { renderSlot?: unknown }) => null) } @@ -31,7 +34,7 @@ async function bench(options: { declareConversation?: boolean } = {}) { function entry( runtime: SlotTestRuntime, - key: 'conversation' | 'conversation.session' | 'conversation.session.header', + key: 'main.conversation' | 'conversation.session' | 'conversation.session.header', ) { return runtime.slots.entries(key)[0] as { store?: unknown } | undefined } @@ -39,14 +42,18 @@ function entry( describe('target-neutral Conversation apply wiring', () => { it('waits for the layout-owned conversation declaration before registering its subtree', async () => { const b = await bench({ declareConversation: false }) - expect(b.runtime.slots.entries('conversation')).toHaveLength(0) + expect(b.runtime.slots.entries('main')).toHaveLength(0) + expect(b.runtime.slots.entries('main.conversation')).toHaveLength(0) await b.runtime.root.declare({ - 'conversation': { kind: 'single', scope: 'session-maybe' }, + 'main': { kind: 'keyed', scope: 'root' }, 'settings.general.item': { kind: 'list', scope: 'root' }, }, (_props: { renderSlot?: unknown }) => null) - expect(b.runtime.slots.entries('conversation')).toHaveLength(1) + expect(b.runtime.slots.entries('main').map(row => row.options.key)).toEqual(['conversation']) + expect(b.runtime.slots.entries('main.conversation')).toHaveLength(1) + expect(b.runtime.slots.spec('main.conversation')) + .toEqual({ kind: 'single', scope: 'session-maybe' }) expect(b.runtime.slots.entries('conversation.session')).toHaveLength(1) expect(b.runtime.slots.entries('conversation.session.header')).toHaveLength(1) expect(b.runtime.slots.entries('conversation.composer.bar')).toHaveLength(1) @@ -65,7 +72,7 @@ describe('target-neutral Conversation apply wiring', () => { const b = await bench() const session = entry(b.runtime, 'conversation.session') const header = entry(b.runtime, 'conversation.session.header') - expect(entry(b.runtime, 'conversation')?.store).toBeUndefined() + expect(entry(b.runtime, 'main.conversation')?.store).toBeUndefined() expect(session?.store).toBeDefined() expect(header?.store).toBe(session?.store) expect(b.runtime.slots.spec('conversation.composer')) @@ -110,7 +117,9 @@ describe('target-neutral Conversation apply wiring', () => { await b.feature.dispose() expect(b.runtime.ctx.get('conversation')).toBeUndefined() expect(b.runtime.ctx.get('uiConversation')).toBeUndefined() - expect(b.runtime.slots.entries('conversation')).toHaveLength(0) + expect(b.runtime.slots.entries('main')).toHaveLength(0) + expect(b.runtime.slots.entries('main.conversation')).toHaveLength(0) + expect(b.runtime.slots.spec('main.conversation')).toBeUndefined() expect(b.runtime.slots.spec('conversation.view')).toBeUndefined() await b.runtime.dispose() }) diff --git a/packages/client/ui-conversation/tests/assembly-surfaces.client.spec.tsx b/packages/client/ui-conversation/tests/assembly-surfaces.client.spec.tsx index fd99320d93..fc4eccb528 100644 --- a/packages/client/ui-conversation/tests/assembly-surfaces.client.spec.tsx +++ b/packages/client/ui-conversation/tests/assembly-surfaces.client.spec.tsx @@ -40,13 +40,13 @@ beforeEach(() => { vi.stubGlobal('ResizeObserver', ResizeObserverStub) }) -type AppRootProps = PropsRenderSlots<'conversation'> +type AppRootProps = PropsRenderSlots<'main'> function AppRoot({ renderSlot }: AppRootProps) { - return <>{renderSlot('conversation', {})} + return <>{renderSlot('main', {}, { entryKey: 'conversation' })} } const LAYOUT_CHILDREN = { - 'conversation': { kind: 'single', scope: 'session-maybe' }, + 'main': { kind: 'keyed', scope: 'root' }, } as const function WorkspaceProbe({ open }: EmptyWorkspaceOwnerProps) { @@ -60,7 +60,10 @@ function WorkspaceProbe({ open }: EmptyWorkspaceOwnerProps) { async function bench(opts?: { blank?: boolean }) { const runtime = await SlotTestRuntime.create() - runtime.ctx.provide('uiWorkspace', { connectWorkspace: vi.fn(async () => SID) } as never) + runtime.ctx.provide('uiWorkspace', { + connectWorkspace: vi.fn(async () => SID), + openSession: (id: SessionId) => { runtime.sessions.open(id) }, + } as never) runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) const locale = new LocaleRuntime(runtime.ctx) runtime.ctx.provide('locale', locale) @@ -82,7 +85,10 @@ async function bench(opts?: { blank?: boolean }) { describe('resident composer', () => { it('renders the locked view state while no session exists at all', async () => { const runtime = await SlotTestRuntime.create() - runtime.ctx.provide('uiWorkspace', { connectWorkspace: vi.fn(async () => SID) } as never) + runtime.ctx.provide('uiWorkspace', { + connectWorkspace: vi.fn(async () => SID), + openSession: (id: SessionId) => { runtime.sessions.open(id) }, + } as never) runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) const locale = new LocaleRuntime(runtime.ctx) runtime.ctx.provide('locale', locale) @@ -109,7 +115,10 @@ describe('resident composer', () => { it('keeps the complete Hero tree mounted when the first Workspace session appears', async () => { const runtime = await SlotTestRuntime.create() - runtime.ctx.provide('uiWorkspace', { connectWorkspace: vi.fn(async () => SID) } as never) + runtime.ctx.provide('uiWorkspace', { + connectWorkspace: vi.fn(async () => SID), + openSession: (id: SessionId) => { runtime.sessions.open(id) }, + } as never) runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) const locale = new LocaleRuntime(runtime.ctx) runtime.ctx.provide('locale', locale) @@ -174,7 +183,10 @@ describe('resident composer', () => { describe('prompt rejection through the assembled composer', () => { it('renders the promptError alert strip and keeps the draft in the machine', async () => { const runtime = await SlotTestRuntime.create() - runtime.ctx.provide('uiWorkspace', { connectWorkspace: vi.fn(async () => SID) } as never) + runtime.ctx.provide('uiWorkspace', { + connectWorkspace: vi.fn(async () => SID), + openSession: (id: SessionId) => { runtime.sessions.open(id) }, + } as never) runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) const locale = new LocaleRuntime(runtime.ctx) runtime.ctx.provide('locale', locale) diff --git a/packages/client/ui-conversation/tests/enter-behavior-row.client.spec.tsx b/packages/client/ui-conversation/tests/enter-behavior-row.client.spec.tsx index 0cba7c36b5..4af4d1dbd6 100644 --- a/packages/client/ui-conversation/tests/enter-behavior-row.client.spec.tsx +++ b/packages/client/ui-conversation/tests/enter-behavior-row.client.spec.tsx @@ -40,6 +40,7 @@ function mount() { const policy = new ComposerSubmissionPolicy() const setBusyEnter = vi.fn((behavior: 'queue' | 'steer') => { policy.setBusyEnter(behavior) }) const props: EnterBehaviorRowProps = { + usePanelInfo: selector => selector({ activePanelId: null }), useSessions: emptySessions(), useSessionPendingInteraction: noPendingInteraction(), useResource, diff --git a/packages/client/ui-conversation/tests/input-bar.client.spec.tsx b/packages/client/ui-conversation/tests/input-bar.client.spec.tsx index 051aa295e3..75c30b117d 100644 --- a/packages/client/ui-conversation/tests/input-bar.client.spec.tsx +++ b/packages/client/ui-conversation/tests/input-bar.client.spec.tsx @@ -164,6 +164,7 @@ function bench(over?: BenchOptions) { return null }) as never const props: InputBarProps = { + usePanelInfo: selector => selector({ activePanelId: null }), sessionId: SID, SessionProvider: ({ children }) => children, useSession: bindSnapshotSelector(session), diff --git a/packages/client/ui-conversation/tests/input-matrix.client.spec.tsx b/packages/client/ui-conversation/tests/input-matrix.client.spec.tsx index c333d25ada..57319d7087 100644 --- a/packages/client/ui-conversation/tests/input-matrix.client.spec.tsx +++ b/packages/client/ui-conversation/tests/input-matrix.client.spec.tsx @@ -48,6 +48,7 @@ function mountBar(shell: SessionInputShell, over?: { running?: boolean; disabled removed: over?.disabled ?? false, }) const props: InputBarProps = { + usePanelInfo: selector => selector({ activePanelId: null }), sessionId: SID, SessionProvider: ({ children }) => children, useSession: bindSnapshotSelector(session), diff --git a/packages/client/ui-conversation/tests/input-scenarios.client.spec.tsx b/packages/client/ui-conversation/tests/input-scenarios.client.spec.tsx index 9e41f82eb7..88ab3a809f 100644 --- a/packages/client/ui-conversation/tests/input-scenarios.client.spec.tsx +++ b/packages/client/ui-conversation/tests/input-scenarios.client.spec.tsx @@ -137,6 +137,7 @@ async function scopedBench(register?: (inputTriggers: InputTriggerService) => vo const wiring = shell const sessionStore = createSnapshotStore(sessionSnapshot(sessionId)) const barProps: InputBarProps = { + usePanelInfo: selector => selector({ activePanelId: null }), sessionId, SessionProvider: ({ children }) => children, useSession: bindSnapshotSelector(sessionStore), diff --git a/packages/client/ui-conversation/tests/queue-dock.client.spec.tsx b/packages/client/ui-conversation/tests/queue-dock.client.spec.tsx index 446ce0defd..08db337005 100644 --- a/packages/client/ui-conversation/tests/queue-dock.client.spec.tsx +++ b/packages/client/ui-conversation/tests/queue-dock.client.spec.tsx @@ -73,11 +73,13 @@ function liveSession(initial: SessionSnapshot) { const INPUT_STATE: InputState = { draft: '', attachmentIds: [], draftRev: 0, phase: 'plain', occurrences: [], queue: [] } const t: QueueDockProps['t'] = makeTranslate(zh, commonZh) +const usePanelInfo: GlobalStandardProps['usePanelInfo'] = selector => selector({ activePanelId: null }) function kitFor(snapshot: SessionSnapshot, injected: Partial = {}) { return { sessionId: SID, t, + usePanelInfo, useSessions: (() => { throw new Error('unused') }) as unknown as SnapshotSelectorHook, useResource, useSessionPendingInteraction: bindSnapshotSelector( diff --git a/packages/client/ui-conversation/tests/skeleton.client.spec.tsx b/packages/client/ui-conversation/tests/skeleton.client.spec.tsx index f1735ce415..9ffecfdfaa 100644 --- a/packages/client/ui-conversation/tests/skeleton.client.spec.tsx +++ b/packages/client/ui-conversation/tests/skeleton.client.spec.tsx @@ -199,6 +199,7 @@ function mount( useChat={useChat} useTrajectory={useTrajectory} useSessions={props.useSessions} + usePanelInfo={props.usePanelInfo} useResource={useResource} useSessionPendingInteraction={useSessionPendingInteraction} useWorkspaces={props.useWorkspaces} @@ -225,6 +226,7 @@ function mount( useChat={useChat} useTrajectory={useTrajectory} useSessions={props.useSessions} + usePanelInfo={props.usePanelInfo} useResource={useResource} useSessionPendingInteraction={useSessionPendingInteraction} useWorkspaces={props.useWorkspaces} @@ -251,6 +253,7 @@ function mount( useSession={useSession} useConversation={useConversation} useSessions={props.useSessions} + usePanelInfo={props.usePanelInfo} useSessionPendingInteraction={useSessionPendingInteraction} useWorkspaces={props.useWorkspaces} useProjection={(() => undefined)} @@ -295,6 +298,7 @@ function mount( : (opts?.fallback ?? null) )) as ConversationRootProps['renderSlotChain'] const props: ConversationRootProps = { + usePanelInfo: selector => selector({ activePanelId: null }), sessionId: SID, SessionProvider: ({ children }) => children, useSession, diff --git a/packages/client/ui-layout/tests/app-frame.client.spec.tsx b/packages/client/ui-layout/tests/app-frame.client.spec.tsx index 62623b9b81..55e51bdeac 100644 --- a/packages/client/ui-layout/tests/app-frame.client.spec.tsx +++ b/packages/client/ui-layout/tests/app-frame.client.spec.tsx @@ -1,14 +1,13 @@ // @vitest-environment jsdom /** Frame interactions with a real store and explicitly driven browser measurements. */ -import type { GlobalStandardProps } from '@deepseek-ai/dsh-client-ui-slots' +import type { GlobalStandardProps, RenderOpts } from '@deepseek-ai/dsh-client-ui-slots' +import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { act, cleanup, render } from '@testing-library/react' -import { useSyncExternalStore } from 'react' import { AppFrame } from '../src/client/AppFrame.tsx' import type { AppFrameProps } from '../src/client/AppFrame.tsx' -import type { RightbarOwnerProps, SidebarOwnerProps } from '../src/client/index.ts' +import type { MainPanelId, RightbarOwnerProps, SidebarOwnerProps } from '../src/client/index.ts' import { createLayoutStore } from '../src/client/stores.ts' -import type { SessionListState } from '@deepseek-ai/dsh-api-session-controller/client' import type { WorkspaceSnapshot } from '@deepseek-ai/dsh-api-workspace-controller/client' import type { SessionId } from '@deepseek-ai/dsh-session/types' @@ -19,8 +18,6 @@ let workspacesReady = true type AttentionSnapshot = Parameters[0]>[0] const noAttention: AttentionSnapshot = new Map() const useSessionPendingInteraction: AppFrameProps['useSessionPendingInteraction'] = selector => selector(noAttention) -const SessionProviderStub: AppFrameProps['SessionProvider'] = ({ children, empty }) => - selectedSession === undefined ? <>{empty?.() ?? null} : <>{children} let observers: ResizeObserverStub[] class ResizeObserverStub { @@ -63,19 +60,15 @@ function resize(width: number): void { }) } -function hookOf(inst: { subscribe: (fn: () => void) => () => void; getSnapshot: () => T }) { - return function useSelector(sel: (s: T) => S): S { return sel(useSyncExternalStore(inst.subscribe, inst.getSnapshot)) } -} - function mountFrame(windowWidth = frameWidth) { vi.stubGlobal('innerWidth', windowWidth) const instance = createLayoutStore().create() - const slotCalls: { key: string; props: object }[] = [] - const renderSlot = ((key: string, owner: object) => { - slotCalls.push({ key, props: owner }) - return
    - }) as AppFrameProps['renderSlot'] - const useSessions = ((sel: (s: SessionListState) => unknown) => sel({ + const slotCalls: { key: string; props: object; options: RenderOpts | undefined }[] = [] + const renderSlot: AppFrameProps['renderSlot'] = (key, owner, options) => { + slotCalls.push({ key, props: owner, options }) + return
    + } + const useSessions: AppFrameProps['useSessions'] = sel => sel({ ids: selectedSession === undefined ? [] : [selectedSession], byId: selectedSession === undefined ? {} : { [selectedSession]: { @@ -85,22 +78,29 @@ function mountFrame(windowWidth = frameWidth) { }, current: selectedSession, phase: 'ready', - } as SessionListState)) as AppFrameProps['useSessions'] + subagentsByParent: {}, + jobsBySession: {}, + currentAddress: undefined, + }) const workspaceState: WorkspaceSnapshot = { items: [], archivedSessionIds: [], state: 'idle', phase: 'ready', error: null, ...(workspacesReady ? {} : { state: 'loading' as const, phase: 'pending' as const }), } - const useStore = hookOf(instance) + const useStore = bindSnapshotSelector(instance) + const usePanelInfo = bindSnapshotSelector({ + getSnapshot: () => instance.getSnapshot().panelInfo, + subscribe: listener => instance.subscribe(listener), + }) const element = () => ( unknown) => sel(workspaceState)) as AppFrameProps['useWorkspaces']} - SessionProvider={SessionProviderStub} + useWorkspaces={sel => sel(workspaceState)} t={key => key === 'brand.localBuild' ? 'DSH Local Build' : key} /> ) @@ -202,22 +202,22 @@ describe('AppFrame', () => { expect(tracks(frame)).toEqual([280, 0]) expect(sidebarOwner()).toEqual({ collapsed: false, width: 280 }) expect(rightOwner()).toEqual({ width: 864, viewportWidth: 1920, canShow: true }) - expect(slotCalls.find(c => c.key === 'conversation')!.props).toEqual({}) + expect(slotCalls.find(c => c.key === 'main')).toEqual({ key: 'main', props: {}, options: { entryKey: 'conversation' } }) }) - it('retains conversation and sidebar content without a current Session', () => { + it('renders the main, sidebar, and root-scoped rightbar outlets without a current Session', () => { selectedSession = undefined - const { frame, getByTestId, queryByTestId } = mountFrame() - expect(getByTestId('conversation-content')).toBeTruthy() + const { frame, getByTestId } = mountFrame() + expect(getByTestId('main-content').getAttribute('data-entry-key')).toBe('conversation') expect(getByTestId('sidebar-content')).toBeTruthy() - expect(queryByTestId('rightbar-content')).toBeNull() + expect(getByTestId('rightbar-content')).toBeTruthy() expect(tracks(frame)).toEqual([280, 0]) }) it('renders both occupants before workspace baselines settle', () => { workspacesReady = false const { getByTestId } = mountFrame() - expect(getByTestId('conversation-content')).toBeTruthy() + expect(getByTestId('main-content').getAttribute('data-entry-key')).toBe('conversation') expect(getByTestId('rightbar-content')).toBeTruthy() }) @@ -229,16 +229,34 @@ describe('AppFrame', () => { expect(getByTestId('sidebar-content')).toBeTruthy() expect(frame.querySelector('[data-side="sidebar"]')).toBeNull() }) + + it('switches only the keyed main outlet when the active panel changes', () => { + selectedSessionTitle = 'Session title' + const { instance, frame, slotCalls, getByTestId } = mountFrame() + const sessionId = selectedSession + const layoutInfo = instance.getSnapshot().layoutInfo + for (const panelId of ['panel-a' as MainPanelId, 'panel-b' as MainPanelId, null]) { + slotCalls.length = 0 + act(() => { instance.actions.selectPanel(panelId) }) + expect(slotCalls).toEqual([{ key: 'main', props: {}, options: { entryKey: panelId ?? 'conversation' } }]) + expect(getByTestId('main-content').getAttribute('data-entry-key')).toBe(panelId ?? 'conversation') + expect(instance.getSnapshot().panelInfo).toEqual({ activePanelId: panelId }) + expect(instance.getSnapshot().layoutInfo).toBe(layoutInfo) + expect(tracks(frame)).toEqual([280, 0]) + expect(selectedSession).toBe(sessionId) + expect(document.title).toBe(panelId === null ? 'Session title — DSH Local Build' : 'DSH Local Build') + } + }) }) describe('AppFrame normal width concessions', () => { it('measures the frame, not the window, before choosing the first-open preference', () => { frameWidth = 1000 const { instance, rightOwner } = mountFrame(1920) - expect(instance.getSnapshot().viewportWidth).toBe(1000) + expect(instance.getSnapshot().layoutInfo.viewportWidth).toBe(1000) expect(rightOwner()).toEqual({ width: 450, viewportWidth: 1000, canShow: true }) act(() => { instance.actions.openRightbar(true, false) }) - expect(instance.getSnapshot().rightbar).toBe(450) + expect(instance.getSnapshot().layoutInfo.rightbar).toBe(450) resize(1920) expect(rightOwner().width).toBe(450) }) @@ -255,7 +273,7 @@ describe('AppFrame normal width concessions', () => { expect(tracks(frame)).toEqual([420, 0]) expect(rightOwner()).toEqual({ width: 0, viewportWidth: 1119, canShow: false }) expect(frame.querySelector('[data-side="rightbar"]')).toBeNull() - expect(instance.getSnapshot()).toMatchObject({ rightbarShown: true, rightbar: 864 }) + expect(instance.getSnapshot().layoutInfo).toMatchObject({ rightbarShown: true, rightbar: 864 }) act(() => { instance.actions.closeRightbar() }) resize(455) expect(tracks(frame)).toEqual([56, 0]) @@ -271,7 +289,7 @@ describe('AppFrame normal width concessions', () => { expect(rightOwner()).toEqual({ width: 344, viewportWidth: 800, canShow: true }) act(() => { instance.actions.openRightbar(true, false) }) expect(tracks(frame)).toEqual([56, 344]) - expect(instance.getSnapshot()).toMatchObject({ narrowExpanded: false, rightbar: 360 }) + expect(instance.getSnapshot().layoutInfo).toMatchObject({ narrowExpanded: false, rightbar: 360 }) expect(rightOwner().canShow).toBe(true) }) @@ -312,7 +330,7 @@ describe('AppFrame normal width concessions', () => { resize(980) act(() => { instance.actions.toggleSidebar() }) expect(tracks(frame)[0]).toBe(280) - expect(instance.getSnapshot().sidebar).toBe(0) + expect(instance.getSnapshot().layoutInfo.sidebar).toBe(0) }) }) @@ -404,7 +422,7 @@ describe('AppFrame right panel presentation', () => { act(() => { instance.actions.openRightbar(false, true) }) expect(tracks(frame)).toEqual([56, 0]) expect(rightOwner()).toEqual({ width: 0, viewportWidth: 700, canShow: false }) - expect(instance.getSnapshot().rightbarShown).toBe(true) + expect(instance.getSnapshot().layoutInfo.rightbarShown).toBe(true) expect(frame.querySelector('[data-side="rightbar"]')).toBeNull() }) @@ -415,7 +433,7 @@ describe('AppFrame right panel presentation', () => { expect(tracks(frame)).toEqual([280, 0]) expect(rightOwner().width).toBe(420) drag(handleFor(frame, 'rightbar'), 680, 690) - expect(instance.getSnapshot().rightbar).toBe(410) + expect(instance.getSnapshot().layoutInfo.rightbar).toBe(410) expect(rightOwner().width).toBe(410) expect(tracks(frame)[1]).toBe(0) }) @@ -436,7 +454,7 @@ describe('AppFrame pointer resizing', () => { act(flushFrames) expect(tracks(frame)[0]).toBe(360) pointer(handle, 'pointerup', 360) - expect(instance.getSnapshot().sidebar).toBe(360) + expect(instance.getSnapshot().layoutInfo.sidebar).toBe(360) expect(frame.dataset.dragging).toBeUndefined() expect(handle.hasPointerCapture(1)).toBe(false) }) @@ -450,7 +468,7 @@ describe('AppFrame pointer resizing', () => { expect(tracks(frame)[1]).toBe(420) expect(handle.style.left).toBe('680px') drag(handle, 680, 690) - expect(instance.getSnapshot().rightbar).toBe(410) + expect(instance.getSnapshot().layoutInfo.rightbar).toBe(410) expect(rightOwner().width).toBe(410) expect(tracks(frame)[1]).toBe(410) expect(handle.style.left).toBe('690px') @@ -474,10 +492,10 @@ describe('AppFrame pointer resizing', () => { pointer(handle, 'pointerdown', 280) pointer(handle, 'pointermove', 320) pointer(handle, 'pointerup', 360) - expect(instance.getSnapshot().sidebar).toBe(360) + expect(instance.getSnapshot().layoutInfo.sidebar).toBe(360) expect(animationFrames.size).toBe(0) act(flushFrames) - expect(instance.getSnapshot().sidebar).toBe(360) + expect(instance.getSnapshot().layoutInfo.sidebar).toBe(360) }) it('ignores uncaptured motion, secondary buttons, and a second pointer', () => { @@ -493,9 +511,9 @@ describe('AppFrame pointer resizing', () => { pointer(handle, 'pointermove', 500, 9) pointer(handle, 'pointerup', 500, 9) expect(animationFrames.size).toBe(0) - expect(instance.getSnapshot().sidebar).toBe(280) + expect(instance.getSnapshot().layoutInfo.sidebar).toBe(280) pointer(handle, 'pointerup', 300) - expect(instance.getSnapshot().sidebar).toBe(300) + expect(instance.getSnapshot().layoutInfo.sidebar).toBe(300) }) it.each(['pointercancel', 'lostpointercapture'])('ends %s without committing queued motion', (event) => { @@ -506,7 +524,7 @@ describe('AppFrame pointer resizing', () => { if (event === 'lostpointercapture') handle.releasePointerCapture(1) pointer(handle, event, 340) act(flushFrames) - expect(instance.getSnapshot().sidebar).toBe(280) + expect(instance.getSnapshot().layoutInfo.sidebar).toBe(280) expect(animationFrames.size).toBe(0) expect(frame.dataset.dragging).toBeUndefined() expect(handle.hasPointerCapture(1)).toBe(false) @@ -526,7 +544,7 @@ describe('AppFrame pointer resizing', () => { const settled = instance.getSnapshot() act(flushFrames) expect(instance.getSnapshot()).toBe(settled) - expect(instance.getSnapshot().rightbar).toBe(864) + expect(instance.getSnapshot().layoutInfo.rightbar).toBe(864) expect(animationFrames.size).toBe(0) expect(handle.hasPointerCapture(1)).toBe(false) if (change !== 'unmount') expect(frame.dataset.dragging).toBeUndefined() @@ -544,17 +562,17 @@ describe('AppFrame frame measurement lifecycle', () => { observer.fire() }) expect(animationFrames.size).toBe(1) - expect(instance.getSnapshot().viewportWidth).toBe(1920) + expect(instance.getSnapshot().layoutInfo.viewportWidth).toBe(1920) act(flushFrames) - expect(instance.getSnapshot().viewportWidth).toBe(1200) + expect(instance.getSnapshot().layoutInfo.viewportWidth).toBe(1200) expect(rightOwner().viewportWidth).toBe(1200) - expect(instance.getSnapshot().rightbar).toBeNull() + expect(instance.getSnapshot().layoutInfo.rightbar).toBeNull() }) it('retains the last positive measurement while the frame is hidden', () => { const { instance, rightOwner } = mountFrame() resize(0) - expect(instance.getSnapshot().viewportWidth).toBe(1920) + expect(instance.getSnapshot().layoutInfo.viewportWidth).toBe(1920) expect(rightOwner().viewportWidth).toBe(1920) }) @@ -568,7 +586,7 @@ describe('AppFrame frame measurement lifecycle', () => { expect(observer.disconnected).toBe(true) expect(animationFrames.size).toBe(0) act(() => { observer.fire(); flushFrames() }) - expect(instance.getSnapshot().viewportWidth).toBe(1920) + expect(instance.getSnapshot().layoutInfo.viewportWidth).toBe(1920) expect(animationFrames.size).toBe(0) }) }) diff --git a/packages/client/ui-layout/tests/apply.client.spec.ts b/packages/client/ui-layout/tests/apply.client.spec.ts index 38732eda2c..87123fc922 100644 --- a/packages/client/ui-layout/tests/apply.client.spec.ts +++ b/packages/client/ui-layout/tests/apply.client.spec.ts @@ -1,20 +1,58 @@ // @vitest-environment jsdom -import { Context } from '@deepseek-ai/cordis' +import { Context, type Fiber } from '@deepseek-ai/cordis' import { stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client' +import type { SlotRendererHost } from '@deepseek-ai/dsh-client-ui-slots' import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client' import { apply as themeApply, inject as themeInject, ThemeRuntime } from '@deepseek-ai/dsh-client-ui-theme/client' import { apply, inject, LayoutController } from '@deepseek-ai/dsh-client-ui-layout/client' import { apply as nodeApply } from '@deepseek-ai/dsh-client-ui-layout' +import type { MainPanelId } from '../src/client/service.ts' +import type { createLayoutStore } from '../src/client/stores.ts' + +const owners = new Set() +let originalRootStyle: string | null +let originalBodyStyle: string | null +let originalDarkTheme: string | null +let originalThemeMetadata: Element[] beforeEach(() => { - document.head.querySelectorAll('meta[name="theme-color"]').forEach((node) => { node.remove() }) + originalRootStyle = document.documentElement.getAttribute('style') + originalBodyStyle = document.body.getAttribute('style') + originalDarkTheme = document.body.getAttribute('data-ds-dark-theme') + originalThemeMetadata = [...document.head.querySelectorAll('meta[name="theme-color"]')] + originalThemeMetadata.forEach(node => { node.remove() }) + vi.stubGlobal('innerWidth', 1920) }) +afterEach(async () => { + try { + for (const owner of owners) await owner.dispose() + } finally { + owners.clear() + restoreAttribute(document.documentElement, 'style', originalRootStyle) + restoreAttribute(document.body, 'style', originalBodyStyle) + restoreAttribute(document.body, 'data-ds-dark-theme', originalDarkTheme) + document.head.querySelectorAll('meta[name="theme-color"]').forEach(node => { node.remove() }) + document.head.append(...originalThemeMetadata) + vi.unstubAllGlobals() + } +}) + +function restoreAttribute(element: Element, name: string, value: string | null): void { + if (value === null) element.removeAttribute(name) + else element.setAttribute(name, value) +} + async function bench() { - const ctx = new Context() + const root = new Context() + let ctx: Context | undefined + const owner = root.plugin((ownedContext: Context) => { ctx = ownedContext }) + owners.add(owner) + await owner.await() + if (ctx === undefined) throw new Error('the fixture owner did not activate') const slotsFiber = ctx.plugin(SlotRegistry) // Theme registers its Appearance settings row and requires the connection // seam for persistence; model this bench as a remote, memory-only browser. @@ -25,7 +63,15 @@ async function bench() { ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) await ctx.plugin({ inject: themeInject, apply: themeApply }).await() await slotsFiber.await() - return { ctx, slots: ctx.get('slots') as SlotRegistry } + const slots = ctx.get('slots') as SlotRegistry + let host: SlotRendererHost | undefined + slots.install({ renderRoot: value => { host = value; return null } }) + const rendererHost = (): SlotRendererHost => { + slots.renderSlot('root', {}) + if (host === undefined) throw new Error('the root renderer did not receive its host') + return host + } + return { ctx, slots, rendererHost } } describe('ui-layout client apply', () => { @@ -33,30 +79,41 @@ describe('ui-layout client apply', () => { expect(inject).toEqual(['slots', 'theme', 'locale']) }) - it('provides ctx.layout and registers AppFrame into root with the three child declarations', async () => { + it('provides ctx.layout and declares the four root-scoped frame slots', async () => { const { ctx, slots } = await bench() const fiber = ctx.plugin({ inject: [...inject], apply }) await fiber.await() expect(ctx.get('layout')).toBeInstanceOf(LayoutController) - // The one register() call occupied 'root'… expect(slots.entries('root')).toHaveLength(1) - // …and declared the three children in the ledger. expect(slots.spec('sidebar')).toEqual({ kind: 'single', scope: 'root' }) - expect(slots.spec('conversation')).toEqual({ kind: 'single', scope: 'session-maybe' }) + expect(slots.spec('main')).toEqual({ kind: 'keyed', scope: 'root' }) + expect(slots.spec('rightbar')).toEqual({ kind: 'single', scope: 'root' }) + expect(slots.spec('shell.overlay')).toEqual({ kind: 'list', scope: 'root' }) }) - it('injects no business face and attaches the layout actions', async () => { - const { ctx, slots } = await bench() + it('shares a pre-created instance between service actions, root rendering, and panelInfo', async () => { + const { ctx, slots, rendererHost } = await bench() const fiber = ctx.plugin({ inject: [...inject], apply }) await fiber.await() - const actions = { - setSidebar: vi.fn(), setDetails: vi.fn(), toggleSidebar: vi.fn(), openDetails: vi.fn(), closeDetails: vi.fn(), - } - const injected = (slots.entries('root')[0]!.inject as (actions: never) => object)(actions as never) - expect(injected).toEqual({}) + const entry = slots.entries('root')[0]! + expect(entry.inject).toBeUndefined() + const handle = entry.store as ReturnType + const instance = handle.create() + expect(handle.create()).toBe(instance) const layout = ctx.get('layout') as LayoutController layout.toggleSidebar() - expect(actions.toggleSidebar).toHaveBeenCalledOnce() + expect(instance.getSnapshot().layoutInfo.sidebar).toBe(0) + const host = rendererHost() + expect(host.storeOf(entry, undefined)).toBe(instance) + const panelInfo = host.root.getSnapshot().hooks.panelInfo! + expect(panelInfo.getSnapshot()).toBe(instance.getSnapshot().panelInfo) + const panelId = 'panel-a' as MainPanelId + const disposePanel = slots.register({ name: 'main', key: panelId }, () => null) + layout.selectPanel(panelId) + expect(panelInfo.getSnapshot()).toEqual({ activePanelId: panelId }) + disposePanel() + await vi.waitFor(() => { expect(panelInfo.getSnapshot()).toEqual({ activePanelId: null }) }) + expect(instance.getSnapshot().layoutInfo.sidebar).toBe(0) }) it('theme presenter applies the initial snapshot, follows theme/change, and unwinds on dispose', async () => { @@ -85,13 +142,19 @@ describe('ui-layout client apply', () => { }) it('teardown unwinds the service, the root registration, and the child declarations', async () => { - const { ctx, slots } = await bench() + const { ctx, slots, rendererHost } = await bench() const fiber = ctx.plugin({ inject: [...inject], apply }) await fiber.await() + const host = rendererHost() + expect(host.root.getSnapshot().hooks.panelInfo).toBeDefined() await fiber.dispose() expect(ctx.get('layout')).toBeUndefined() expect(slots.entries('root')).toHaveLength(0) expect(slots.spec('sidebar')).toBeUndefined() + expect(slots.spec('main')).toBeUndefined() + expect(slots.spec('rightbar')).toBeUndefined() + expect(slots.spec('shell.overlay')).toBeUndefined() + expect(host.root.getSnapshot().hooks.panelInfo).toBeUndefined() // The built-in root declaration survives entry teardown (renderer-owned). expect(slots.spec('root')).toEqual({ kind: 'single', scope: 'root' }) }) diff --git a/packages/client/ui-layout/tests/document-title.client.spec.tsx b/packages/client/ui-layout/tests/document-title.client.spec.tsx index 2cec953cb1..87def87bda 100644 --- a/packages/client/ui-layout/tests/document-title.client.spec.tsx +++ b/packages/client/ui-layout/tests/document-title.client.spec.tsx @@ -1,36 +1,82 @@ // @vitest-environment jsdom -import { afterEach, describe, expect, it, vi } from 'vitest' -import { cleanup, render } from '@testing-library/react' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { act, cleanup, render } from '@testing-library/react' +import { createSnapshotStore } from '@deepseek-ai/dsh-client-store' +import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime' +import type { SessionListState } from '@deepseek-ai/dsh-api-session-controller/client' +import type { SessionId } from '@deepseek-ai/dsh-session/types' import { DocumentTitle } from '../src/client/DocumentTitle.tsx' +import type { MainPanelId, PanelInfo } from '../src/client/service.ts' +let originalTitle: string +beforeEach(() => { originalTitle = document.title }) afterEach(() => { - cleanup() - document.title = '' - vi.unstubAllEnvs() + try { cleanup() } finally { document.title = originalTitle } }) +function titleSources() { + const sessionId = 'session-title' as SessionId + const sessions = createSnapshotStore({ + ids: [sessionId], + byId: { [sessionId]: { id: sessionId, displayTitle: 'Test', running: false, blank: false, updatedAt: 1 } }, + current: sessionId, + phase: 'ready', + subagentsByParent: {}, + jobsBySession: {}, + currentAddress: undefined, + }) + const panelInfo = createSnapshotStore({ activePanelId: null }) + return { + sessionId, sessions, panelInfo, + props: { useSessions: bindSnapshotSelector(sessions), usePanelInfo: bindSnapshotSelector(panelInfo) }, + } +} + describe('DocumentTitle', () => { it('projects a durable title and restores the product title', () => { - vi.stubEnv('DSH_CLIENT_TITLE', 'DeepSeek Harness') + const { sessionId, sessions, props } = titleSources() document.title = 'stale title' - const mounted = render() + const mounted = render() expect(document.title).toBe('DeepSeek Harness') - mounted.rerender() + act(() => { sessions.update(state => { state.byId[sessionId]!.title = 'First title' }) }) expect(document.title).toBe('First title — DeepSeek Harness') - mounted.rerender() + act(() => { sessions.update(state => { state.byId[sessionId]!.title = 'Revised title' }) }) expect(document.title).toBe('Revised title — DeepSeek Harness') - mounted.rerender() + act(() => { sessions.update(state => { state.current = undefined }) }) expect(document.title).toBe('DeepSeek Harness') mounted.unmount() expect(document.title).toBe('DeepSeek Harness') }) - it('uses the generic title when the build provides no title', () => { - vi.stubEnv('DSH_CLIENT_TITLE', '') - delete process.env.DSH_CLIENT_TITLE - const mounted = render() + it('uses the localized product title supplied by the frame', () => { + const { sessionId, sessions, props } = titleSources() + sessions.update(state => { state.byId[sessionId]!.title = 'First title' }) + const mounted = render() expect(document.title).toBe('First title — DSH Local Build') mounted.unmount() expect(document.title).toBe('DSH Local Build') }) + + it('keeps the product title across global panels and restores the latest Session title on return', () => { + const { sessionId, sessions, panelInfo, props } = titleSources() + sessions.update(state => { state.byId[sessionId]!.title = 'Session title' }) + render() + expect(document.title).toBe('Session title — Product') + act(() => { panelInfo.set({ activePanelId: 'panel-a' as MainPanelId }) }) + expect(document.title).toBe('Product') + act(() => { sessions.update(state => { state.byId[sessionId]!.title = 'Updated title' }) }) + expect(document.title).toBe('Product') + act(() => { panelInfo.set({ activePanelId: 'panel-b' as MainPanelId }) }) + expect(document.title).toBe('Product') + expect(sessions.getSnapshot().current).toBe(sessionId) + act(() => { panelInfo.set({ activePanelId: null }) }) + expect(document.title).toBe('Updated title — Product') + }) + + it('uses the product title when the current Session row is not available', () => { + const { sessions, props } = titleSources() + sessions.update(state => { state.byId = {}; state.ids = [] }) + render() + expect(document.title).toBe('Product') + }) }) diff --git a/packages/client/ui-layout/tests/layout-store.client.spec.ts b/packages/client/ui-layout/tests/layout-store.client.spec.ts index 60dacf2f4b..edf2a5a3c9 100644 --- a/packages/client/ui-layout/tests/layout-store.client.spec.ts +++ b/packages/client/ui-layout/tests/layout-store.client.spec.ts @@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { createLayoutStore } from '../src/client/stores.ts' +import type { MainPanelId } from '../src/client/service.ts' beforeEach(() => { vi.stubGlobal('innerWidth', 1920) }) afterEach(() => { vi.unstubAllGlobals(); vi.restoreAllMocks() }) @@ -10,14 +11,17 @@ describe('createLayoutStore', () => { it('starts with the default sidebar and no right panel preference', () => { const { store } = createLayoutStore().create() expect(store.getSnapshot()).toEqual({ - sidebar: 280, - viewportWidth: 1920, - narrowExpanded: false, - rightbar: null, - rightbarShown: false, - rightbarTrack: false, - rightbarFullscreen: false, - rightbarInstant: false, + panelInfo: { activePanelId: null }, + layoutInfo: { + sidebar: 280, + viewportWidth: 1920, + narrowExpanded: false, + rightbar: null, + rightbarShown: false, + rightbarTrack: false, + rightbarFullscreen: false, + rightbarInstant: false, + }, }) }) @@ -27,26 +31,26 @@ describe('createLayoutStore', () => { const b = createLayoutStore().create() a.actions.setSidebar(400) a.actions.openRightbar(true, false) - expect(b.store.getSnapshot().sidebar).toBe(280) - expect(b.store.getSnapshot().rightbar).toBeNull() + expect(b.store.getSnapshot().layoutInfo.sidebar).toBe(280) + expect(b.store.getSnapshot().layoutInfo.rightbar).toBeNull() expect(write).not.toHaveBeenCalled() }) it('clamps the sidebar to 264–420px', () => { const { store, actions } = createLayoutStore().create() actions.setSidebar(1) - expect(store.getSnapshot().sidebar).toBe(264) + expect(store.getSnapshot().layoutInfo.sidebar).toBe(264) actions.setSidebar(9999) - expect(store.getSnapshot().sidebar).toBe(420) + expect(store.getSnapshot().layoutInfo.sidebar).toBe(420) }) it('toggles the wide sidebar between closed and default width', () => { const { store, actions } = createLayoutStore().create() actions.setSidebar(400) actions.toggleSidebar() - expect(store.getSnapshot().sidebar).toBe(0) + expect(store.getSnapshot().layoutInfo.sidebar).toBe(0) actions.toggleSidebar() - expect(store.getSnapshot().sidebar).toBe(280) + expect(store.getSnapshot().layoutInfo.sidebar).toBe(280) }) it('keeps the sidebar preference while toggling its narrow override', () => { @@ -54,9 +58,9 @@ describe('createLayoutStore', () => { actions.setSidebar(400) actions.setViewportWidth(980) actions.toggleSidebar() - expect(store.getSnapshot()).toMatchObject({ sidebar: 400, viewportWidth: 980, narrowExpanded: true }) + expect(store.getSnapshot().layoutInfo).toMatchObject({ sidebar: 400, viewportWidth: 980, narrowExpanded: true }) actions.toggleSidebar() - expect(store.getSnapshot()).toMatchObject({ sidebar: 400, narrowExpanded: false }) + expect(store.getSnapshot().layoutInfo).toMatchObject({ sidebar: 400, narrowExpanded: false }) }) it('clears the manual override only when crossing 1024px', () => { @@ -65,39 +69,101 @@ describe('createLayoutStore', () => { actions.toggleSidebar() actions.setViewportWidth(980) actions.setViewportWidth(1023) - expect(store.getSnapshot().narrowExpanded).toBe(true) + expect(store.getSnapshot().layoutInfo.narrowExpanded).toBe(true) actions.setViewportWidth(1024) - expect(store.getSnapshot().narrowExpanded).toBe(false) + expect(store.getSnapshot().layoutInfo.narrowExpanded).toBe(false) actions.setViewportWidth(980) - expect(store.getSnapshot().narrowExpanded).toBe(false) + expect(store.getSnapshot().layoutInfo.narrowExpanded).toBe(false) }) }) +describe('main panel selection', () => { + const panelA = 'panel-a' as MainPanelId + const panelB = 'panel-b' as MainPanelId + + it('changes only panelInfo when switching panels and returning to the Conversation', () => { + const { store, actions } = createLayoutStore().create() + actions.setSidebar(400) + actions.openRightbar(true, true) + actions.closeRightbar() + const layoutInfo = store.getSnapshot().layoutInfo + for (const activePanelId of [panelA, panelB, null]) { + const previousPanelInfo = store.getSnapshot().panelInfo + actions.selectPanel(activePanelId) + expect(store.getSnapshot().panelInfo).toEqual({ activePanelId }) + expect(store.getSnapshot().panelInfo).not.toBe(previousPanelInfo) + expect(store.getSnapshot().layoutInfo).toBe(layoutInfo) + } + }) + + it('keeps the complete snapshot when selecting the current panel again', () => { + const { store, actions } = createLayoutStore().create() + actions.selectPanel(panelA) + const selected = store.getSnapshot() + actions.selectPanel(panelA) + expect(store.getSnapshot()).toBe(selected) + expect(store.getSnapshot().panelInfo.activePanelId).toBe(panelA) + }) + + it('returns to the Conversation only when the selected main registration disappears', () => { + const { store, actions } = createLayoutStore().create() + const initial = store.getSnapshot() + actions.retainMainPanels([]) + expect(store.getSnapshot()).toBe(initial) + actions.selectPanel(panelA) + const selected = store.getSnapshot() + actions.retainMainPanels(['conversation', panelA, panelB]) + expect(store.getSnapshot()).toBe(selected) + actions.retainMainPanels(['conversation', panelB]) + expect(store.getSnapshot().panelInfo).toEqual({ activePanelId: null }) + expect(store.getSnapshot().layoutInfo).toBe(selected.layoutInfo) + }) + + it.each(['setSidebar', 'toggleSidebar', 'setViewportWidth', 'setRightbar', 'openRightbar', 'closeRightbar'] as const)( + 'preserves panelInfo identity when %s changes layoutInfo', (action) => { + const { store, actions } = createLayoutStore().create() + actions.selectPanel(panelA) + if (action === 'closeRightbar') actions.openRightbar(true, true) + const previous = store.getSnapshot() + switch (action) { + case 'setSidebar': actions.setSidebar(400); break + case 'toggleSidebar': actions.toggleSidebar(); break + case 'setViewportWidth': actions.setViewportWidth(980); break + case 'setRightbar': actions.setRightbar(500); break + case 'openRightbar': actions.openRightbar(true, true); break + case 'closeRightbar': actions.closeRightbar(); break + } + expect(store.getSnapshot().panelInfo).toBe(previous.panelInfo) + expect(store.getSnapshot().layoutInfo).not.toBe(previous.layoutInfo) + }, + ) +}) + describe('right panel', () => { it('initializes at 45% of the latest frame only on first opening', () => { const { store, actions } = createLayoutStore().create() actions.setViewportWidth(1000) - expect(store.getSnapshot().rightbar).toBeNull() + expect(store.getSnapshot().layoutInfo.rightbar).toBeNull() actions.openRightbar(true, false) - expect(store.getSnapshot().rightbar).toBe(450) + expect(store.getSnapshot().layoutInfo.rightbar).toBe(450) actions.setViewportWidth(2000) actions.openRightbar(true, true) - expect(store.getSnapshot().rightbar).toBe(450) + expect(store.getSnapshot().layoutInfo.rightbar).toBe(450) actions.closeRightbar() actions.openRightbar(true, false) - expect(store.getSnapshot().rightbar).toBe(450) + expect(store.getSnapshot().layoutInfo.rightbar).toBe(450) }) it('keeps track and fullscreen reports independent and clears both on close', () => { const { store, actions } = createLayoutStore().create() actions.openRightbar(true, false) - expect(store.getSnapshot()).toMatchObject({ rightbarShown: true, rightbarTrack: true, rightbarFullscreen: false }) + expect(store.getSnapshot().layoutInfo).toMatchObject({ rightbarShown: true, rightbarTrack: true, rightbarFullscreen: false }) actions.openRightbar(true, true) - expect(store.getSnapshot()).toMatchObject({ rightbarShown: true, rightbarTrack: true, rightbarFullscreen: true }) + expect(store.getSnapshot().layoutInfo).toMatchObject({ rightbarShown: true, rightbarTrack: true, rightbarFullscreen: true }) actions.openRightbar(false, true) - expect(store.getSnapshot()).toMatchObject({ rightbarShown: true, rightbarTrack: false, rightbarFullscreen: true }) + expect(store.getSnapshot().layoutInfo).toMatchObject({ rightbarShown: true, rightbarTrack: false, rightbarFullscreen: true }) actions.closeRightbar() - expect(store.getSnapshot()).toMatchObject({ rightbarShown: false, rightbarTrack: false, rightbarFullscreen: false }) + expect(store.getSnapshot().layoutInfo).toMatchObject({ rightbarShown: false, rightbarTrack: false, rightbarFullscreen: false }) }) it('keeps dragged px preferences across resize, close, and reopen', () => { @@ -105,29 +171,29 @@ describe('right panel', () => { actions.openRightbar(true, false) actions.setRightbar(1100) actions.setViewportWidth(800) - expect(store.getSnapshot().rightbar).toBe(1100) + expect(store.getSnapshot().layoutInfo.rightbar).toBe(1100) actions.closeRightbar() actions.openRightbar(false, true) - expect(store.getSnapshot().rightbar).toBe(1100) + expect(store.getSnapshot().layoutInfo.rightbar).toBe(1100) }) it('clamps drag preferences to 300px and 70% of the current frame', () => { const { store, actions } = createLayoutStore().create() actions.setViewportWidth(1600) actions.setRightbar(9999) - expect(store.getSnapshot().rightbar).toBe(1120) + expect(store.getSnapshot().layoutInfo.rightbar).toBe(1120) actions.setViewportWidth(1000) actions.setRightbar(9999) - expect(store.getSnapshot().rightbar).toBe(700) + expect(store.getSnapshot().layoutInfo.rightbar).toBe(700) actions.setRightbar(1) - expect(store.getSnapshot().rightbar).toBe(300) + expect(store.getSnapshot().layoutInfo.rightbar).toBe(300) }) it('retains a minimum normal preference when first opened fullscreen on a phone', () => { const { store, actions } = createLayoutStore().create() actions.setViewportWidth(320) actions.openRightbar(false, true) - expect(store.getSnapshot().rightbar).toBe(300) + expect(store.getSnapshot().layoutInfo.rightbar).toBe(300) }) it('collapses a manually expanded narrow sidebar on opening, not presentation reports', () => { @@ -136,23 +202,23 @@ describe('right panel', () => { actions.setViewportWidth(800) actions.toggleSidebar() actions.openRightbar(true, false) - expect(store.getSnapshot()).toMatchObject({ sidebar: 400, narrowExpanded: false }) + expect(store.getSnapshot().layoutInfo).toMatchObject({ sidebar: 400, narrowExpanded: false }) actions.toggleSidebar() actions.openRightbar(true, true) - expect(store.getSnapshot().narrowExpanded).toBe(true) + expect(store.getSnapshot().layoutInfo.narrowExpanded).toBe(true) actions.closeRightbar() actions.openRightbar(true, false) - expect(store.getSnapshot().narrowExpanded).toBe(false) + expect(store.getSnapshot().layoutInfo.narrowExpanded).toBe(false) }) it('keeps the wide sidebar preference and never opens a closed right panel on resize', () => { const { store, actions } = createLayoutStore().create() actions.setSidebar(420) actions.openRightbar(true, false) - expect(store.getSnapshot().sidebar).toBe(420) + expect(store.getSnapshot().layoutInfo.sidebar).toBe(420) actions.closeRightbar() actions.setViewportWidth(3000) - expect(store.getSnapshot()).toMatchObject({ sidebar: 420, rightbarShown: false, rightbarTrack: false }) + expect(store.getSnapshot().layoutInfo).toMatchObject({ sidebar: 420, rightbarShown: false, rightbarTrack: false }) }) }) @@ -161,7 +227,7 @@ describe('right panel instant geometry', () => { const { store, actions } = createLayoutStore().create() actions.openRightbar(track, true) actions.closeRightbar() - expect(store.getSnapshot()).toMatchObject({ + expect(store.getSnapshot().layoutInfo).toMatchObject({ rightbarShown: false, rightbarTrack: false, rightbarFullscreen: false, rightbarInstant: true, }) const closed = store.getSnapshot() @@ -173,38 +239,38 @@ describe('right panel instant geometry', () => { const { store, actions } = createLayoutStore().create() actions.openRightbar(true, true) actions.openRightbar(true, false) - expect(store.getSnapshot()).toMatchObject({ + expect(store.getSnapshot().layoutInfo).toMatchObject({ rightbarShown: true, rightbarTrack: true, rightbarFullscreen: false, rightbarInstant: true, }) const restored = store.getSnapshot() actions.openRightbar(true, false) expect(store.getSnapshot()).toBe(restored) actions.openRightbar(false, false) - expect(store.getSnapshot().rightbarInstant).toBe(false) + expect(store.getSnapshot().layoutInfo.rightbarInstant).toBe(false) }) it('allows a normal close to animate, including after restoring from fullscreen', () => { const { store, actions } = createLayoutStore().create() actions.openRightbar(true, false) actions.closeRightbar() - expect(store.getSnapshot().rightbarInstant).toBe(false) + expect(store.getSnapshot().layoutInfo.rightbarInstant).toBe(false) actions.openRightbar(true, true) actions.openRightbar(true, false) - expect(store.getSnapshot().rightbarInstant).toBe(true) + expect(store.getSnapshot().layoutInfo.rightbarInstant).toBe(true) actions.closeRightbar() - expect(store.getSnapshot()).toMatchObject({ rightbarTrack: false, rightbarInstant: false }) + expect(store.getSnapshot().layoutInfo).toMatchObject({ rightbarTrack: false, rightbarInstant: false }) }) it.each(['setSidebar', 'toggleSidebar', 'setRightbar', 'setViewportWidth'] as const)('clears instant geometry on %s', (action) => { const { store, actions } = createLayoutStore().create() actions.openRightbar(true, true) actions.closeRightbar() - expect(store.getSnapshot().rightbarInstant).toBe(true) + expect(store.getSnapshot().layoutInfo.rightbarInstant).toBe(true) if (action === 'toggleSidebar') actions.toggleSidebar() else actions[action](action === 'setViewportWidth' ? 1800 : 350) - expect(store.getSnapshot().rightbarInstant).toBe(false) + expect(store.getSnapshot().layoutInfo.rightbarInstant).toBe(false) actions.closeRightbar() - expect(store.getSnapshot().rightbarInstant).toBe(false) + expect(store.getSnapshot().layoutInfo.rightbarInstant).toBe(false) }) it('does not let an unchanged frame measurement reset the fullscreen-exit marker', () => { @@ -212,7 +278,7 @@ describe('right panel instant geometry', () => { actions.openRightbar(true, true) actions.closeRightbar() const closed = store.getSnapshot() - actions.setViewportWidth(closed.viewportWidth) + actions.setViewportWidth(closed.layoutInfo.viewportWidth) expect(store.getSnapshot()).toBe(closed) }) @@ -221,6 +287,6 @@ describe('right panel instant geometry', () => { actions.openRightbar(true, true) actions.closeRightbar() actions.openRightbar(true, fullscreen) - expect(store.getSnapshot()).toMatchObject({ rightbarShown: true, rightbarFullscreen: fullscreen, rightbarInstant: false }) + expect(store.getSnapshot().layoutInfo).toMatchObject({ rightbarShown: true, rightbarFullscreen: fullscreen, rightbarInstant: false }) }) }) diff --git a/packages/client/ui-layout/tests/service.client.spec.ts b/packages/client/ui-layout/tests/service.client.spec.ts index a95c6a5b44..14c3fb479e 100644 --- a/packages/client/ui-layout/tests/service.client.spec.ts +++ b/packages/client/ui-layout/tests/service.client.spec.ts @@ -1,9 +1,11 @@ import { describe, expect, it, vi } from 'vitest' import { LayoutController } from '../src/client/service.ts' -import type { PanelActions } from '../src/client/service.ts' +import type { MainPanelId, PanelActions } from '../src/client/service.ts' function fakePanels(): PanelActions { return { + selectPanel: vi.fn(), + retainMainPanels: vi.fn(), setSidebar: vi.fn(), toggleSidebar: vi.fn(), setViewportWidth: vi.fn(), @@ -14,10 +16,9 @@ function fakePanels(): PanelActions { } describe('LayoutController', () => { - it('forwards the right column transitions to the attached set', () => { - const service = new LayoutController() + it('forwards right column transitions to the constructor-supplied actions', () => { const panels = fakePanels() - service.attachPanels(panels) + const service = new LayoutController(panels) service.openRightbar(true, false) service.openRightbar(true, true) @@ -32,10 +33,9 @@ describe('LayoutController', () => { expect(panels.setRightbar).not.toHaveBeenCalled() }) - it('forwards the three panel actions to the attached set', () => { - const service = new LayoutController() + it('can toggle the sidebar immediately after construction', () => { const panels = fakePanels() - service.attachPanels(panels) + const service = new LayoutController(panels) service.toggleSidebar() @@ -43,21 +43,31 @@ describe('LayoutController', () => { expect(panels.setSidebar).not.toHaveBeenCalled() }) - it('fails loud before the root entry wired its actions', () => { - const service = new LayoutController() - expect(() => { service.toggleSidebar() }).toThrow(/panel actions not wired/) + it('forwards panel selection and returning to the Conversation without changing geometry', () => { + const panels = fakePanels() + const service = new LayoutController(panels) + const panelId = 'panel-a' as MainPanelId + service.selectPanel(panelId) + service.selectPanel(panelId) + service.selectPanel(null) + expect(panels.selectPanel).toHaveBeenNthCalledWith(1, panelId) + expect(panels.selectPanel).toHaveBeenNthCalledWith(2, panelId) + expect(panels.selectPanel).toHaveBeenNthCalledWith(3, null) + expect(panels.toggleSidebar).not.toHaveBeenCalled() + expect(panels.openRightbar).not.toHaveBeenCalled() + expect(panels.closeRightbar).not.toHaveBeenCalled() }) - it('re-attach overwrites the stale action set (entry re-register)', () => { - const service = new LayoutController() - const stale = fakePanels() - const fresh = fakePanels() - service.attachPanels(stale) - service.attachPanels(fresh) - - service.toggleSidebar() - - expect(stale.toggleSidebar).not.toHaveBeenCalled() - expect(fresh.toggleSidebar).toHaveBeenCalledTimes(1) + it('keeps separately constructed controllers bound to their own instances', () => { + const first = fakePanels() + const second = fakePanels() + const firstService = new LayoutController(first) + const secondService = new LayoutController(second) + firstService.toggleSidebar() + expect(first.toggleSidebar).toHaveBeenCalledTimes(1) + expect(second.toggleSidebar).not.toHaveBeenCalled() + secondService.closeRightbar() + expect(first.closeRightbar).not.toHaveBeenCalled() + expect(second.closeRightbar).toHaveBeenCalledTimes(1) }) }) diff --git a/packages/client/ui-permission-presets/tests/permission-presets-row.client.spec.tsx b/packages/client/ui-permission-presets/tests/permission-presets-row.client.spec.tsx index 5ef380f656..b1af331463 100644 --- a/packages/client/ui-permission-presets/tests/permission-presets-row.client.spec.tsx +++ b/packages/client/ui-permission-presets/tests/permission-presets-row.client.spec.tsx @@ -13,6 +13,7 @@ import { PermissionPresetSettingsController } from '../src/client/settings-store // Every fixture carries the resource hook the resources plugin merges into GlobalStandardProps. const useResource = (() => ({ status: 'none' as const, value: undefined, failure: undefined, reload: () => {} })) as GlobalStandardProps['useResource'] +const usePanelInfo: GlobalStandardProps['usePanelInfo'] = selector => selector({ activePanelId: null }) const schema = new SettingsSchemaService(new Context()) @@ -60,7 +61,7 @@ const useSessionPendingInteraction: PermissionRowProps['useSessionPendingInterac const runtime = { useSessions: (() => { throw new Error('unused') }) as never, useSessionPendingInteraction, - useResource, + usePanelInfo, useResource, useWorkspaces: (() => { throw new Error('unused') }) as never, } diff --git a/packages/client/ui-settings-general/tests/components.client.spec.tsx b/packages/client/ui-settings-general/tests/components.client.spec.tsx index c3daeaaeb6..c9cb8eadad 100644 --- a/packages/client/ui-settings-general/tests/components.client.spec.tsx +++ b/packages/client/ui-settings-general/tests/components.client.spec.tsx @@ -13,6 +13,7 @@ import { SettingsDocumentStore } from '../src/client/settings-document-store.ts' // Every fixture carries the resource hook the resources plugin merges into GlobalStandardProps. const useResource = (() => ({ status: 'none' as const, value: undefined, failure: undefined, reload: () => {} })) as GlobalStandardProps['useResource'] +const usePanelInfo: GlobalStandardProps['usePanelInfo'] = selector => selector({ activePanelId: null }) /** Store over a real mirror derived from the same scripted context. */ function derivedDocumentStore(remote: object) { @@ -32,7 +33,7 @@ const unusedHook = (() => { throw new Error('unused by settings-general componen type AttentionSnapshot = Parameters[0]>[0] const noAttention: AttentionSnapshot = new Map() const useSessionPendingInteraction: TriggerContentProps['useSessionPendingInteraction'] = selector => selector(noAttention) -const kit = { useSessions: unusedHook, useSessionPendingInteraction, useResource, useWorkspaces: unusedHook } +const kit = { useSessions: unusedHook, useSessionPendingInteraction, usePanelInfo, useResource, useWorkspaces: unusedHook } describe('chrome content', () => { it('TriggerContent renders the icon with the label in the wide column', () => { diff --git a/packages/client/ui-settings-general/tests/settings-root.client.spec.tsx b/packages/client/ui-settings-general/tests/settings-root.client.spec.tsx index 3ebce030b4..bb936d5659 100644 --- a/packages/client/ui-settings-general/tests/settings-root.client.spec.tsx +++ b/packages/client/ui-settings-general/tests/settings-root.client.spec.tsx @@ -10,6 +10,7 @@ import { en } from '../src/client/locales.ts' // Every fixture carries the resource hook the resources plugin merges into GlobalStandardProps. const useResource = (() => ({ status: 'none' as const, value: undefined, failure: undefined, reload: () => {} })) as GlobalStandardProps['useResource'] +const usePanelInfo: GlobalStandardProps['usePanelInfo'] = selector => selector({ activePanelId: null }) afterEach(() => { cleanup() @@ -76,7 +77,7 @@ function mount({ const props: SettingsRootComponentProps = { useSessions, useSessionPendingInteraction, - useResource, + usePanelInfo, useResource, useWorkspaces: unusedHook, wide, reconnect, diff --git a/packages/client/ui-settings-models/tests/onboarding-dialog.client.spec.tsx b/packages/client/ui-settings-models/tests/onboarding-dialog.client.spec.tsx index 9f10184eaa..f4a05c9363 100644 --- a/packages/client/ui-settings-models/tests/onboarding-dialog.client.spec.tsx +++ b/packages/client/ui-settings-models/tests/onboarding-dialog.client.spec.tsx @@ -17,6 +17,7 @@ import { settingsSchema } from './settings-schema.client.ts' // Every fixture carries the resource hook the resources plugin merges into GlobalStandardProps. const useResource = (() => ({ status: 'none' as const, value: undefined, failure: undefined, reload: () => {} })) as GlobalStandardProps['useResource'] +const usePanelInfo: GlobalStandardProps['usePanelInfo'] = selector => selector({ activePanelId: null }) afterEach(() => { cleanup() @@ -147,7 +148,7 @@ function harness(options: { openSection, useSessions: unusedHook, useSessionPendingInteraction, - useResource, + usePanelInfo, useResource, useWorkspaces: unusedHook, controller, useModels: bindSnapshotSelector(controller.store), diff --git a/packages/client/ui-settings-models/tests/welcome-notice.client.spec.tsx b/packages/client/ui-settings-models/tests/welcome-notice.client.spec.tsx index 59b2b7acd8..41abead4e8 100644 --- a/packages/client/ui-settings-models/tests/welcome-notice.client.spec.tsx +++ b/packages/client/ui-settings-models/tests/welcome-notice.client.spec.tsx @@ -10,6 +10,7 @@ import { SettingsScopeController } from '@deepseek-ai/dsh-client-ui-settings/src // Every fixture carries the resource hook the resources plugin merges into GlobalStandardProps. const useResource = (() => ({ status: 'none' as const, value: undefined, failure: undefined, reload: () => {} })) as GlobalStandardProps['useResource'] +const usePanelInfo: GlobalStandardProps['usePanelInfo'] = selector => selector({ activePanelId: null }) /** Stateless schema service for scope construction in this jsdom fixture. */ const schemaService = new SettingsSchemaService(new Context()) @@ -93,7 +94,7 @@ function mount( openSection: vi.fn(), useSessions: unusedHook, useSessionPendingInteraction, - useResource, + usePanelInfo, useResource, useWorkspaces: unusedHook, controller, useWelcome: bindSnapshotSelector(controller.store), diff --git a/packages/client/ui-sidebar-right/tests/apply.client.spec.ts b/packages/client/ui-sidebar-right/tests/apply.client.spec.ts index c54f98f2af..3f9a30d29b 100644 --- a/packages/client/ui-sidebar-right/tests/apply.client.spec.ts +++ b/packages/client/ui-sidebar-right/tests/apply.client.spec.ts @@ -10,6 +10,7 @@ */ import { describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' +import type { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client' import type { SessionId } from '@deepseek-ai/dsh-session/types' import { apply, inject } from '../src/client/index.ts' import type { GuideInjected, SidebarRightInjected } from '../src/client/index.ts' @@ -18,6 +19,7 @@ import { SidebarRightController } from '../src/client/service.ts' import { SidebarRightTabRegistry } from '../src/client/tab-registry.ts' import type { createSidebarRightStore } from '../src/client/stores.ts' import { RightbarSeat } from '../src/client/shell/SidebarRight.tsx' +import { RightbarRoot } from '../src/client/shell/RightbarRoot.tsx' import { ExpandButton } from '../src/client/shell/ExpandButton.tsx' import { GuideBody } from '../src/client/tabs/guide/GuideBody.tsx' import { GUIDE_ID } from '../src/client/tabs/guide/definition.ts' @@ -28,7 +30,7 @@ const SESSION = 's-test' as SessionId interface Recorded { name: string key?: string - locale: string + locale?: string store?: unknown children?: unknown inject?: (sessionId: SessionId) => unknown @@ -39,7 +41,7 @@ async function boot() { const ctx = new Context() const registered: Recorded[] = [] const slots = { - inject: vi.fn((_name: string, register: () => () => void) => register()), + inject: vi.fn((_name: string, register: Parameters[1]) => ctx.effect(register)), register: vi.fn((options: Omit, component: unknown) => { const entry: Recorded = { ...options, component } registered.push(entry) @@ -90,26 +92,25 @@ describe('ui-sidebar-right apply', () => { expect(guide?.id).toBe(GUIDE_ID) expect(guide?.priority).toBe('builtin') expect(guide?.title('sidebar://guide')).toBe('tab.guide.title') - // Three registrations: the panel seat, the header's corner seat, and the - // guide body under the guide implementation's id. expect(registered.map(entry => [entry.name, entry.key, entry.locale, entry.component])).toEqual([ - ['rightbar', undefined, 'sidebarRight', RightbarSeat], + ['rightbar', undefined, undefined, RightbarRoot], + ['rightbar.session', undefined, 'sidebarRight', RightbarSeat], ['conversation.session.header.corner', undefined, 'sidebarRight', ExpandButton], ['sidebar.right.pane.tab', GUIDE_ID, 'sidebarRight', GuideBody], ]) // The panel declares the extension seats; the guide declares its chain child. - expect(Object.keys(seat('rightbar').children as object)).toEqual([ + expect(Object.keys(seat('rightbar.session').children as object)).toEqual([ 'sidebar.right.pane.tab', 'sidebar.right.pane.tab.title', 'sidebar.right.tab.menu.item', ]) expect(seat('sidebar.right.pane.tab').children).toMatchObject({ 'sidebar.right.tab.guide': { kind: 'chain', scope: 'session' } }) // Both seats read one store: the button only needs to know whether the panel is expanded. - expect(seat('rightbar').store).toBeDefined() - expect(seat('conversation.session.header.corner').store).toBe(seat('rightbar').store) + expect(seat('rightbar.session').store).toBeDefined() + expect(seat('conversation.session.header.corner').store).toBe(seat('rightbar.session').store) }) it('hands the panel seat the frame report, the service binding, the opens, the observable registry, and the Tab domain', async () => { const { ctx, layout, resources, seat, injectedOf } = await boot() - const injected = injectedOf(seat('rightbar')) as SidebarRightInjected + const injected = injectedOf(seat('rightbar.session')) as SidebarRightInjected // The frame learns the composition of expanded and presentation, nothing else. injected.syncPresentation({ shown: true, track: true, fullscreen: false }) expect(layout.openRightbar).toHaveBeenLastCalledWith(true, false) @@ -128,7 +129,7 @@ describe('ui-sidebar-right apply', () => { unsubscribe() // The binding makes the service act on this seat's session; the seat's // store instance is minted here from the handle the registration declared. - const handle = seat('rightbar').store as ReturnType + const handle = seat('rightbar.session').store as ReturnType const instance = handle.create() const release = injected.bindService({ sessionId: SESSION, actions: instance.actions, surfaces: {}, canSplitPane: () => true }) injected.openTab('guide', { revealIfOpened: false }) @@ -145,7 +146,7 @@ describe('ui-sidebar-right apply', () => { it('adopts each session\'s store instance as the runtime mints it, so a tab\'s own actions land with no seat bound', async () => { const { ctx, resources, seat } = await boot() - const handle = seat('rightbar').store as ReturnType + const handle = seat('rightbar.session').store as ReturnType // Both seats declare the same wrapped handle, so either minting adopts. expect(seat('conversation.session.header.corner').store).toBe(handle) const instance = handle.create(SESSION) @@ -178,8 +179,8 @@ describe('ui-sidebar-right apply', () => { it('takes every registration and both faces back when disposed, aborting the open records, so a reload registers again', async () => { const { ctx, registered, dictionaries, fiber, seat, injectedOf } = await boot() - const injected = injectedOf(seat('rightbar')) as SidebarRightInjected - const handle = seat('rightbar').store as ReturnType + const injected = injectedOf(seat('rightbar.session')) as SidebarRightInjected + const handle = seat('rightbar.session').store as ReturnType // Minted under the session key, so the instance is adopted and the teardown releases it. const instance = handle.create(SESSION) injected.bindService({ sessionId: SESSION, actions: instance.actions, surfaces: {}, canSplitPane: () => true }) @@ -199,6 +200,6 @@ describe('ui-sidebar-right apply', () => { expect(dictionaries.size).toBe(0) await ctx.plugin({ inject: [...inject], apply }).await() expect(ctx.sidebarRightTabs.get('guide')?.id).toBe(GUIDE_ID) - expect(registered).toHaveLength(3) + expect(registered).toHaveLength(4) }) }) diff --git a/packages/client/ui-sidebar-right/tests/seat.client.spec.tsx b/packages/client/ui-sidebar-right/tests/seat.client.spec.tsx index adf00ef762..19d28629e8 100644 --- a/packages/client/ui-sidebar-right/tests/seat.client.spec.tsx +++ b/packages/client/ui-sidebar-right/tests/seat.client.spec.tsx @@ -6,6 +6,7 @@ import { useState } from 'react' import { SlotTestRuntime } from '@deepseek-ai/dsh-client-test-runtime' import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client' import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' +import type { MainPanelId } from '@deepseek-ai/dsh-client-ui-layout/client' import type { PaneId, SplitId, TabId } from '@deepseek-ai/dsh-client-ui-dockkit' import { dockPaneIds, getPane } from '@deepseek-ai/dsh-client-ui-dockkit' import type { SessionId } from '@deepseek-ai/dsh-session/types' @@ -67,7 +68,7 @@ async function mountSeat(viewportWidth = 1440, canShow = true) { runtime.ctx.provide('locale', locale) runtime.slots.installLocale(locale) await runtime.declare({ - 'rightbar': { kind: 'single', scope: 'session' }, + 'rightbar': { kind: 'single', scope: 'root' }, 'conversation.session.header.corner': { kind: 'single', scope: 'session' }, }) await runtime.sessions.add({ id: SESSION }) @@ -98,7 +99,7 @@ async function mountSeat(viewportWidth = 1440, canShow = true) { runtime.slots.register({ name: 'sidebar.right.pane.tab.title', key: 'test/text' }, Title) }) const view = runtime.renderSlot('rightbar', { width: 420, viewportWidth, canShow }) - const instance = runtime.storeOf('rightbar', SESSION) as ReturnType['create']> + const instance = runtime.storeOf('rightbar.session', SESSION) as ReturnType['create']> const controller = runtime.ctx.sidebarRight const layout = () => instance.getSnapshot().bySession[SESSION]!.layout const open = (name = 'a.txt', options?: Parameters[1]) => { @@ -115,6 +116,19 @@ function element(container: HTMLElement, selector: string): HTMLElement { } describe('RightbarSeat presentation', () => { + it('hides for a global main panel and retains the Session sidebar state', async () => { + const h = await mountSeat() + h.open('retained.txt') + const retained = h.layout() + act(() => { h.runtime.panelInfo.set({ activePanelId: 'other-panel' as MainPanelId }) }) + expect(h.view.container.querySelector('[data-sidebar-right-panel]')).toBeNull() + expect(h.frame.closeRightbar).toHaveBeenCalled() + expect(h.layout()).toBe(retained) + act(() => { h.runtime.panelInfo.set({ activePanelId: null }) }) + expect(h.view.container.querySelector('[data-sidebar-right-panel]')).not.toBeNull() + expect(h.layout()).toBe(retained) + }) + it('keeps the panel mounted while collapsed and releases the frame on unmount', async () => { const h = await mountSeat() const panel = element(h.view.container, '[data-sidebar-right-panel]') diff --git a/packages/client/ui-sidebar/tests/__snapshots__/sidebar-snapshot.client.spec.tsx.snap b/packages/client/ui-sidebar/tests/__snapshots__/sidebar-snapshot.client.spec.tsx.snap index ba3e4d9540..62aff29600 100644 --- a/packages/client/ui-sidebar/tests/__snapshots__/sidebar-snapshot.client.spec.tsx.snap +++ b/packages/client/ui-sidebar/tests/__snapshots__/sidebar-snapshot.client.spec.tsx.snap @@ -60,6 +60,35 @@ exports[`sidebar shell snapshots > renders the collapsed rail after the crossfad xmlns="http://www.w3.org/2000/svg" /> +
    @@ -190,6 +219,45 @@ exports[`sidebar shell snapshots > renders the expanded column (wordmark, capsul New Session +
    @@ -320,6 +388,45 @@ exports[`sidebar shell snapshots > renders the expanded column in the default lo 新会话 +
    diff --git a/packages/client/ui-sidebar/tests/apply.client.spec.tsx b/packages/client/ui-sidebar/tests/apply.client.spec.tsx index e638e1fb9d..f0cf52c5f3 100644 --- a/packages/client/ui-sidebar/tests/apply.client.spec.tsx +++ b/packages/client/ui-sidebar/tests/apply.client.spec.tsx @@ -2,15 +2,20 @@ import { Context } from '@deepseek-ai/cordis' import { describe, expect, it, vi } from 'vitest' import { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client' +import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots' import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client' import { apply, inject } from '@deepseek-ai/dsh-client-ui-sidebar/client' import type { SidebarRootInjected } from '@deepseek-ai/dsh-client-ui-sidebar/client' import { apply as hostApply } from '../src/index.ts' +function SidebarFrame({ renderSlot }: PropsRenderSlots<'sidebar'>) { + return renderSlot('sidebar', { collapsed: false, width: 300 }) +} + async function bench(declare = true) { const ctx = new Context() await ctx.plugin(SlotRegistry).await() - const layout = { toggleSidebar: vi.fn() } + const layout = { toggleSidebar: vi.fn(), selectPanel: vi.fn() } const uiWorkspace = { startSession: vi.fn() } ctx.provide('layout', layout) ctx.provide('uiWorkspace', uiWorkspace as never) @@ -18,8 +23,11 @@ async function bench(declare = true) { const slots = ctx.get('slots') as SlotRegistry if (declare) { slots.register( - { name: 'root', children: { 'sidebar': { kind: 'single', scope: 'root' } } } as never, - () => null, + { name: 'root', children: { + 'sidebar': { kind: 'single', scope: 'root' }, + 'main': { kind: 'keyed', scope: 'root' }, + } }, + SidebarFrame, ) } return { ctx, slots, layout, uiWorkspace } @@ -43,10 +51,15 @@ describe('ui-sidebar apply', () => { expect(b.slots.spec('sidebar.workspaces')).toEqual({ kind: 'single', scope: 'root' }) expect(b.slots.spec('sidebar.settings')).toEqual({ kind: 'single', scope: 'root' }) expect(b.slots.spec('sidebar.footer.action')).toEqual({ kind: 'list', scope: 'root' }) + expect(b.slots.spec('sidebar.panellist')).toEqual({ kind: 'list', scope: 'root' }) + expect(b.slots.spec('sidebar.panellist.title')).toEqual({ kind: 'keyed', scope: 'root' }) // Copy rides the standard locale seat, not the inject face. expect(b.slots.entries('sidebar')[0]!.locale).toBe('sidebar') const injected = (b.slots.entries('sidebar')[0]!.inject as () => SidebarRootInjected)() - expect(Object.keys(injected)).toEqual(['startSession', 'toggleSidebar']) + expect(Object.keys(injected)).toEqual(['startSession', 'toggleSidebar', 'selectPanel', 'hooks']) + expect(injected.hooks.panels.getSnapshot()).toEqual([ + { id: 'hello-world', order: 100, label: 'Hello World' }, + ]) // Both arms delegate to the Workspace UI's shared New Session action. injected.startSession('workspace' as never) expect(b.uiWorkspace.startSession).toHaveBeenCalledWith('workspace') @@ -56,9 +69,18 @@ describe('ui-sidebar apply', () => { expect(b.layout.toggleSidebar).toHaveBeenCalledOnce() }) - it('fails when no live owner declared the sidebar slot', async () => { + it('waits for the sidebar declaration before registering', async () => { const b = await bench(false) - await expect(b.ctx.plugin({ inject: [...inject], apply })).rejects.toThrow(/not declared/) + const fiber = b.ctx.plugin({ inject: [...inject], apply }) + await fiber.await() + expect(b.slots.entries('sidebar')).toHaveLength(0) + const disposeRoot = b.slots.register({ + name: 'root', + children: { sidebar: { kind: 'single', scope: 'root' } }, + }, SidebarFrame) + expect(b.slots.entries('sidebar')).toHaveLength(1) + disposeRoot() + await fiber.dispose() }) it('removes the entry and child declaration on teardown', async () => { @@ -71,5 +93,7 @@ describe('ui-sidebar apply', () => { expect(b.slots.spec('sidebar.brand.name')).toBeUndefined() expect(b.slots.spec('sidebar.workspaces')).toBeUndefined() expect(b.slots.spec('sidebar.footer.action')).toBeUndefined() + expect(b.slots.spec('sidebar.panellist')).toBeUndefined() + expect(b.slots.entries('main')).toHaveLength(0) }) }) diff --git a/packages/client/ui-sidebar/tests/pointer-scrollbars.client.spec.tsx b/packages/client/ui-sidebar/tests/pointer-scrollbars.client.spec.tsx index ed5b687e73..065b9d2816 100644 --- a/packages/client/ui-sidebar/tests/pointer-scrollbars.client.spec.tsx +++ b/packages/client/ui-sidebar/tests/pointer-scrollbars.client.spec.tsx @@ -14,6 +14,7 @@ import { en } from '../src/client/locales.ts' // Every fixture carries the resource hook the resources plugin merges into GlobalStandardProps. const useResource = (() => ({ status: 'none' as const, value: undefined, failure: undefined, reload: () => {} })) as GlobalStandardProps['useResource'] +const usePanelInfo: GlobalStandardProps['usePanelInfo'] = selector => selector({ activePanelId: null }) /** Pinned column box; the shell compares pointer coordinates against it. */ const COLUMN_WIDTH = 280 @@ -40,7 +41,7 @@ function mountColumn(): { column: HTMLElement; quiet: () => boolean } { {}} usePanels={selector => selector([])} useResource={useResource} useWorkspaces={neverHook} startSession={vi.fn()} toggleSidebar={vi.fn()} t={t} renderSlot={((_key: string, owner: SidebarSectionOwnerProps) =>
    ) as SidebarRootComponentProps['renderSlot']} diff --git a/packages/client/ui-sidebar/tests/sidebar-root.client.spec.tsx b/packages/client/ui-sidebar/tests/sidebar-root.client.spec.tsx index 9973a1958b..7a689ad2fa 100644 --- a/packages/client/ui-sidebar/tests/sidebar-root.client.spec.tsx +++ b/packages/client/ui-sidebar/tests/sidebar-root.client.spec.tsx @@ -13,6 +13,7 @@ import { en as commonEn } from '@deepseek-ai/dsh-client-locale/src/locales/en.ts // Every fixture carries the resource hook the resources plugin merges into GlobalStandardProps. const useResource = (() => ({ status: 'none' as const, value: undefined, failure: undefined, reload: () => {} })) as GlobalStandardProps['useResource'] +const usePanelInfo: GlobalStandardProps['usePanelInfo'] = selector => selector({ activePanelId: null }) // English-dictionary translate stub: the shell renders the same copy the // assertions below query by accessible name. @@ -45,7 +46,7 @@ function mountShell({ collapsed = false, width = 300 }: { collapsed?: boolean; w {}} usePanels={selector => selector([])} useResource={useResource} useWorkspaces={neverHook} startSession={startSession} toggleSidebar={toggleSidebar} t={t} renderSlot={(( key: string, @@ -110,7 +111,7 @@ describe('SidebarRoot shell', () => { const { container } = render( {}} usePanels={selector => selector([])} useResource={useResource} useWorkspaces={neverHook} startSession={vi.fn()} toggleSidebar={vi.fn()} t={t} renderSlot={((_key: string, _owner: unknown, options?: { fallback?: ReactNode }) => options?.fallback ?? null) as SidebarRootComponentProps['renderSlot']} @@ -129,7 +130,7 @@ describe('SidebarRoot shell', () => { render( {}} usePanels={selector => selector([])} useResource={useResource} useWorkspaces={neverHook} startSession={vi.fn()} toggleSidebar={vi.fn()} t={t} renderSlot={((_key: string, _owner: unknown, options?: { fallback?: ReactNode }) => options?.fallback ?? null) as SidebarRootComponentProps['renderSlot']} @@ -143,7 +144,7 @@ describe('SidebarRoot shell', () => { render( {}} usePanels={selector => selector([])} useResource={useResource} useWorkspaces={neverHook} startSession={vi.fn()} toggleSidebar={vi.fn()} t={t} renderSlot={((_key: string, _owner: unknown, options?: { fallback?: ReactNode }) => options?.fallback ?? null) as SidebarRootComponentProps['renderSlot']} diff --git a/packages/client/ui-theme/tests/appearance-row.client.spec.tsx b/packages/client/ui-theme/tests/appearance-row.client.spec.tsx index 4e6b61616e..5ad602ff05 100644 --- a/packages/client/ui-theme/tests/appearance-row.client.spec.tsx +++ b/packages/client/ui-theme/tests/appearance-row.client.spec.tsx @@ -13,6 +13,7 @@ import type { ThemePreference } from '../src/client/index.ts' // Every fixture carries the resource hook the resources plugin merges into GlobalStandardProps. const useResource = (() => ({ status: 'none' as const, value: undefined, failure: undefined, reload: () => {} })) as GlobalStandardProps['useResource'] +const usePanelInfo: GlobalStandardProps['usePanelInfo'] = selector => selector({ activePanelId: null }) afterEach(cleanup) @@ -47,7 +48,7 @@ function mount(preference: ThemePreference = 'system') { const props: AppearanceRowComponentProps = { useSessions: emptySessions(), useSessionPendingInteraction, - useResource, + usePanelInfo, useResource, useWorkspaces: emptyWorkspaces(), useStore: bindSnapshotSelector(store), actions: store.actions, diff --git a/packages/client/ui-theme/tests/font-size-row.client.spec.tsx b/packages/client/ui-theme/tests/font-size-row.client.spec.tsx index 3c4601e1f8..c917901cab 100644 --- a/packages/client/ui-theme/tests/font-size-row.client.spec.tsx +++ b/packages/client/ui-theme/tests/font-size-row.client.spec.tsx @@ -14,6 +14,7 @@ import { createFontSizeRowStore } from '../src/client/settings-store.ts' // Every fixture carries the resource hook the resources plugin merges into GlobalStandardProps. const useResource = (() => ({ status: 'none' as const, value: undefined, failure: undefined, reload: () => {} })) as GlobalStandardProps['useResource'] +const usePanelInfo: GlobalStandardProps['usePanelInfo'] = selector => selector({ activePanelId: null }) afterEach(cleanup) @@ -49,7 +50,7 @@ function mount(fontSize = 14) { const props: FontSizeRowComponentProps = { useSessions: emptySessions(), useSessionPendingInteraction, - useResource, + usePanelInfo, useResource, useWorkspaces: emptyWorkspaces(), useStore: bindSnapshotSelector(store), actions: store.actions, diff --git a/packages/client/ui-tool/tests/assembly-surfaces.client.spec.tsx b/packages/client/ui-tool/tests/assembly-surfaces.client.spec.tsx index 636124555e..99ee175099 100644 --- a/packages/client/ui-tool/tests/assembly-surfaces.client.spec.tsx +++ b/packages/client/ui-tool/tests/assembly-surfaces.client.spec.tsx @@ -59,13 +59,13 @@ const bashResult = (seq: number, callId: string, over?: Partial) }) /** Test-owned AppFrame role: declares and renders the resident conversation area. */ -type AppRootProps = PropsRenderSlots<'conversation'> +type AppRootProps = PropsRenderSlots<'main'> function AppRoot({ renderSlot }: AppRootProps) { - return <>{renderSlot('conversation', {})} + return <>{renderSlot('main', {}, { entryKey: 'conversation' })} } const LAYOUT_CHILDREN = { - 'conversation': { kind: 'single', scope: 'session-maybe' }, + 'main': { kind: 'keyed', scope: 'root' }, } as const async function bench(nodes: ToolResultNode[]) { @@ -80,6 +80,7 @@ async function bench(nodes: ToolResultNode[]) { runtime.ctx.provide('sidebarRight', { openResource: vi.fn() } as never) runtime.ctx.provide('uiWorkspace', { connectWorkspace: vi.fn(async () => SID), + openSession: (id: SessionId) => { runtime.sessions.open(id) }, } as never) const locale = new LocaleRuntime(runtime.ctx) runtime.ctx.provide('locale', locale) diff --git a/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx b/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx index bb471caff9..2f72a5d2b7 100644 --- a/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx +++ b/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx @@ -42,13 +42,13 @@ const toolResult = (seq: number, callId: string, name: string, args = '{"command }) /** Test-owned AppFrame role: declares and renders the resident conversation area. */ -type AppRootProps = PropsRenderSlots<'conversation'> +type AppRootProps = PropsRenderSlots<'main'> function AppRoot({ renderSlot }: AppRootProps) { - return <>{renderSlot('conversation', {})} + return <>{renderSlot('main', {}, { entryKey: 'conversation' })} } const LAYOUT_CHILDREN = { - 'conversation': { kind: 'single', scope: 'session-maybe' }, + 'main': { kind: 'keyed', scope: 'root' }, } as const /** @@ -67,6 +67,7 @@ async function bench(nodes: ToolResultNode[]) { runtime.ctx.provide('sidebarRight', sidebarRight as never) runtime.ctx.provide('uiWorkspace', { connectWorkspace: vi.fn(async () => SID), + openSession: (id: SessionId) => { runtime.sessions.open(id) }, } as never) const locale = new LocaleRuntime(runtime.ctx) runtime.ctx.provide('locale', locale) @@ -213,6 +214,7 @@ describe('registrant declaration injection', () => { runtime.ctx.provide('sidebarRight', { openResource: vi.fn() } as never) runtime.ctx.provide('uiWorkspace', { connectWorkspace: vi.fn(async () => SID), + openSession: (id: SessionId) => { runtime.sessions.open(id) }, } as never) const locale = new LocaleRuntime(runtime.ctx) runtime.ctx.provide('locale', locale) diff --git a/packages/client/ui-trajectory/tests/views.client.spec.tsx b/packages/client/ui-trajectory/tests/views.client.spec.tsx index 91ba0468f1..79a0e14b0e 100644 --- a/packages/client/ui-trajectory/tests/views.client.spec.tsx +++ b/packages/client/ui-trajectory/tests/views.client.spec.tsx @@ -55,6 +55,7 @@ import { t as tTrajectory, tZh } from './locale.client.ts' // Every session-scope fixture carries the resource hook the resources plugin merges into GlobalStandardProps. const useResource = (() => ({ status: 'none' as const, value: undefined, failure: undefined, reload: () => {} })) as GlobalStandardProps['useResource'] +const usePanelInfo: GlobalStandardProps['usePanelInfo'] = selector => selector({ activePanelId: null }) function TrajectoryTimeline( props: Omit, 't'>, @@ -224,7 +225,7 @@ function standaloneProps( sessionId: SID, useChat: bindSnapshotSelector(createSnapshotStore(EMPTY_CHAT_SNAPSHOT)), useSessions: emptySessions(), - useResource, + usePanelInfo, useResource, useSessionPendingInteraction: bindSnapshotSelector( createSnapshotStore(new Map()), ), @@ -351,7 +352,7 @@ function mount(fixture: Awaited>) { useConversation, useConversationViews, useSessions, - useResource, + usePanelInfo, useResource, useSessionPendingInteraction, useWorkspaces, useProjection, diff --git a/packages/client/ui-user-questions/tests/plan-review-panel.client.spec.tsx b/packages/client/ui-user-questions/tests/plan-review-panel.client.spec.tsx index f3cc685d4d..238cc195fd 100644 --- a/packages/client/ui-user-questions/tests/plan-review-panel.client.spec.tsx +++ b/packages/client/ui-user-questions/tests/plan-review-panel.client.spec.tsx @@ -14,6 +14,7 @@ import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts // Every session-scope fixture carries the resource hook the resources plugin merges into GlobalStandardProps. const useResource = (() => ({ status: 'none' as const, value: undefined, failure: undefined, reload: () => {} })) as GlobalStandardProps['useResource'] +const usePanelInfo: GlobalStandardProps['usePanelInfo'] = selector => selector({ activePanelId: null }) afterEach(cleanup) @@ -115,7 +116,7 @@ const kit: Omit = { pendingInteraction: undefined, useSession: selector => selector(sessionState), useSessions: selector => selector(sessionList), - useResource, + usePanelInfo, useResource, useSessionPendingInteraction: selector => selector(attentionState), useWorkspaces: selector => selector(workspaceState), useConversation: selector => selector(conversationState), diff --git a/packages/client/ui-user-questions/tests/user-questions-composer.client.spec.tsx b/packages/client/ui-user-questions/tests/user-questions-composer.client.spec.tsx index 80d7d9307c..90848b3e9e 100644 --- a/packages/client/ui-user-questions/tests/user-questions-composer.client.spec.tsx +++ b/packages/client/ui-user-questions/tests/user-questions-composer.client.spec.tsx @@ -13,6 +13,7 @@ import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts // Every session-scope fixture carries the resource hook the resources plugin merges into GlobalStandardProps. const useResource = (() => ({ status: 'none' as const, value: undefined, failure: undefined, reload: () => {} })) as GlobalStandardProps['useResource'] +const usePanelInfo: GlobalStandardProps['usePanelInfo'] = selector => selector({ activePanelId: null }) afterEach(cleanup) @@ -114,7 +115,7 @@ const kitBase: Omit = pendingInteraction: undefined, useSession: selector => selector(sessionState), useSessions: selector => selector(sessionList), - useResource, + usePanelInfo, useResource, useSessionPendingInteraction: selector => selector(attentionState), useWorkspaces: selector => selector(workspaceState), useConversation: selector => selector(conversationState), diff --git a/packages/client/ui-workflow-run/tests/workflow-run.client.spec.tsx b/packages/client/ui-workflow-run/tests/workflow-run.client.spec.tsx index e6b38074a5..4f99fb3b44 100644 --- a/packages/client/ui-workflow-run/tests/workflow-run.client.spec.tsx +++ b/packages/client/ui-workflow-run/tests/workflow-run.client.spec.tsx @@ -34,6 +34,7 @@ import type {} from '../src/client/index.ts' // Every session-scope fixture carries the resource hook the resources plugin merges into GlobalStandardProps. const useResource = (() => ({ status: 'none' as const, value: undefined, failure: undefined, reload: () => {} })) as GlobalStandardProps['useResource'] +const usePanelInfo: GlobalStandardProps['usePanelInfo'] = selector => selector({ activePanelId: null }) afterEach(cleanup) @@ -314,7 +315,7 @@ function panelProps(data: WorkflowRunChatData, sessions = listState(), openSessi node: node(data), sessionId: PARENT_ID, useSessions: selector => selector(sessions), - useResource, + usePanelInfo, useResource, useSessionPendingInteraction: selector => selector(panelAttention), useSession: selector => selector(panelSession), useProjection: () => undefined, diff --git a/packages/client/ui-workspace/tests/apply.client.spec.ts b/packages/client/ui-workspace/tests/apply.client.spec.ts index be05aae9df..d35e29c81a 100644 --- a/packages/client/ui-workspace/tests/apply.client.spec.ts +++ b/packages/client/ui-workspace/tests/apply.client.spec.ts @@ -21,6 +21,8 @@ async function bench() { const insertSessionBefore = vi.fn(async () => ({})) const open = vi.fn() const clear = vi.fn() + const selectPanel = vi.fn() + ctx.provide('layout', { selectPanel }) const search = vi.fn(async () => ({ ok: true as const, value: { items: [{ sessionId: 'session' as never, snippet: 'match' }], hasMore: false }, @@ -71,7 +73,7 @@ async function bench() { ctx.provide('locale', locale) return { ctx, slots: ctx.get('slots') as SlotRegistry, locale, create, rename, - insertSessionBefore, open, clear, search, renameSession, binding, fork, pickDirectory, + insertSessionBefore, open, clear, selectPanel, search, renameSession, binding, fork, pickDirectory, } } @@ -90,7 +92,7 @@ describe('ui-workspace apply', () => { it('declares the services it drives', () => { expect(inject).toEqual([ - 'slots', 'sessions', 'workspaces', 'locale', 'remote', 'remote.directoryPicker', + 'slots', 'sessions', 'workspaces', 'locale', 'remote', 'remote.directoryPicker', 'layout', ]) }) diff --git a/packages/client/ui-workspace/tests/host-home-staleness.client.spec.tsx b/packages/client/ui-workspace/tests/host-home-staleness.client.spec.tsx index 1b3e256c97..0dd5eecea9 100644 --- a/packages/client/ui-workspace/tests/host-home-staleness.client.spec.tsx +++ b/packages/client/ui-workspace/tests/host-home-staleness.client.spec.tsx @@ -28,6 +28,7 @@ function SidebarFrame({ renderSlot }: FrameProps) { /** The assembled sidebar over one Workspace inside the POSIX home the Host reports. */ async function bench() { const runtime = await SlotTestRuntime.create() + runtime.ctx.provide('layout', { selectPanel: vi.fn() }) runtime.releaseWorkspaceSource() const directoryPicker = {} const remote = new TestRemote(runtime.ctx) diff --git a/packages/client/ui-workspace/tests/rename-assembly.client.spec.tsx b/packages/client/ui-workspace/tests/rename-assembly.client.spec.tsx index d57a89ba8b..b814e39803 100644 --- a/packages/client/ui-workspace/tests/rename-assembly.client.spec.tsx +++ b/packages/client/ui-workspace/tests/rename-assembly.client.spec.tsx @@ -33,6 +33,7 @@ beforeEach(() => { localStorage.clear() }) /** Runtime with the locale face installed (the browser entry declares `locale:` — zh default backs the t seat). */ async function createRuntime(): Promise { const runtime = await SlotTestRuntime.create() + runtime.ctx.provide('layout', { selectPanel: vi.fn() }) runtime.releaseWorkspaceSource() // The rename flow never picks a directory; the namespace only has to be there // for ui-workspace's inject to settle. diff --git a/packages/client/ui-workspace/tests/workspace-browser.client.spec.tsx b/packages/client/ui-workspace/tests/workspace-browser.client.spec.tsx index 6958423e5f..1986a568ba 100644 --- a/packages/client/ui-workspace/tests/workspace-browser.client.spec.tsx +++ b/packages/client/ui-workspace/tests/workspace-browser.client.spec.tsx @@ -18,6 +18,7 @@ import { zh } from '../src/client/locales.ts' // Every fixture carries the resource hook the resources plugin merges into GlobalStandardProps. const useResource = (() => ({ status: 'none' as const, value: undefined, failure: undefined, reload: () => {} })) as GlobalStandardProps['useResource'] +const usePanelInfo: GlobalStandardProps['usePanelInfo'] = selector => selector({ activePanelId: null }) afterEach(cleanup) const scrollIntoView = vi.fn() @@ -78,7 +79,7 @@ function mount(overrides: Partial = {}) { expandSidebar: vi.fn(), useSessions: hook(sessionState([])), useSessionPendingInteraction: hook(noPendingInteraction), - useResource, + usePanelInfo, useResource, useWorkspaces: hook(workspaceState([])), useStore: bindSnapshotSelector(store), actions: store.actions, diff --git a/packages/client/ui-workspace/tests/workspace-picker.client.spec.tsx b/packages/client/ui-workspace/tests/workspace-picker.client.spec.tsx index b2ac4a6d1a..3783a1d16a 100644 --- a/packages/client/ui-workspace/tests/workspace-picker.client.spec.tsx +++ b/packages/client/ui-workspace/tests/workspace-picker.client.spec.tsx @@ -16,6 +16,7 @@ import { zh } from '../src/client/locales.ts' // Every fixture carries the resource hook the resources plugin merges into GlobalStandardProps. const useResource = (() => ({ status: 'none' as const, value: undefined, failure: undefined, reload: () => {} })) as GlobalStandardProps['useResource'] +const usePanelInfo: GlobalStandardProps['usePanelInfo'] = selector => selector({ activePanelId: null }) afterEach(cleanup) @@ -98,7 +99,7 @@ function mount( anchorRef={anchorRef} useSessions={hook(sessions)} useSessionPendingInteraction={hook(noPendingInteraction)} - useResource={useResource} + usePanelInfo={usePanelInfo} useResource={useResource} useWorkspaces={hook(workspaceState(nextItems))} onPick={onPick} onClose={onClose} @@ -220,7 +221,7 @@ describe('WorkspacePicker', () => { , @@ -237,7 +238,7 @@ describe('WorkspacePicker', () => { , diff --git a/packages/client/ui-workspace/tests/workspaces-service.client.spec.ts b/packages/client/ui-workspace/tests/workspaces-service.client.spec.ts index 393619bbb7..7bc3b537c3 100644 --- a/packages/client/ui-workspace/tests/workspaces-service.client.spec.ts +++ b/packages/client/ui-workspace/tests/workspaces-service.client.spec.ts @@ -189,6 +189,8 @@ interface BenchOptions { function bench(options: BenchOptions = {}) { const ctx = new Context() + const selectPanel = vi.fn() + ctx.provide('layout', { selectPanel }) const directoryPicker = new FakeDirectoryPicker() const workspaces = new FakeWorkspaces(options.workspaces ?? workspaceState([], [], 'pending')) const sessions = new FakeSessions(options.sessions ?? sessionState([], undefined, 'pending')) @@ -198,7 +200,7 @@ function bench(options: BenchOptions = {}) { workspaces, sessions as unknown as ISessions, ) - return { ctx, directoryPicker, sessions, uiWorkspace, workspaces } + return { ctx, directoryPicker, sessions, uiWorkspace, workspaces, selectPanel } } async function flush(): Promise { @@ -207,6 +209,25 @@ async function flush(): Promise { } describe('UiWorkspaceService', () => { + it('selects a Session before revealing its Conversation, including the current Session', () => { + const current = sid('current') + const b = bench({ sessions: sessionState([summary('current')], current) }) + b.selectPanel.mockImplementation(() => { + expect(b.sessions.list.getSnapshot().current).toBe(current) + }) + b.uiWorkspace.openSession(current) + expect(b.sessions.open).toHaveBeenCalledWith(current) + expect(b.selectPanel).toHaveBeenCalledWith(null) + expect(b.sessions.open.mock.invocationCallOrder[0]).toBeLessThan(b.selectPanel.mock.invocationCallOrder[0]!) + }) + + it('keeps the current panel when selecting a Session throws', () => { + const b = bench() + b.sessions.open.mockImplementationOnce(() => { throw new Error('selection failed') }) + expect(() => { b.uiWorkspace.openSession(sid('target')) }).toThrow('selection failed') + expect(b.selectPanel).not.toHaveBeenCalled() + }) + it('reuses only an unarchived member blank and coalesces concurrent creation', async () => { const b = bench() const memberBlank = sid('member-blank') From fbbcbf33fe69740a3492a8abb2e8011738b26a41 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Tue, 8 Sep 2026 18:55:50 +0800 Subject: [PATCH 37/58] docs(client): document global main-panel extension points --- ...2-slot-type-chain-implementation.i18n.yaml | 4 +- ...26-07-22-slot-type-chain-implementation.md | 4 +- ...07-22-slot-type-chain-implementation.zh.md | 4 +- .../2026-09-08-global-main-panels.i18n.yaml | 6 ++ .../2026-09-08-global-main-panels.md | 33 ++++++++ .../2026-09-08-global-main-panels.zh.md | 33 ++++++++ docs/subsystems/slots.i18n.yaml | 4 +- docs/subsystems/slots.md | 77 ++++++++++--------- docs/subsystems/slots.zh.md | 77 ++++++++++--------- .../client/ui-conversation/README.i18n.yaml | 4 +- packages/client/ui-conversation/README.md | 2 +- packages/client/ui-conversation/README.zh.md | 2 +- packages/client/ui-layout/README.i18n.yaml | 4 +- packages/client/ui-layout/README.md | 8 +- packages/client/ui-layout/README.zh.md | 8 +- .../client/ui-sidebar-right/README.i18n.yaml | 4 +- packages/client/ui-sidebar-right/README.md | 2 + packages/client/ui-sidebar-right/README.zh.md | 2 + packages/client/ui-sidebar/README.i18n.yaml | 4 +- packages/client/ui-sidebar/README.md | 6 +- packages/client/ui-sidebar/README.zh.md | 6 +- packages/client/ui-workspace/README.i18n.yaml | 4 +- packages/client/ui-workspace/README.md | 2 + packages/client/ui-workspace/README.zh.md | 2 + .../client-runtime/README.i18n.yaml | 4 +- .../test-support/client-runtime/README.md | 4 +- .../test-support/client-runtime/README.zh.md | 4 +- 27 files changed, 212 insertions(+), 102 deletions(-) create mode 100644 .agents/notes/implemented/architecture/2026-09-08-global-main-panels.i18n.yaml create mode 100644 .agents/notes/implemented/architecture/2026-09-08-global-main-panels.md create mode 100644 .agents/notes/implemented/architecture/2026-09-08-global-main-panels.zh.md diff --git a/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.i18n.yaml index 7d108bbf7d..f633a831d3 100644 --- a/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md -2026-07-22-slot-type-chain-implementation.md: 0e493a19e232acf6f289714e53e0cf64b3ffc4b0 -2026-07-22-slot-type-chain-implementation.zh.md: 5e5c2ec808cec6949396a333c6a86e267bb4268b +2026-07-22-slot-type-chain-implementation.md: 98585a42f71844594da4ada19d7438a7d8321702 +2026-07-22-slot-type-chain-implementation.zh.md: d1093537ccbc5ee5ff2dc18c234233ffca39879d diff --git a/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md b/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md index 0e493a19e2..98585a42f7 100644 --- a/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md +++ b/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md @@ -12,6 +12,8 @@ The page is composed at runtime from independently loaded plugins, so the UI nee ## Decision +Global main-panel selection and its root lifetime are defined by the [global main-panels decision](2026-09-08-global-main-panels.md). + One sentence: **the ui-renderer renders only `'root'`; a plugin composes UI through a single `register` call that simultaneously occupies a slot, declares+authorizes its child slots, declares its store, and injects its business face; components are pure functions whose props arrive in four shares, each auto-derived from its single source of truth.** ### 'root' is the only a-priori slot @@ -25,7 +27,7 @@ ctx.slots.register({ name: 'root', children: { 'sidebar': { kind: 'single', scope: 'root' }, - 'conversation': { kind: 'single', scope: 'session' }, + 'main': { kind: 'keyed', scope: 'root' }, }, store: createLayoutStore, // StoreHandle or factory (below) inject: injectFrame, // business face (below) diff --git a/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.zh.md b/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.zh.md index 5e5c2ec808..d1093537cc 100644 --- a/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.zh.md @@ -12,6 +12,8 @@ Status: implemented ## 决策 +全局主面板选择及其 root 生命周期由[全局主面板决策](2026-09-08-global-main-panels.zh.md)定义。 + 一句话:**ui-renderer 只渲染 `'root'`;插件用单独一次 `register` 调用组合 UI——这一次调用同时占用 slot、声明并授权子 slot、声明 store、注入业务面;组件是纯函数,props 分四份额到达,每一份额都从各自唯一的真源自动推导。** ### 'root' 是唯一的先验 slot @@ -25,7 +27,7 @@ ctx.slots.register({ name: 'root', children: { 'sidebar': { kind: 'single', scope: 'root' }, - 'conversation': { kind: 'single', scope: 'session' }, + 'main': { kind: 'keyed', scope: 'root' }, }, store: createLayoutStore, // StoreHandle or factory (below) inject: injectFrame, // business face (below) diff --git a/.agents/notes/implemented/architecture/2026-09-08-global-main-panels.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-08-global-main-panels.i18n.yaml new file mode 100644 index 0000000000..550bfbea6e --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-08-global-main-panels.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-08-global-main-panels.md +2026-09-08-global-main-panels.md: 510b869b9bd32f4edd054a982a77baa57dace9c3 +2026-09-08-global-main-panels.zh.md: 226f46918132237f2049844e8647e01e6d2e63c4 diff --git a/.agents/notes/implemented/architecture/2026-09-08-global-main-panels.md b/.agents/notes/implemented/architecture/2026-09-08-global-main-panels.md new file mode 100644 index 0000000000..510b869b9b --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-08-global-main-panels.md @@ -0,0 +1,33 @@ +# Agent Note: Global main panels without default UI additions + +Status: implemented + +English | [中文](2026-09-08-global-main-panels.zh.md) + +## Problem + +Plugins need application-wide views that do not belong to a Session. A Session-scoped Conversation view cannot provide that lifetime, and replacing the Conversation's single slot removes the ordinary conversation surface. Adding this extension must not add navigation controls or reserved space to the default application. + +## Decision + +The layout declares a root-scoped keyed `main` slot. The reserved `conversation` key belongs to the Conversation plugin, whose `main.conversation` child retains optional-Session binding. Other main entries receive no implicit Session binding. + +The sidebar owns the root-scoped `sidebar.panellist` list and `sidebar.panellist.title` keyed slot. Each list entry supplies its icon and an id matching its main entry; its label provides ordinary text and the accessible name. A title registration can replace the visible label with React content. The shipped composition registers no panel entry, so the empty list has no DOM or spacing. + +One eagerly created root store is shared by the renderer and layout controller. Its `panelInfo` and `layoutInfo` objects preserve independent references. The framework supplies `usePanelInfo`; individual rows and main content subscribe to their required selection values, while AppFrame reads only layout information. The right Sidebar's root controller decides whether to mount its Session subtree and reports the resulting track requirements to the frame. + +`uiWorkspace.openSession(id)` selects the Session before returning the main area to the Conversation, including when the same Session is selected again. New Session and workspace navigation use that operation. Panel navigation neither cancels the retained Session nor writes a Session event. + +## Alternatives considered + +**Session-scoped main views.** Their lifetime and standard props bind application-wide state to whichever Session happens to be current. + +**A second navigation stack.** Back buttons and saved return destinations are unnecessary when New Session and workspace Session rows already provide explicit destinations. + +**Flat selection and layout state with shallow comparison.** Separating the two stored objects preserves reference equality directly and avoids allocating and comparing a fresh layout projection on every panel selection. + +## Consequences + +The default sidebar snapshots remain unchanged. Extension panels have no right Sidebar, and selecting a different global panel does not change layout preferences. Switching between a Conversation with a visible right Sidebar and a global panel still changes the required column widths; this is not a promise of zero browser layout work. + +Panel selection is transient and resets on reload. Plugin disposal removes its contributions; removing the selected main entry returns the main area to the Conversation. Tests cover independent stored references, explicit Session navigation, declaration lifetimes, and the empty default sidebar. The [Slots reference](../../../../docs/subsystems/slots.md) owns the composition API. diff --git a/.agents/notes/implemented/architecture/2026-09-08-global-main-panels.zh.md b/.agents/notes/implemented/architecture/2026-09-08-global-main-panels.zh.md new file mode 100644 index 0000000000..226f469181 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-08-global-main-panels.zh.md @@ -0,0 +1,33 @@ +# Agent Note: 不增加默认界面的全局主面板 + +Status: implemented + +[English](2026-09-08-global-main-panels.md) | 中文 + +## 问题 + +插件需要不属于任何会话的应用级视图。会话作用域的 Conversation 视图无法提供这种生命周期,而替换 Conversation 的 single slot 又会移除普通会话界面。增加此扩展不能在默认应用中增加导航控件或预留空间。 + +## 决策 + +布局声明 root 作用域的 keyed `main` slot。保留的 `conversation` key 属于 Conversation 插件,其 `main.conversation` 子 slot 保留可选的会话绑定。其他主面板条目不获得隐式会话绑定。 + +侧栏拥有 root 作用域的 `sidebar.panellist` list 和 `sidebar.panellist.title` keyed slot。每个 list 条目提供图标,以及与主面板条目匹配的 id;标签提供普通文字和无障碍名称。标题注册可用 React 内容替换可见标签。默认组合不注册面板条目,因此空列表没有 DOM 或间距。 + +渲染器与布局控制器共享一个直接创建的 root 存储。其 `panelInfo` 和 `layoutInfo` 对象保持独立的引用。框架提供 `usePanelInfo`;各行和中央内容订阅所需的选中态值,AppFrame 仅读取布局信息。右侧 Sidebar 的 root 控制器决定是否挂载其会话子树,并把最终所需的列宽报告给框架。 + +`uiWorkspace.openSession(id)` 先选中会话,再将中央区域切回 Conversation,包括再次选中同一个会话的情况。新会话和工作区导航使用该操作。面板导航既不取消保留的会话,也不写入会话事件。 + +## 考虑过的替代方案 + +**会话作用域的主视图。** 其生命周期和标准 props 会把应用级状态绑定到恰好处于当前态的会话。 + +**另一套导航栈。** 新会话和工作区会话行已经提供明确目标,不需要返回按钮或保存返回目的地。 + +**平铺选中态和布局状态,再做浅比较。** 将两者存为独立对象可以直接保持引用相等,避免每次选择面板都分配并比较新的布局投影。 + +## 后果 + +默认侧栏快照保持不变。扩展面板没有右侧 Sidebar,选择另一个全局面板不会改变布局偏好。在显示右侧 Sidebar 的 Conversation 与全局面板之间切换时,所需列宽仍会变化;这并不保证浏览器完全不计算布局。 + +面板选中态是瞬时状态,刷新后重置。插件 dispose(资源释放)会移除其贡献;移除当前选中的主面板条目会使中央区域回到 Conversation。测试覆盖存储引用的独立性、显式会话导航、声明生命周期和默认空侧栏。[Slots 参考](../../../../docs/subsystems/slots.zh.md)拥有组合 API 的说明。 diff --git a/docs/subsystems/slots.i18n.yaml b/docs/subsystems/slots.i18n.yaml index 49925a9aa5..6a214a8670 100644 --- a/docs/subsystems/slots.i18n.yaml +++ b/docs/subsystems/slots.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/slots.md -slots.md: 136607b035c4a40c8ab96d6bf933e6b5c8a925bf -slots.zh.md: b3a54048f5df20ad3ef271aea868ce15d5e18bce +slots.md: 92206d2deb3d1681e6c1d230d1f37146af6b720b +slots.zh.md: 47028aec897adcfdca499a19513162ad825928b4 diff --git a/docs/subsystems/slots.md b/docs/subsystems/slots.md index 136607b035..92206d2deb 100644 --- a/docs/subsystems/slots.md +++ b/docs/subsystems/slots.md @@ -82,6 +82,7 @@ The shipped adapters add these standard props. They are available according to t |---|---|---| | every scope | `useSessions`, `useSessionPendingInteraction` | `ui-session` | | every scope | `useWorkspaces` | `ui-workspace` | +| every scope | `usePanelInfo` | `ui-layout` | | `session` | `sessionId`, `useSession`, `useProjection` | `ui-session` | | `session-maybe` | optional `sessionId`, `useSession`, `useProjection` results | `ui-session` | | `session` | `useConversation`, `useInput`, `inputActions` | `ui-conversation` | @@ -112,6 +113,8 @@ root ├─ sidebar │ ├─ sidebar.brand.mark │ ├─ sidebar.brand.name +│ ├─ sidebar.panellist +│ ├─ sidebar.panellist.title │ ├─ sidebar.footer.action │ ├─ sidebar.workspaces │ │ └─ sidebar.workspaces.directoryFlow @@ -127,43 +130,45 @@ root │ ├─ settings.models.footer │ └─ settings.plugins.tab │ └─ settings.plugin.item -├─ conversation -│ ├─ conversation.session -│ │ └─ conversation.view -│ │ ├─ conversation.chat.node -│ │ │ ├─ conversation.chat.assistant-actions -│ │ │ ├─ conversation.chat.commandview -│ │ │ ├─ conversation.chat.turnTail -│ │ │ └─ tool.call.toolview -│ │ │ ├─ tool.call.images -│ │ │ └─ tool.view.cordis -│ │ ├─ conversation.message.images -│ │ └─ conversation.trajectory.images -│ ├─ conversation.session.header -│ │ ├─ conversation.session.header.lineage -│ │ ├─ conversation.session.header.actions -│ │ ├─ conversation.session.header.utilities -│ │ └─ conversation.session.header.corner -│ ├─ conversation.composer -│ │ └─ conversation.approval.detail -│ ├─ conversation.composer.bar -│ │ ├─ conversation.input.attachments -│ │ ├─ conversation.input.plan -│ │ └─ conversation.input.model -│ ├─ conversation.input.overlay -│ ├─ conversation.input.dock -│ ├─ conversation.composer.dock -│ ├─ conversation.input.left -│ ├─ conversation.input.right -│ ├─ conversation.hero.brand.mark -│ ├─ conversation.hero.workspace -│ │ └─ conversation.hero.workspace.directoryFlow -│ └─ conversation.hero.agentPreset +├─ main +│ └─ main.conversation +│ ├─ conversation.session +│ │ └─ conversation.view +│ │ ├─ conversation.chat.node +│ │ │ ├─ conversation.chat.assistant-actions +│ │ │ ├─ conversation.chat.commandview +│ │ │ ├─ conversation.chat.turnTail +│ │ │ └─ tool.call.toolview +│ │ │ ├─ tool.call.images +│ │ │ └─ tool.view.cordis +│ │ ├─ conversation.message.images +│ │ └─ conversation.trajectory.images +│ ├─ conversation.session.header +│ │ ├─ conversation.session.header.lineage +│ │ ├─ conversation.session.header.actions +│ │ ├─ conversation.session.header.utilities +│ │ └─ conversation.session.header.corner +│ ├─ conversation.composer +│ │ └─ conversation.approval.detail +│ ├─ conversation.composer.bar +│ │ ├─ conversation.input.attachments +│ │ ├─ conversation.input.plan +│ │ └─ conversation.input.model +│ ├─ conversation.input.overlay +│ ├─ conversation.input.dock +│ ├─ conversation.composer.dock +│ ├─ conversation.input.left +│ ├─ conversation.input.right +│ ├─ conversation.hero.brand.mark +│ ├─ conversation.hero.workspace +│ │ └─ conversation.hero.workspace.directoryFlow +│ └─ conversation.hero.agentPreset ├─ rightbar -│ ├─ sidebar.right.pane.tab -│ │ └─ sidebar.right.tab.guide -│ ├─ sidebar.right.pane.tab.title -│ └─ sidebar.right.tab.menu.item +│ └─ rightbar.session +│ ├─ sidebar.right.pane.tab +│ │ └─ sidebar.right.tab.guide +│ ├─ sidebar.right.pane.tab.title +│ └─ sidebar.right.tab.menu.item └─ shell.overlay ``` diff --git a/docs/subsystems/slots.zh.md b/docs/subsystems/slots.zh.md index b3a54048f5..47028aec89 100644 --- a/docs/subsystems/slots.zh.md +++ b/docs/subsystems/slots.zh.md @@ -82,6 +82,7 @@ Slot 声明固定两个相互独立的维度。 |---|---|---| | 所有 scope | `useSessions`、`useSessionPendingInteraction` | `ui-session` | | 所有 scope | `useWorkspaces` | `ui-workspace` | +| 所有作用域 | `usePanelInfo` | `ui-layout` | | `session` | `sessionId`、`useSession`、`useProjection` | `ui-session` | | `session-maybe` | 结果可选的 `sessionId`、`useSession`、`useProjection` | `ui-session` | | `session` | `useConversation`、`useInput`、`inputActions` | `ui-conversation` | @@ -112,6 +113,8 @@ root ├─ sidebar │ ├─ sidebar.brand.mark │ ├─ sidebar.brand.name +│ ├─ sidebar.panellist +│ ├─ sidebar.panellist.title │ ├─ sidebar.footer.action │ ├─ sidebar.workspaces │ │ └─ sidebar.workspaces.directoryFlow @@ -127,43 +130,45 @@ root │ ├─ settings.models.footer │ └─ settings.plugins.tab │ └─ settings.plugin.item -├─ conversation -│ ├─ conversation.session -│ │ └─ conversation.view -│ │ ├─ conversation.chat.node -│ │ │ ├─ conversation.chat.assistant-actions -│ │ │ ├─ conversation.chat.commandview -│ │ │ ├─ conversation.chat.turnTail -│ │ │ └─ tool.call.toolview -│ │ │ ├─ tool.call.images -│ │ │ └─ tool.view.cordis -│ │ ├─ conversation.message.images -│ │ └─ conversation.trajectory.images -│ ├─ conversation.session.header -│ │ ├─ conversation.session.header.lineage -│ │ ├─ conversation.session.header.actions -│ │ ├─ conversation.session.header.utilities -│ │ └─ conversation.session.header.corner -│ ├─ conversation.composer -│ │ └─ conversation.approval.detail -│ ├─ conversation.composer.bar -│ │ ├─ conversation.input.attachments -│ │ ├─ conversation.input.plan -│ │ └─ conversation.input.model -│ ├─ conversation.input.overlay -│ ├─ conversation.input.dock -│ ├─ conversation.composer.dock -│ ├─ conversation.input.left -│ ├─ conversation.input.right -│ ├─ conversation.hero.brand.mark -│ ├─ conversation.hero.workspace -│ │ └─ conversation.hero.workspace.directoryFlow -│ └─ conversation.hero.agentPreset +├─ main +│ └─ main.conversation +│ ├─ conversation.session +│ │ └─ conversation.view +│ │ ├─ conversation.chat.node +│ │ │ ├─ conversation.chat.assistant-actions +│ │ │ ├─ conversation.chat.commandview +│ │ │ ├─ conversation.chat.turnTail +│ │ │ └─ tool.call.toolview +│ │ │ ├─ tool.call.images +│ │ │ └─ tool.view.cordis +│ │ ├─ conversation.message.images +│ │ └─ conversation.trajectory.images +│ ├─ conversation.session.header +│ │ ├─ conversation.session.header.lineage +│ │ ├─ conversation.session.header.actions +│ │ ├─ conversation.session.header.utilities +│ │ └─ conversation.session.header.corner +│ ├─ conversation.composer +│ │ └─ conversation.approval.detail +│ ├─ conversation.composer.bar +│ │ ├─ conversation.input.attachments +│ │ ├─ conversation.input.plan +│ │ └─ conversation.input.model +│ ├─ conversation.input.overlay +│ ├─ conversation.input.dock +│ ├─ conversation.composer.dock +│ ├─ conversation.input.left +│ ├─ conversation.input.right +│ ├─ conversation.hero.brand.mark +│ ├─ conversation.hero.workspace +│ │ └─ conversation.hero.workspace.directoryFlow +│ └─ conversation.hero.agentPreset ├─ rightbar -│ ├─ sidebar.right.pane.tab -│ │ └─ sidebar.right.tab.guide -│ ├─ sidebar.right.pane.tab.title -│ └─ sidebar.right.tab.menu.item +│ └─ rightbar.session +│ ├─ sidebar.right.pane.tab +│ │ └─ sidebar.right.tab.guide +│ ├─ sidebar.right.pane.tab.title +│ └─ sidebar.right.tab.menu.item └─ shell.overlay ``` diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index b28c9069b7..f5dab9a40a 100644 --- a/packages/client/ui-conversation/README.i18n.yaml +++ b/packages/client/ui-conversation/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-conversation/README.md -README.md: edfcb83193d4513fba846f673d3572c35e8c0d1e -README.zh.md: f92e4c77ba3d3a2ceb96758e5817e447ba2ec1ac +README.md: 633a35b3a66056628a0eeaff0261c97ab5e93c33 +README.zh.md: f86dd863ecc338cca63956ae6e75bf9c7c5fea6a diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index edfcb83193..633a35b3a6 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -36,7 +36,7 @@ Target packages declaration-merge their snapshot and Location data maps, then re ## Shell and standard props -The package registers the optional-Session `conversation` shell, strict Session header/body entries, View list, composer chain and bar, input regions, Hero regions, queue dock, draft persistence, and phase calculation. `ctx.uiSession.provide()` materializes the Conversation and input sources from the same Session binding and supplies `inputActions` as a stable standard prop. +The package occupies the root-scoped `main` key `conversation`, whose wrapper declares the optional-Session `main.conversation` shell. It registers strict Session header/body entries, View list, composer chain and bar, input regions, Hero regions, queue dock, draft persistence, and phase calculation. `ctx.uiSession.provide()` materializes the Conversation and input sources from the same Session binding and supplies `inputActions` as a stable standard prop. View selection is deterministic: a registered persisted selection wins, otherwise registered `chat` wins, otherwise no View renders. It never chooses the first registered View. Shell phase combines Session lifecycle with the active-target set; no target-specific snapshot is read by the shell. diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index f92e4c77ba..f86dd863ec 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -36,7 +36,7 @@ target package 通过 declaration merge 扩展 snapshot 与 Location data map, ## Shell 与标准 props -本包注册 optional-Session `conversation` shell、strict Session header/body、View list、composer chain 与 bar、输入区域、Hero 区域、queue dock、草稿持久化和 phase 计算。`ctx.uiSession.provide()` 从同一个 Session binding 物化 Conversation 与 input source,并将 `inputActions` 作为稳定标准 prop 提供。 +本包占据 root 作用域 `main` 中的 `conversation` key,其包装层声明 optional-Session `main.conversation` shell。本包注册strict Session header/body、View list、composer chain 与 bar、输入区域、Hero 区域、queue dock、草稿持久化和 phase 计算。`ctx.uiSession.provide()` 从同一个 Session binding 物化 Conversation 与 input source,并将 `inputActions` 作为稳定标准 prop 提供。 View 选择规则固定:有效且已注册的持久化选择优先,其次是已注册的 `chat`,否则不渲染 View;绝不选择第一个已注册 View。Shell phase 只组合 Session lifecycle 与 active-target set,不读取任何 target-specific snapshot。 diff --git a/packages/client/ui-layout/README.i18n.yaml b/packages/client/ui-layout/README.i18n.yaml index eb1cbebbee..bf8dc86fe1 100644 --- a/packages/client/ui-layout/README.i18n.yaml +++ b/packages/client/ui-layout/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-layout/README.md -README.md: 0e353e21f39ba8a84e661f1c540db2c2424bcbfa -README.zh.md: 16471fdabb46bbfb35158ef85aa2544db4ceae93 +README.md: 50fef3853cdab83c7c04edd46d73a31967a495b7 +README.zh.md: d649643155dd5e23ee185ff282aa14011ed73793 diff --git a/packages/client/ui-layout/README.md b/packages/client/ui-layout/README.md index 0e353e21f3..50fef3853c 100644 --- a/packages/client/ui-layout/README.md +++ b/packages/client/ui-layout/README.md @@ -25,7 +25,9 @@ This package provides the Web GUI's three-column AppFrame, edge-column widths, a ## Use this package -The root slot composes the sidebar, conversation, and right column. The sidebar spans 264–420px, defaults to 280px, and retains a 56px rail when collapsed; below 1024px it collapses automatically, and opening the right panel collapses a manually expanded sidebar. The right panel first opens at 45% of the viewport, then retains the user's pixel preference, capped at 70%. To protect 400px for the center, the frame first reduces the right panel to 300px, then reports insufficient room so its occupant closes it, and only then compresses the center further. Dragging has no transition delay; the right handle is absent while closed or fullscreen. +The root slot composes the sidebar, main content, and right column. The sidebar spans 264–420px, defaults to 280px, and retains a 56px rail when collapsed; below 1024px it collapses automatically, and opening the right panel collapses a manually expanded sidebar. The right panel first opens at 45% of the viewport, then retains the user's pixel preference, capped at 70%. To protect 400px for the center, the frame first reduces the right panel to 300px, then reports insufficient room so its occupant closes it, and only then compresses the center further. Dragging has no transition delay; the right handle is absent while closed or fullscreen. + +Global panels occupy the root-scoped `main` keyed slot; `conversation` is the reserved key for the Conversation. `ctx.layout.selectPanel(id)` selects a registered panel, and `null` selects the Conversation without changing the current Session. No global panel is registered by the shipped composition. ### Theme presentation @@ -39,7 +41,7 @@ The presenter consumes resolved theme snapshots and projects them onto the docum
    Implementation internals — click to expand -One registration declares four child slots and binds `ctx.layout` methods `toggleSidebar`, `openRightbar(track, fullscreen)`, and `closeRightbar`. The store owns the single frame-width measurement, width preferences, and the occupant's presentation report. The `rightbar` owner supplies actual `width`, `viewportWidth`, and normal-presentation eligibility `canShow`; insufficient room causes a deterministic close, never automatic reopening on widening. Fullscreen hides the width handle without releasing a track the occupant retains. AppFrame always mounts the conversation and right columns; a connected Session renders through `SessionProvider`, and without one the right column is an empty zero-width track. It projects the selected Session title over the build-configured product title or the localized `common.brand.localBuild` fallback, so locale revisions update document metadata with the root entry. The theme presenter is a second effect: pure DOM writes from resolved snapshots — initial state through the getter once, then event-driven only, with no React path. It applies palette, font-size, and token variables before measuring the rendered background as the single color authority. Fullscreen presentation suppresses grid and handle transitions; its occupant reports the new columns only after covering the frame. Fullscreen exit keeps transitions suppressed while the frame installs its destination geometry: close removes the right track, and restore retains it. Subsequent normal geometry actions restore ordinary transitions. +One registration declares four child slots and binds `ctx.layout` methods `selectPanel`, `toggleSidebar`, `openRightbar(track, fullscreen)`, and `closeRightbar`. One root store separates `panelInfo` selection from `layoutInfo` measurements, width preferences, and presentation reports. `usePanelInfo` subscribes to the stable selection object; AppFrame subscribes to the stable layout object. The `rightbar` owner supplies actual `width`, `viewportWidth`, and normal-presentation eligibility `canShow`; insufficient room causes a deterministic close, never automatic reopening on widening. Fullscreen hides the width handle without releasing a track the occupant retains. AppFrame keeps the column containers mounted. The right column's root controller renders `rightbar.session` through `SessionProvider` only while the Conversation is selected; its unmount report releases the track. The independent title component uses the selected Session title only while the Conversation is visible, with the build-configured product title or localized `common.brand.localBuild` as its fallback; locale revisions update that fallback. The theme presenter is a second effect: pure DOM writes from resolved snapshots — initial state through the getter once, then event-driven only, with no React path. It applies palette, font-size, and token variables before measuring the rendered background as the single color authority. Fullscreen presentation suppresses grid and handle transitions; its occupant reports the new columns only after covering the frame. Fullscreen exit keeps transitions suppressed while the frame installs its destination geometry: close removes the right track, and restore retains it. Subsequent normal geometry actions restore ordinary transitions.
    @@ -51,7 +53,7 @@ One registration declares four child slots and binds `ctx.layout` methods `toggl Read these pages when the layout surface is not enough. They move from the frame to the columns it renders and the theme it presents. - [ui-sidebar](../ui-sidebar/README.md) — occupies the `sidebar` column and its seats. -- [ui-conversation](../ui-conversation/README.md) — occupies the `conversation` column. +- [ui-conversation](../ui-conversation/README.md) — occupies the `main` key `conversation`. - [ui-sidebar-right](../ui-sidebar-right/README.md) — occupies the `rightbar` column with one docking surface per session. - [ui-theme](../ui-theme/README.md) — the theme seam whose resolved snapshots the presenter consumes. - [Web client architecture](../../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md) — how browser plugin rows load and register slots. diff --git a/packages/client/ui-layout/README.zh.md b/packages/client/ui-layout/README.zh.md index 16471fdabb..d649643155 100644 --- a/packages/client/ui-layout/README.zh.md +++ b/packages/client/ui-layout/README.zh.md @@ -25,7 +25,9 @@ kind: "package-reference" ## 使用本包 -本插件在 root slot 组合侧栏、会话与右栏。左栏为264~420px,默认280px,收起后保留56px;窗口低于1024px时自动收起,打开右栏也会收起手动展开的左栏。右栏首次打开使用窗口宽度的45%,之后保留用户像素偏好,上限为70%;中栏不足400px时先把右栏压到300px,仍不足则通知占用方收起,最后才继续压缩中栏。拖拽跟手且无过渡延迟,关闭或全屏时不显示右栏拖拽区。 +本插件在 root slot 组合侧栏、中央内容与右栏。左栏为264~420px,默认280px,收起后保留56px;窗口低于1024px时自动收起,打开右栏也会收起手动展开的左栏。右栏首次打开使用窗口宽度的45%,之后保留用户像素偏好,上限为70%;中栏不足400px时先把右栏压到300px,仍不足则通知占用方收起,最后才继续压缩中栏。拖拽跟手且无过渡延迟,关闭或全屏时不显示右栏拖拽区。 + +全局面板占据 root 作用域的 `main` keyed slot;`conversation` 是为会话界面保留的 key。`ctx.layout.selectPanel(id)` 选中已注册面板,`null` 则选中会话界面,但不改变当前会话。默认组合不注册任何全局面板。 ### 主题呈现 @@ -39,7 +41,7 @@ kind: "package-reference"
    实现细节——点击展开 -一次注册声明四个子slot并绑定 `ctx.layout` 的 `toggleSidebar`、`openRightbar(track, fullscreen)` 与 `closeRightbar`。store持有唯一的frame宽度测量、左右栏偏好及占用方报告的呈现状态。`rightbar` 的owner参数为实际 `width`、`viewportWidth` 与普通呈现的 `canShow`;占用方在空间不足时执行确定性的收起,变宽不自行重新展开。全屏隐藏宽度手柄,但不自行释放占用方要求保留的轨道。AppFrame 始终挂载会话与右栏;已连接 Session 经 `SessionProvider` 渲染,没有 Session 时右栏是一条空的零宽轨道。它把所选 Session 标题投影到构建配置的产品标题或本地化 `common.brand.localBuild` 回退值之上,因此 locale revision 会随根 entry 一起更新文档元数据。主题呈现器是第二个 effect:从解析后的快照做纯 DOM 写入——初始状态经 getter 读取一次,此后仅事件驱动,不经过 React。它先应用调色板、字号与 token 变量,再把渲染出的背景测量为唯一的颜色依据。 全屏呈现禁用网格和手柄过渡;占用方完全覆盖框架后才报告新的列宽。 退出全屏时,框架先保持无过渡并安装目标布局:关闭移除右轨道,恢复保留右轨道。后续普通几何操作恢复正常过渡。 +一次注册声明四个子slot并绑定 `ctx.layout` 的 `selectPanel`、`toggleSidebar`、`openRightbar(track, fullscreen)` 与 `closeRightbar`。同一个 root 存储把 `panelInfo` 选中态与 `layoutInfo` 测量、宽度偏好、呈现报告分开。`usePanelInfo` 订阅引用稳定的选中态对象,AppFrame 订阅引用稳定的布局对象。`rightbar` 的owner参数为实际 `width`、`viewportWidth` 与普通呈现的 `canShow`;占用方在空间不足时执行确定性的收起,变宽不自行重新展开。全屏隐藏宽度手柄,但不自行释放占用方要求保留的轨道。AppFrame 保持各列容器挂载。右栏的 root 控制器仅在选中会话界面时,经 `SessionProvider` 渲染 `rightbar.session`;内容卸载时的报告释放列宽。独立的标题组件仅在会话界面可见时使用所选会话标题,以构建配置的产品标题或本地化 `common.brand.localBuild` 为回退值;语言变化会更新该回退值。主题呈现器是第二个 effect:从解析后的快照做纯 DOM 写入——初始状态经 getter 读取一次,此后仅事件驱动,不经过 React。它先应用调色板、字号与 token 变量,再把渲染出的背景测量为唯一的颜色依据。 全屏呈现禁用网格和手柄过渡;占用方完全覆盖框架后才报告新的列宽。 退出全屏时,框架先保持无过渡并安装目标布局:关闭移除右轨道,恢复保留右轨道。后续普通几何操作恢复正常过渡。
    @@ -51,7 +53,7 @@ kind: "package-reference" 当布局面不够用时阅读以下页面。它们从框架进入它所渲染的栏与它所呈现的主题。 - [ui-sidebar](../ui-sidebar/README.zh.md)——占据 `sidebar` 栏及其座位。 -- [ui-conversation](../ui-conversation/README.zh.md)——占据 `conversation` 栏。 +- [ui-conversation](../ui-conversation/README.zh.md)——占据 `main` 中的 `conversation` key。 - [ui-sidebar-right](../ui-sidebar-right/README.zh.md)——以每会话一个停靠面占据 `rightbar` 栏。 - [ui-theme](../ui-theme/README.zh.md)——呈现器消费其解析快照的主题 seam。 - [Web 客户端架构](../../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md)——浏览器插件行如何加载并注册槽位。 diff --git a/packages/client/ui-sidebar-right/README.i18n.yaml b/packages/client/ui-sidebar-right/README.i18n.yaml index 8d11c28b28..eff8b1d7a3 100644 --- a/packages/client/ui-sidebar-right/README.i18n.yaml +++ b/packages/client/ui-sidebar-right/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-sidebar-right/README.md -README.md: feb75c6184098f54961ca161b4dc5ca5d0c7fc78 -README.zh.md: 03c841dfdaac970e938cf934e7f09d151268be8b +README.md: 7520a927f8e77b4fca1bbb7e61fe415dda814b4b +README.zh.md: 648a2c20d8e55a48a684a92a351b7900f3a8d517 diff --git a/packages/client/ui-sidebar-right/README.md b/packages/client/ui-sidebar-right/README.md index feb75c6184..7520a927f8 100644 --- a/packages/client/ui-sidebar-right/README.md +++ b/packages/client/ui-sidebar-right/README.md @@ -54,6 +54,8 @@ While the panel is hidden, one button in the conversation header's corner seat ( The panel takes the conversation's ground colour and content font sizes rather than a raised layer of its own: it is a column of the page, not a card over it. +The `rightbar` entry is a root-scoped controller. It reads `usePanelInfo` and mounts the Session-scoped `rightbar.session` subtree only while the Conversation is selected. Switching to a global panel hides the right Sidebar and releases its frame track without deleting the Session's tab state. + ## State diff --git a/packages/client/ui-sidebar-right/README.zh.md b/packages/client/ui-sidebar-right/README.zh.md index 03c841dfda..648a2c20d8 100644 --- a/packages/client/ui-sidebar-right/README.zh.md +++ b/packages/client/ui-sidebar-right/README.zh.md @@ -54,6 +54,8 @@ kind: "package-reference" 面板取会话区的底色与正文字号,而不是自成一层浮起的表面:它是页面的一列,不是压在页面上的卡片。 +`rightbar` 入口是 root 作用域的控制器。它读取 `usePanelInfo`,仅在选中会话界面时挂载 session 作用域的 `rightbar.session` 子树。切换到全局面板会隐藏右侧 Sidebar 并释放框架列宽,但不删除会话的 tab 状态。 + ## 状态 diff --git a/packages/client/ui-sidebar/README.i18n.yaml b/packages/client/ui-sidebar/README.i18n.yaml index d4d0d29e1d..3a87b39288 100644 --- a/packages/client/ui-sidebar/README.i18n.yaml +++ b/packages/client/ui-sidebar/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-sidebar/README.md -README.md: 87b04e965fbe26591e38d49a5c272113edfd28aa -README.zh.md: 5a7f050d7099437e5c8be965e89be405492d5353 +README.md: 00c0ffce05f19c86bff86d0eff01154b98d24aeb +README.zh.md: 4e4c9777a30680a5015e69c11c8cb57b187f1b0a diff --git a/packages/client/ui-sidebar/README.md b/packages/client/ui-sidebar/README.md index 87b04e965f..00c0ffce05 100644 --- a/packages/client/ui-sidebar/README.md +++ b/packages/client/ui-sidebar/README.md @@ -31,6 +31,10 @@ The sidebar is the navigation shell: users see the brand, start new sessions, co The expanded brand row renders `sidebar.brand.mark` and `sidebar.brand.name` as independent single slots; the collapsed rail renders the same mark slot. Without occupants, the shell uses the fish mark and a localized local-build label. A complete build stacks a code badge below the label as `version[-commit][-dirty]`, using `DSH_CLIENT_VERSION`, the optional 7-character `DSH_CLIENT_COMMIT_HASH`, and `DSH_CLIENT_GIT_DIRTY=true`; missing version metadata omits the badge. New Session targets the explicit Workspace used by a scoped action, otherwise the current Session's Workspace, otherwise the most recently active Workspace; when none exists it clears into the blank New Session page. +### Global panel entries + +Plugins add an icon component to the root-scoped `sidebar.panellist` list with an `id`, optional `order`, and a string or locale-aware `label`. The same id addresses the component registered in the layout's root-scoped `main` keyed slot. An optional `sidebar.panellist.title` keyed registration replaces the label's visible content; the label remains the accessible name. Each row reads its own selected state through `usePanelInfo`. With no registrations, neither the list nor spacing for it is rendered. The shipped composition registers no example panel. + ### Collapse behavior During a live collapse, the expanded content fades out at its current width, the upper controls share one fade and leftward translation into the 56px rail, and the layout's column slide ends the motion. A page that starts collapsed renders the rail statically, and reduced-motion mode disables both transitions. The bottom-pinned `sidebar.settings` control shares the fade timing but has no horizontal translation. @@ -47,7 +51,7 @@ Scrollbars in the column are a pointer affordance: the shell rebinds the scrollb
    Implementation internals — click to expand -The shell is pure composition: `SidebarRootComponentProps` composes the layout owner share, the global `useSessions` and `useWorkspaces` hooks, the declared brand, the `sidebar.workspaces` and `sidebar.settings` child slots, and injected `startSession` plus sidebar-toggle callbacks. There is no plugin store. +The shell is pure composition: `SidebarRootComponentProps` composes the layout owner share, the global `useSessions` and `useWorkspaces` hooks, the declared brand, the `sidebar.workspaces` and `sidebar.settings` child slots, and injected navigation callbacks. Panel entries and their optional titles use the same composition path. Panel metadata is derived from list registrations and locale changes; selection belongs to the layout store. ### Slot discipline diff --git a/packages/client/ui-sidebar/README.zh.md b/packages/client/ui-sidebar/README.zh.md index 5a7f050d70..4e4c9777a3 100644 --- a/packages/client/ui-sidebar/README.zh.md +++ b/packages/client/ui-sidebar/README.zh.md @@ -31,6 +31,10 @@ dsh Web 客户端的侧边栏让用户识别当前构建、启动新会话、将 展开的品牌行把 `sidebar.brand.mark` 与 `sidebar.brand.name` 渲染为两个独立的 single slot;收起轨道则渲染同一个 mark slot。没有占位者时,外壳使用鱼形标记和本地化的本地构建标签。完整构建会在标签下方显示代码徽标;该徽标使用 `DSH_CLIENT_VERSION`、可选的 7 位 `DSH_CLIENT_COMMIT_HASH` 与 `DSH_CLIENT_GIT_DIRTY=true` 组装成 `version[-commit][-dirty]`;缺少版本元数据时不显示徽标。New Session 优先使用作用域操作明确指定的 Workspace,否则使用当前 Session 所属 Workspace,再否则使用最近活跃 Workspace;一个 Workspace 都没有时则清空选择,进入空白 New Session 页面。 +### 全局面板入口 + +插件在 root 作用域的 `sidebar.panellist` list 中注册图标组件,提供 `id`、可选 `order`,以及字符串或随语言变化的 `label`。同一个 id 寻址布局中 root 作用域 `main` keyed slot 的组件。可选的 `sidebar.panellist.title` keyed 注册替换标签的可见内容;`label` 仍用作无障碍名称。每一行通过 `usePanelInfo` 读取自己的选中态。没有注册项时,列表及其间距均不渲染。默认组合不注册示例面板。 + ### 折叠行为 实时收起时,展开内容在当前宽度淡出,上方控件共用一次淡入并左移进入 56px 轨道,由布局的栏滑动结束整段动画。页面初始即为收起状态时会静态渲染轨道;减少动态效果模式会禁用两段过渡。固定在底部的 `sidebar.settings` 控件只共用淡入时序,不发生横向位移。 @@ -47,7 +51,7 @@ dsh Web 客户端的侧边栏让用户识别当前构建、启动新会话、将
    实现细节——点击展开 -外壳是纯组合:`SidebarRootComponentProps` 组合布局 owner share、全局 `useSessions` 与 `useWorkspaces` 钩子、已声明的品牌、`sidebar.workspaces` 与 `sidebar.settings` 子 slot,以及注入的 `startSession` 与侧边栏切换回调。这里没有插件 store。 +外壳是纯组合:`SidebarRootComponentProps` 组合布局 owner share、全局 `useSessions` 与 `useWorkspaces` 钩子、已声明的品牌、`sidebar.workspaces` 与 `sidebar.settings` 子 slot,以及注入的导航回调。面板入口及其可选标题使用相同的组合方式。面板元数据由列表注册和语言变化派生;选中态属于布局存储。 ### Slot 纪律 diff --git a/packages/client/ui-workspace/README.i18n.yaml b/packages/client/ui-workspace/README.i18n.yaml index 26b70e0843..b19f293b9c 100644 --- a/packages/client/ui-workspace/README.i18n.yaml +++ b/packages/client/ui-workspace/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-workspace/README.md -README.md: a819b07fecb74faeb3bebb6665aaabfdc8ecfba6 -README.zh.md: f9565b12bccd596fa61d1f1436f82b090139f506 +README.md: 231cff649addac22c4383f372201c2ce79a22100 +README.zh.md: e8bac20531bee009cb61db42749597a415de4279 diff --git a/packages/client/ui-workspace/README.md b/packages/client/ui-workspace/README.md index a819b07fec..231cff649a 100644 --- a/packages/client/ui-workspace/README.md +++ b/packages/client/ui-workspace/README.md @@ -51,6 +51,8 @@ The value is intentionally best effort for cold Sessions. An identity-matching u ----- +`ctx.uiWorkspace.openSession(id)` selects the Session and returns the main area to the Conversation as one UI navigation action, including when that Session was already current. New Session uses the same path after resolving its target; selection failure leaves a global panel visible. Session rows read `usePanelInfo` to suppress their selected appearance while a global panel is active. + ## Understand the implementation diff --git a/packages/client/ui-workspace/README.zh.md b/packages/client/ui-workspace/README.zh.md index f9565b12bc..e8bac20531 100644 --- a/packages/client/ui-workspace/README.zh.md +++ b/packages/client/ui-workspace/README.zh.md @@ -51,6 +51,8 @@ Session 行渲染运行时的实时 `pendingInteraction` 分类:审批显示** ----- +`ctx.uiWorkspace.openSession(id)` 将选中会话与中央区域返回会话界面作为一次 UI 导航操作,包括目标会话已经是当前会话的情况。新会话操作解析目标后使用同一路径;选中失败时保留当前全局面板。会话行读取 `usePanelInfo`,在全局面板活跃时不显示会话选中样式。 + ## 理解实现 diff --git a/packages/test-support/client-runtime/README.i18n.yaml b/packages/test-support/client-runtime/README.i18n.yaml index 9c209aae99..1bdb09785b 100644 --- a/packages/test-support/client-runtime/README.i18n.yaml +++ b/packages/test-support/client-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/test-support/client-runtime/README.md -README.md: 616eab2cd5ef03ae4e514ca335b0c60b43d62cf0 -README.zh.md: 08effcef7eaacad67b5496507925337e772d4ce9 +README.md: 38b0c1b305d83920a819c6eddf9b54ecc895ef7f +README.zh.md: 301e3633d25a7fa7599eb9283d141c641163d588 diff --git a/packages/test-support/client-runtime/README.md b/packages/test-support/client-runtime/README.md index 616eab2cd5..38b0c1b305 100644 --- a/packages/test-support/client-runtime/README.md +++ b/packages/test-support/client-runtime/README.md @@ -29,7 +29,7 @@ This package gives a browser feature spec a real runtime to mount against: creat ### Setting up a feature spec -`SlotTestRuntime.create()` assembles the runtime, `declare(children)` registers an auto frame whose per-key `
    ` wrappers become snapshot roots, `mount(plugin)` runs the feature on a real fiber, and `renderSlot(key, owner)` returns the slot-local view with scoped queries and in-place updates: +`SlotTestRuntime.create()` assembles the runtime, `declare(children)` registers an auto frame whose per-key `
    ` wrappers become snapshot roots, `mount(plugin)` runs the feature on a real fiber, and `renderSlot(key, owner, opts?)` returns the slot-local view with scoped queries and in-place updates: ```text const runtime = await SlotTestRuntime.create() @@ -42,6 +42,8 @@ await runtime.dispose() `mount` prechecks required services and fails loud when one is missing — `provide(name, value)` supplies an extra service first. The runtime provides an unavailable `fileUpload` stub so assemblies can mount; replace `runtime.fileUpload.upload` before mounting when a test exercises upload behavior. `storeOf(key, scopeKey)` returns the live store instance the renderer hands a slot's component for identity and action-driven-write assertions. +The optional render options select a keyed entry with `entryKey` or a list item with `only`; `view.update(owner)` retains that selection. `runtime.panelInfo` supplies the default `usePanelInfo` source with no global panel selected. Release it with `releasePanelInfoSource()` before mounting the production Layout owner. + ### Local DOM snapshots A registered snapshot serializer folds CSS-module class hashes (`_frame_a1b2c3` → `frame`) so `.snap` files stay structural, and collapses `` internals to a `data-content` fingerprint. Suites needing a custom page frame use `root.declare(children, Frame)` instead of the auto frame; `dispose()` tears down views, feature fibers, minted scopes, and persisted store state on one axis and is idempotent. diff --git a/packages/test-support/client-runtime/README.zh.md b/packages/test-support/client-runtime/README.zh.md index 08effcef7e..301e3633d2 100644 --- a/packages/test-support/client-runtime/README.zh.md +++ b/packages/test-support/client-runtime/README.zh.md @@ -29,7 +29,7 @@ kind: "package-library" ### 搭建功能测试 -`SlotTestRuntime.create()` 组装运行时,`declare(children)` 注册一个自动 frame,其逐 key 的 `
    ` 包裹层成为快照根,`mount(plugin)` 在真实 fiber 上运行功能,`renderSlot(key, owner)` 返回带限定查询与原位更新的 slot 局部视图: +`SlotTestRuntime.create()` 组装运行时,`declare(children)` 注册一个自动 frame,其逐 key 的 `
    ` 包裹层成为快照根,`mount(plugin)` 在真实 fiber 上运行功能,`renderSlot(key, owner, opts?)` 返回带限定查询与原位更新的 slot 局部视图: ```text const runtime = await SlotTestRuntime.create() @@ -42,6 +42,8 @@ await runtime.dispose() `mount` 会预检必需服务,缺失时自明报错——先用 `provide(name, value)` 提供额外服务。运行时会提供不可用的 `fileUpload` 替身,使装配可以挂载;测试上传行为时,需要在挂载前替换 `runtime.fileUpload.upload`。`storeOf(key, scopeKey)` 返回渲染器交给 slot 组件的实时 store 实例,用于身份与动作驱动写入断言。 +可选渲染参数通过 `entryKey` 选择 keyed 条目,或通过 `only` 选择 list 条目;`view.update(owner)` 保留该选择。`runtime.panelInfo` 提供默认的 `usePanelInfo` 数据源,初始不选中全局面板。挂载生产 Layout 所有者之前,先调用 `releasePanelInfoSource()` 释放该数据源。 + ### 局部 DOM 快照 注册的快照序列化器把 CSS-module 哈希类名折回语义名(`_frame_a1b2c3` → `frame`),使 `.snap` 文件只含结构,并把 `` 内部折叠为 `data-content` 指纹。需要自定义页面 frame 的套件改用 `root.declare(children, Frame)` 而非自动 frame;`dispose()` 沿单一轴拆除视图、feature fiber、已铸 scope 与持久化 store 状态,且幂等。 From 9736136d9598f76d5114b8e58e71d2e74bf95965 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Tue, 8 Sep 2026 18:55:50 +0800 Subject: [PATCH 38/58] fix(client): leave global panel entries empty by default --- .../hello-world/HelloWorldPanel.module.css | 33 ------ .../client/hello-world/HelloWorldPanel.tsx | 29 ----- .../client/ui-sidebar/src/client/index.ts | 17 +-- .../client/ui-sidebar/src/client/locales.ts | 4 - .../sidebar-snapshot.client.spec.tsx.snap | 107 ------------------ .../ui-sidebar/tests/apply.client.spec.tsx | 36 +++++- .../src/client/api-catalog.ts | 4 +- .../src/client/slot-catalog.ts | 5 +- 8 files changed, 37 insertions(+), 198 deletions(-) delete mode 100644 packages/client/ui-sidebar/src/client/hello-world/HelloWorldPanel.module.css delete mode 100644 packages/client/ui-sidebar/src/client/hello-world/HelloWorldPanel.tsx diff --git a/packages/client/ui-sidebar/src/client/hello-world/HelloWorldPanel.module.css b/packages/client/ui-sidebar/src/client/hello-world/HelloWorldPanel.module.css deleted file mode 100644 index 94f03cdb3f..0000000000 --- a/packages/client/ui-sidebar/src/client/hello-world/HelloWorldPanel.module.css +++ /dev/null @@ -1,33 +0,0 @@ -/* Root-scoped panel content; the layout owns its column dimensions. */ -.root { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 16px; - min-height: 100%; - padding: 32px; - box-sizing: border-box; - color: var(--dsw-alias-label-primary); - text-align: center; -} - -.icon { - display: inline-flex; - color: var(--dsw-alias-label-secondary); -} - -.title { - margin: 0; - font-size: 28px; - font-weight: 600; - line-height: 36px; -} - -.description { - max-width: 480px; - margin: 0; - color: var(--dsw-alias-label-secondary); - font-size: 14px; - line-height: 22px; -} diff --git a/packages/client/ui-sidebar/src/client/hello-world/HelloWorldPanel.tsx b/packages/client/ui-sidebar/src/client/hello-world/HelloWorldPanel.tsx deleted file mode 100644 index 1b35fa7ec2..0000000000 --- a/packages/client/ui-sidebar/src/client/hello-world/HelloWorldPanel.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/** Minimal global panel and its sidebar icon, composed through separate slots. */ -import { IconGlobeOutline14 } from '@deepseek-ai/dsh-client-ui-primitives' -import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' -import type {} from '@deepseek-ai/dsh-client-ui-layout/client' -import css from './HelloWorldPanel.module.css' - -/** - * Render the Hello World navigation icon; the sidebar owns the button and label. - * @param props - framework-composed panel icon props. - * @returns the globe glyph. - */ -export function HelloWorldIcon({ size }: PropsRuntime<'sidebar.panellist'>) { - return -} - -/** - * Render a root-scoped panel independent of the selected Session. - * @param props - main slot props and the sidebar locale seat. - * @returns the global panel content. - */ -export function HelloWorldPanel({ t }: PropsRuntime<'main'> & PropsLocale<'sidebar'>) { - return ( -
    -
    -

    {t('panel.helloWorld.title')}

    -

    {t('panel.helloWorld.description')}

    - - ) -} diff --git a/packages/client/ui-sidebar/src/client/index.ts b/packages/client/ui-sidebar/src/client/index.ts index 91bfb40818..361ae4d612 100644 --- a/packages/client/ui-sidebar/src/client/index.ts +++ b/packages/client/ui-sidebar/src/client/index.ts @@ -1,4 +1,4 @@ -/** Registers the sidebar shell, global panel navigation, and Hello World panel. */ +/** Registers the sidebar shell and global panel navigation. */ import type { Context as ClientContext } from '@deepseek-ai/cordis' import { createSnapshotStore } from '@deepseek-ai/dsh-client-store' import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots' @@ -10,7 +10,6 @@ import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' // Type-only: pulls the Session root standard-props merge. import type {} from '@deepseek-ai/dsh-client-ui-session/client' import type { SidebarPanelMetadata, SidebarRootInjected } from './contract/slots.ts' -import { HelloWorldIcon, HelloWorldPanel } from './hello-world/HelloWorldPanel.tsx' import { SidebarRoot } from './SidebarRoot.tsx' import { en, zh, type SidebarKey } from './locales.ts' @@ -31,8 +30,6 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { /** Dictionary namespace owned by this plugin. */ const NS = 'sidebar' -const HELLO_WORLD_PANEL_ID = 'hello-world' as MainPanelId - interface WorkspaceNavigation { startSession(workspaceId?: Parameters[0]): void } @@ -46,7 +43,6 @@ export const inject = ['slots', 'layout', 'uiWorkspace', 'locale'] export function apply(ctx: ClientContext): void { const workspaceNavigation = ctx.get('uiWorkspace') as unknown as WorkspaceNavigation ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-sidebar: dictionaries') - const t = ctx.locale.bind(NS) const panels = createSnapshotStore([]) const syncPanels = (): void => { const next = ctx.slots.entriesOfSlot('sidebar.panellist').map(({ options }) => { @@ -86,16 +82,5 @@ export function apply(ctx: ClientContext): void { }, inject: injectProps, }, SidebarRoot)) - ctx.slots.inject('main', () => ctx.slots.register({ - name: 'main', - key: HELLO_WORLD_PANEL_ID, - locale: NS, - }, HelloWorldPanel)) - ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({ - name: 'sidebar.panellist', - id: HELLO_WORLD_PANEL_ID, - order: 100, - label: () => t('panel.helloWorld.title'), - }, HelloWorldIcon)) syncPanels() } diff --git a/packages/client/ui-sidebar/src/client/locales.ts b/packages/client/ui-sidebar/src/client/locales.ts index e0df89de4c..bdd1486333 100644 --- a/packages/client/ui-sidebar/src/client/locales.ts +++ b/packages/client/ui-sidebar/src/client/locales.ts @@ -7,8 +7,6 @@ export const zh = { 'toggle.open': '打开侧边栏', 'toggle.collapse': '收起侧边栏', 'panels.label': '全局面板', - 'panel.helloWorld.title': 'Hello World', - 'panel.helloWorld.description': '这是一个全局面板,不属于任何会话。', } satisfies Record /** The sidebar namespace key union. */ @@ -21,6 +19,4 @@ export const en = { 'toggle.open': 'Open sidebar', 'toggle.collapse': 'Collapse sidebar', 'panels.label': 'Global panels', - 'panel.helloWorld.title': 'Hello World', - 'panel.helloWorld.description': 'This global panel is independent of any session.', } satisfies Record diff --git a/packages/client/ui-sidebar/tests/__snapshots__/sidebar-snapshot.client.spec.tsx.snap b/packages/client/ui-sidebar/tests/__snapshots__/sidebar-snapshot.client.spec.tsx.snap index 62aff29600..ba3e4d9540 100644 --- a/packages/client/ui-sidebar/tests/__snapshots__/sidebar-snapshot.client.spec.tsx.snap +++ b/packages/client/ui-sidebar/tests/__snapshots__/sidebar-snapshot.client.spec.tsx.snap @@ -60,35 +60,6 @@ exports[`sidebar shell snapshots > renders the collapsed rail after the crossfad xmlns="http://www.w3.org/2000/svg" /> -
    @@ -219,45 +190,6 @@ exports[`sidebar shell snapshots > renders the expanded column (wordmark, capsul New Session -
    @@ -388,45 +320,6 @@ exports[`sidebar shell snapshots > renders the expanded column in the default lo 新会话 -
    diff --git a/packages/client/ui-sidebar/tests/apply.client.spec.tsx b/packages/client/ui-sidebar/tests/apply.client.spec.tsx index f0cf52c5f3..9afd13bc99 100644 --- a/packages/client/ui-sidebar/tests/apply.client.spec.tsx +++ b/packages/client/ui-sidebar/tests/apply.client.spec.tsx @@ -57,9 +57,8 @@ describe('ui-sidebar apply', () => { expect(b.slots.entries('sidebar')[0]!.locale).toBe('sidebar') const injected = (b.slots.entries('sidebar')[0]!.inject as () => SidebarRootInjected)() expect(Object.keys(injected)).toEqual(['startSession', 'toggleSidebar', 'selectPanel', 'hooks']) - expect(injected.hooks.panels.getSnapshot()).toEqual([ - { id: 'hello-world', order: 100, label: 'Hello World' }, - ]) + expect(injected.hooks.panels.getSnapshot()).toEqual([]) + expect(b.slots.entries('main')).toEqual([]) // Both arms delegate to the Workspace UI's shared New Session action. injected.startSession('workspace' as never) expect(b.uiWorkspace.startSession).toHaveBeenCalledWith('workspace') @@ -83,6 +82,37 @@ describe('ui-sidebar apply', () => { await fiber.dispose() }) + it('projects plugin-owned panel entries and removes them with their owner', async () => { + const b = await bench() + const sidebar = b.ctx.plugin({ inject: [...inject], apply }) + await sidebar.await() + const injected = (b.slots.entries('sidebar')[0]!.inject as () => SidebarRootInjected)() + const panel = b.ctx.plugin({ + inject: ['slots'], + apply(ctx: Context) { + ctx.slots.register({ name: 'main', key: 'custom-panel' }, () => 'Custom panel') + ctx.slots.register({ + name: 'sidebar.panellist', id: 'custom-panel', order: 20, label: 'Custom panel', + }, () => null) + }, + }) + try { + await panel.await() + await vi.waitFor(() => { + expect(injected.hooks.panels.getSnapshot()).toEqual([ + { id: 'custom-panel', order: 20, label: 'Custom panel' }, + ]) + }) + expect(b.slots.entries('main').map(entry => entry.options.key)).toEqual(['custom-panel']) + await panel.dispose() + await vi.waitFor(() => { expect(injected.hooks.panels.getSnapshot()).toEqual([]) }) + expect(b.slots.entries('main')).toEqual([]) + } finally { + await panel.dispose() + await sidebar.dispose() + } + }) + it('removes the entry and child declaration on teardown', async () => { const b = await bench() const fiber = b.ctx.plugin({ inject: [...inject], apply }) diff --git a/packages/extensions/cordis-client-runner/src/client/api-catalog.ts b/packages/extensions/cordis-client-runner/src/client/api-catalog.ts index 47bf4af64a..8f4ea2f887 100644 --- a/packages/extensions/cordis-client-runner/src/client/api-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/api-catalog.ts @@ -83,8 +83,8 @@ export interface TypeApiEntry { export const SERVICE_API: readonly ServiceApiEntry[] = [ { key: 'layout', - summary: 'The outward layout face (`ctx.layout`): the panel transitions other plugins may trigger — and exactly what a test fake must supply.', - description: 'The outward layout face (`ctx.layout`): the panel transitions other plugins may trigger — and exactly what a test fake must supply. The attachPanels wiring hook stays on the concrete class (root-entry assembly only).', + summary: 'Panel navigation and geometry actions exposed through ctx.layout.', + description: 'Panel navigation and geometry actions exposed through ctx.layout.', methods: [ { signature: 'toggleSidebar(): void', diff --git a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts index 93db9d89c0..691bc619fc 100644 --- a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts @@ -1385,7 +1385,6 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ declaredBy: 'an entry in \'root\' (client-ui-layout), so it exists while that entry is mounted', occupants: [ 'client-ui-conversation ConversationPanel key \'conversation\'', - 'client-ui-sidebar HelloWorldPanel', ], replaceRisk: 'shadows-shipped-ui', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'main\', () => ctx.slots.register(\n { name: \'main\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', @@ -2218,9 +2217,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ hookContext: '', slotInject: '', declaredBy: 'an entry in \'sidebar\' (client-ui-sidebar), so it exists while that entry is mounted', - occupants: [ - 'client-ui-sidebar HelloWorldIcon', - ], + occupants: [], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.panellist\', () => ctx.slots.register(\n { name: \'sidebar.panellist\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:32', From 092d16d7ab5a64a55596af74899d2b3db4647310 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Tue, 8 Sep 2026 19:18:37 +0800 Subject: [PATCH 39/58] style(client): satisfy panel lint rules --- .../client/ui-layout/src/client/DocumentTitle.tsx | 2 +- .../client/ui-layout/tests/apply.client.spec.ts | 6 +++--- .../ui-layout/tests/document-title.client.spec.tsx | 14 +++++++------- packages/client/ui-sidebar/src/client/index.ts | 2 +- .../tests/pointer-scrollbars.client.spec.tsx | 3 ++- .../ui-sidebar/tests/sidebar-root.client.spec.tsx | 12 ++++++++---- 6 files changed, 22 insertions(+), 17 deletions(-) diff --git a/packages/client/ui-layout/src/client/DocumentTitle.tsx b/packages/client/ui-layout/src/client/DocumentTitle.tsx index 13663f53d1..69cd5f4cfe 100644 --- a/packages/client/ui-layout/src/client/DocumentTitle.tsx +++ b/packages/client/ui-layout/src/client/DocumentTitle.tsx @@ -16,7 +16,7 @@ export type DocumentTitleProps = Pick, 'useSessions' | 'use */ export function DocumentTitle({ useSessions, usePanelInfo, productTitle }: DocumentTitleProps): null { const showSessionTitle = usePanelInfo(info => info.activePanelId === null) - const title = useSessions(state => { + const title = useSessions((state) => { const current = state.current return !showSessionTitle || current === undefined ? undefined : state.byId[current]?.title }) diff --git a/packages/client/ui-layout/tests/apply.client.spec.ts b/packages/client/ui-layout/tests/apply.client.spec.ts index 87123fc922..94124c2805 100644 --- a/packages/client/ui-layout/tests/apply.client.spec.ts +++ b/packages/client/ui-layout/tests/apply.client.spec.ts @@ -23,7 +23,7 @@ beforeEach(() => { originalBodyStyle = document.body.getAttribute('style') originalDarkTheme = document.body.getAttribute('data-ds-dark-theme') originalThemeMetadata = [...document.head.querySelectorAll('meta[name="theme-color"]')] - originalThemeMetadata.forEach(node => { node.remove() }) + originalThemeMetadata.forEach((node) => { node.remove() }) vi.stubGlobal('innerWidth', 1920) }) @@ -35,7 +35,7 @@ afterEach(async () => { restoreAttribute(document.documentElement, 'style', originalRootStyle) restoreAttribute(document.body, 'style', originalBodyStyle) restoreAttribute(document.body, 'data-ds-dark-theme', originalDarkTheme) - document.head.querySelectorAll('meta[name="theme-color"]').forEach(node => { node.remove() }) + document.head.querySelectorAll('meta[name="theme-color"]').forEach((node) => { node.remove() }) document.head.append(...originalThemeMetadata) vi.unstubAllGlobals() } @@ -65,7 +65,7 @@ async function bench() { await slotsFiber.await() const slots = ctx.get('slots') as SlotRegistry let host: SlotRendererHost | undefined - slots.install({ renderRoot: value => { host = value; return null } }) + slots.install({ renderRoot: (value) => { host = value; return null } }) const rendererHost = (): SlotRendererHost => { slots.renderSlot('root', {}) if (host === undefined) throw new Error('the root renderer did not receive its host') diff --git a/packages/client/ui-layout/tests/document-title.client.spec.tsx b/packages/client/ui-layout/tests/document-title.client.spec.tsx index 87def87bda..5c3184970f 100644 --- a/packages/client/ui-layout/tests/document-title.client.spec.tsx +++ b/packages/client/ui-layout/tests/document-title.client.spec.tsx @@ -38,11 +38,11 @@ describe('DocumentTitle', () => { document.title = 'stale title' const mounted = render() expect(document.title).toBe('DeepSeek Harness') - act(() => { sessions.update(state => { state.byId[sessionId]!.title = 'First title' }) }) + act(() => { sessions.update((state) => { state.byId[sessionId]!.title = 'First title' }) }) expect(document.title).toBe('First title — DeepSeek Harness') - act(() => { sessions.update(state => { state.byId[sessionId]!.title = 'Revised title' }) }) + act(() => { sessions.update((state) => { state.byId[sessionId]!.title = 'Revised title' }) }) expect(document.title).toBe('Revised title — DeepSeek Harness') - act(() => { sessions.update(state => { state.current = undefined }) }) + act(() => { sessions.update((state) => { state.current = undefined }) }) expect(document.title).toBe('DeepSeek Harness') mounted.unmount() expect(document.title).toBe('DeepSeek Harness') @@ -50,7 +50,7 @@ describe('DocumentTitle', () => { it('uses the localized product title supplied by the frame', () => { const { sessionId, sessions, props } = titleSources() - sessions.update(state => { state.byId[sessionId]!.title = 'First title' }) + sessions.update((state) => { state.byId[sessionId]!.title = 'First title' }) const mounted = render() expect(document.title).toBe('First title — DSH Local Build') mounted.unmount() @@ -59,12 +59,12 @@ describe('DocumentTitle', () => { it('keeps the product title across global panels and restores the latest Session title on return', () => { const { sessionId, sessions, panelInfo, props } = titleSources() - sessions.update(state => { state.byId[sessionId]!.title = 'Session title' }) + sessions.update((state) => { state.byId[sessionId]!.title = 'Session title' }) render() expect(document.title).toBe('Session title — Product') act(() => { panelInfo.set({ activePanelId: 'panel-a' as MainPanelId }) }) expect(document.title).toBe('Product') - act(() => { sessions.update(state => { state.byId[sessionId]!.title = 'Updated title' }) }) + act(() => { sessions.update((state) => { state.byId[sessionId]!.title = 'Updated title' }) }) expect(document.title).toBe('Product') act(() => { panelInfo.set({ activePanelId: 'panel-b' as MainPanelId }) }) expect(document.title).toBe('Product') @@ -75,7 +75,7 @@ describe('DocumentTitle', () => { it('uses the product title when the current Session row is not available', () => { const { sessions, props } = titleSources() - sessions.update(state => { state.byId = {}; state.ids = [] }) + sessions.update((state) => { state.byId = {}; state.ids = [] }) render() expect(document.title).toBe('Product') }) diff --git a/packages/client/ui-sidebar/src/client/index.ts b/packages/client/ui-sidebar/src/client/index.ts index 361ae4d612..6fc1bde880 100644 --- a/packages/client/ui-sidebar/src/client/index.ts +++ b/packages/client/ui-sidebar/src/client/index.ts @@ -52,7 +52,7 @@ export function apply(ctx: ClientContext): void { }).sort((a, b) => a.order - b.order) const previous = panels.getSnapshot() if (previous.length === next.length && previous.every((panel, index) => { - const candidate = next[index]! + const candidate = next[index] as SidebarPanelMetadata return panel.id === candidate.id && panel.order === candidate.order && panel.label === candidate.label })) return panels.set(next) diff --git a/packages/client/ui-sidebar/tests/pointer-scrollbars.client.spec.tsx b/packages/client/ui-sidebar/tests/pointer-scrollbars.client.spec.tsx index 065b9d2816..e010a3385f 100644 --- a/packages/client/ui-sidebar/tests/pointer-scrollbars.client.spec.tsx +++ b/packages/client/ui-sidebar/tests/pointer-scrollbars.client.spec.tsx @@ -41,7 +41,8 @@ function mountColumn(): { column: HTMLElement; quiet: () => boolean } { {}} usePanels={selector => selector([])} useResource={useResource} useWorkspaces={neverHook} + usePanelInfo={usePanelInfo} selectPanel={() => {}} usePanels={selector => selector([])} + useResource={useResource} useWorkspaces={neverHook} startSession={vi.fn()} toggleSidebar={vi.fn()} t={t} renderSlot={((_key: string, owner: SidebarSectionOwnerProps) =>
    ) as SidebarRootComponentProps['renderSlot']} diff --git a/packages/client/ui-sidebar/tests/sidebar-root.client.spec.tsx b/packages/client/ui-sidebar/tests/sidebar-root.client.spec.tsx index 7a689ad2fa..c7f890303f 100644 --- a/packages/client/ui-sidebar/tests/sidebar-root.client.spec.tsx +++ b/packages/client/ui-sidebar/tests/sidebar-root.client.spec.tsx @@ -46,7 +46,8 @@ function mountShell({ collapsed = false, width = 300 }: { collapsed?: boolean; w {}} usePanels={selector => selector([])} useResource={useResource} useWorkspaces={neverHook} + usePanelInfo={usePanelInfo} selectPanel={() => {}} usePanels={selector => selector([])} + useResource={useResource} useWorkspaces={neverHook} startSession={startSession} toggleSidebar={toggleSidebar} t={t} renderSlot={(( key: string, @@ -111,7 +112,8 @@ describe('SidebarRoot shell', () => { const { container } = render( {}} usePanels={selector => selector([])} useResource={useResource} useWorkspaces={neverHook} + usePanelInfo={usePanelInfo} selectPanel={() => {}} usePanels={selector => selector([])} + useResource={useResource} useWorkspaces={neverHook} startSession={vi.fn()} toggleSidebar={vi.fn()} t={t} renderSlot={((_key: string, _owner: unknown, options?: { fallback?: ReactNode }) => options?.fallback ?? null) as SidebarRootComponentProps['renderSlot']} @@ -130,7 +132,8 @@ describe('SidebarRoot shell', () => { render( {}} usePanels={selector => selector([])} useResource={useResource} useWorkspaces={neverHook} + usePanelInfo={usePanelInfo} selectPanel={() => {}} usePanels={selector => selector([])} + useResource={useResource} useWorkspaces={neverHook} startSession={vi.fn()} toggleSidebar={vi.fn()} t={t} renderSlot={((_key: string, _owner: unknown, options?: { fallback?: ReactNode }) => options?.fallback ?? null) as SidebarRootComponentProps['renderSlot']} @@ -144,7 +147,8 @@ describe('SidebarRoot shell', () => { render( {}} usePanels={selector => selector([])} useResource={useResource} useWorkspaces={neverHook} + usePanelInfo={usePanelInfo} selectPanel={() => {}} usePanels={selector => selector([])} + useResource={useResource} useWorkspaces={neverHook} startSession={vi.fn()} toggleSidebar={vi.fn()} t={t} renderSlot={((_key: string, _owner: unknown, options?: { fallback?: ReactNode }) => options?.fallback ?? null) as SidebarRootComponentProps['renderSlot']} From bc840b483e1bbd772a9efd5a139cc980408aa0a8 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:35:49 +0800 Subject: [PATCH 40/58] feat(client): simplify and exercise sidebar panel entries --- .../ui-sidebar/src/client/SidebarRoot.tsx | 4 +- .../ui-sidebar/src/client/contract/slots.ts | 15 +- .../client/ui-sidebar/src/client/index.ts | 3 +- .../ui-sidebar/tests/apply.client.spec.tsx | 26 +- .../tests/panel-list.client.spec.tsx | 276 ++++++++++++++++++ .../tests/workspace-browser.client.spec.tsx | 25 ++ 6 files changed, 329 insertions(+), 20 deletions(-) create mode 100644 packages/client/ui-sidebar/tests/panel-list.client.spec.tsx diff --git a/packages/client/ui-sidebar/src/client/SidebarRoot.tsx b/packages/client/ui-sidebar/src/client/SidebarRoot.tsx index 94232d3043..35fdee173b 100644 --- a/packages/client/ui-sidebar/src/client/SidebarRoot.tsx +++ b/packages/client/ui-sidebar/src/client/SidebarRoot.tsx @@ -53,7 +53,7 @@ type PanelRowProps = & Pick & Pick, 'usePanelInfo'> & Pick, 'selectPanel'> - & PropsRenderSlots<'sidebar.panellist' | 'sidebar.panellist.title'> + & PropsRenderSlots<'sidebar.panellist'> /** Each panel row subscribes only to its own selection state. */ function PanelRow({ id, label, wide, usePanelInfo, selectPanel, renderSlot }: PanelRowProps) { @@ -72,7 +72,7 @@ function PanelRow({ id, label, wide, usePanelInfo, selectPanel, renderSlot }: Pa {wide && ( - {renderSlot('sidebar.panellist.title', { active }, { entryKey: id, fallback: label })} + {label} )} diff --git a/packages/client/ui-sidebar/src/client/contract/slots.ts b/packages/client/ui-sidebar/src/client/contract/slots.ts index c3bac9a042..df2933430e 100644 --- a/packages/client/ui-sidebar/src/client/contract/slots.ts +++ b/packages/client/ui-sidebar/src/client/contract/slots.ts @@ -30,8 +30,6 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { * the sidebar owns the button and resolves its label from list metadata. */ 'sidebar.panellist': { kind: 'list'; scope: 'root'; owner: SidebarPanelIconOwnerProps } - /** Global panel titles keyed by list id, with the list label as fallback. */ - 'sidebar.panellist.title': { kind: 'keyed'; scope: 'root'; owner: SidebarPanelTitleOwnerProps } /** * The workspace/session browsing region: section header, search, the * grouped/flat session list, and every workspace dialog. Declared by this @@ -73,19 +71,13 @@ export interface SidebarPanelIconOwnerProps { active: boolean } -/** Title presentation supplied by the global panel row. */ -export interface SidebarPanelTitleOwnerProps { - /** Whether this panel is selected in the main column. */ - active: boolean -} - /** Serializable metadata for one active global panel list registration. */ export interface SidebarPanelMetadata { /** List id and matching main panel key. */ id: MainPanelId /** Ascending row order; ties retain registration order. */ order: number - /** Localized accessible name and fallback title. */ + /** Row title and accessible name: resolved label, or the id when omitted. */ label: string } @@ -128,8 +120,8 @@ export type SidebarRootInjected = { startSession: (workspaceId?: WorkspaceId) => void /** Toggle the sidebar column through the layout service. */ toggleSidebar: () => void - /** Select a global panel, or the conversation when null. */ - selectPanel: (id: MainPanelId | null) => void + /** Select the global panel addressed by a sidebar row. */ + selectPanel: (id: MainPanelId) => void /** Private reactive sources bound to framework selector hooks. */ hooks: { panels: ObservableSnapshot } } @@ -145,7 +137,6 @@ export type SidebarRootComponentProps = | 'sidebar.brand.mark' | 'sidebar.brand.name' | 'sidebar.panellist' - | 'sidebar.panellist.title' | 'sidebar.workspaces' | 'sidebar.settings' | 'sidebar.footer.action' diff --git a/packages/client/ui-sidebar/src/client/index.ts b/packages/client/ui-sidebar/src/client/index.ts index 6fc1bde880..38c384f697 100644 --- a/packages/client/ui-sidebar/src/client/index.ts +++ b/packages/client/ui-sidebar/src/client/index.ts @@ -15,7 +15,7 @@ import { en, zh, type SidebarKey } from './locales.ts' export type { SidebarBrandMarkOwnerProps, SidebarBrandNameOwnerProps, SidebarFooterActionOwnerProps, - SidebarPanelIconOwnerProps, SidebarPanelMetadata, SidebarPanelTitleOwnerProps, + SidebarPanelIconOwnerProps, SidebarPanelMetadata, SidebarRootComponentProps, SidebarRootInjected, SidebarSectionOwnerProps, SidebarSettingsOwnerProps, } from './contract/slots.ts' export type { SidebarKey } from './locales.ts' @@ -75,7 +75,6 @@ export function apply(ctx: ClientContext): void { 'sidebar.brand.mark': { kind: 'single', scope: 'root' }, 'sidebar.brand.name': { kind: 'single', scope: 'root' }, 'sidebar.panellist': { kind: 'list', scope: 'root' }, - 'sidebar.panellist.title': { kind: 'keyed', scope: 'root' }, 'sidebar.workspaces': { kind: 'single', scope: 'root' }, 'sidebar.settings': { kind: 'single', scope: 'root' }, 'sidebar.footer.action': { kind: 'list', scope: 'root' }, diff --git a/packages/client/ui-sidebar/tests/apply.client.spec.tsx b/packages/client/ui-sidebar/tests/apply.client.spec.tsx index 9afd13bc99..701de45e74 100644 --- a/packages/client/ui-sidebar/tests/apply.client.spec.tsx +++ b/packages/client/ui-sidebar/tests/apply.client.spec.tsx @@ -1,19 +1,34 @@ /** Sidebar shell slot registration and its Session/layout callbacks. */ -import { Context } from '@deepseek-ai/cordis' -import { describe, expect, it, vi } from 'vitest' +import { Context, type Fiber } from '@deepseek-ai/cordis' +import { afterEach, describe, expect, expectTypeOf, it, vi } from 'vitest' import { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client' import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots' import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client' import { apply, inject } from '@deepseek-ai/dsh-client-ui-sidebar/client' import type { SidebarRootInjected } from '@deepseek-ai/dsh-client-ui-sidebar/client' +import type { MainPanelId } from '@deepseek-ai/dsh-client-ui-layout/client' import { apply as hostApply } from '../src/index.ts' +const owners = new Set() +afterEach(async () => { + try { + for (const owner of owners) await owner.dispose() + } finally { + owners.clear() + } +}) + function SidebarFrame({ renderSlot }: PropsRenderSlots<'sidebar'>) { return renderSlot('sidebar', { collapsed: false, width: 300 }) } async function bench(declare = true) { - const ctx = new Context() + const root = new Context() + let ctx: Context | undefined + const owner = root.plugin((owned: Context) => { ctx = owned }) + owners.add(owner) + await owner.await() + if (ctx === undefined) throw new Error('the sidebar fixture owner did not activate') await ctx.plugin(SlotRegistry).await() const layout = { toggleSidebar: vi.fn(), selectPanel: vi.fn() } const uiWorkspace = { startSession: vi.fn() } @@ -52,7 +67,6 @@ describe('ui-sidebar apply', () => { expect(b.slots.spec('sidebar.settings')).toEqual({ kind: 'single', scope: 'root' }) expect(b.slots.spec('sidebar.footer.action')).toEqual({ kind: 'list', scope: 'root' }) expect(b.slots.spec('sidebar.panellist')).toEqual({ kind: 'list', scope: 'root' }) - expect(b.slots.spec('sidebar.panellist.title')).toEqual({ kind: 'keyed', scope: 'root' }) // Copy rides the standard locale seat, not the inject face. expect(b.slots.entries('sidebar')[0]!.locale).toBe('sidebar') const injected = (b.slots.entries('sidebar')[0]!.inject as () => SidebarRootInjected)() @@ -66,6 +80,10 @@ describe('ui-sidebar apply', () => { expect(b.uiWorkspace.startSession).toHaveBeenLastCalledWith(undefined) injected.toggleSidebar() expect(b.layout.toggleSidebar).toHaveBeenCalledOnce() + const panelId = 'custom-panel' as MainPanelId + injected.selectPanel(panelId) + expect(b.layout.selectPanel).toHaveBeenCalledExactlyOnceWith(panelId) + expectTypeOf[0]>().toEqualTypeOf() }) it('waits for the sidebar declaration before registering', async () => { diff --git a/packages/client/ui-sidebar/tests/panel-list.client.spec.tsx b/packages/client/ui-sidebar/tests/panel-list.client.spec.tsx new file mode 100644 index 0000000000..41a01d53ef --- /dev/null +++ b/packages/client/ui-sidebar/tests/panel-list.client.spec.tsx @@ -0,0 +1,276 @@ +// @vitest-environment jsdom +/** Global panel rows and DOM focus through the production slot renderer. */ +import type { Context } from '@deepseek-ai/cordis' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { act, cleanup, fireEvent, waitFor, within } from '@testing-library/react' +import { SlotTestRuntime } from '@deepseek-ai/dsh-client-test-runtime' +import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client' +import { en as commonEn } from '@deepseek-ai/dsh-client-locale/src/locales/en.ts' +import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts' +import { IconGlobeOutline14 } from '@deepseek-ai/dsh-client-ui-primitives' +import type { ILayout, MainPanelId } from '@deepseek-ai/dsh-client-ui-layout/client' +import type { PropsRenderSlots, PropsRuntime, SlotLabel } from '@deepseek-ai/dsh-client-ui-slots' +import { apply, inject } from '../src/client/index.ts' + +declare module '@deepseek-ai/dsh-client-ui-slots' { + interface LocaleNamespaceMap { + 'sidebar-panel-test': 'alpha' + } +} + +const ALPHA = 'sidebar-test-alpha' as MainPanelId +const BETA = 'sidebar-test-beta' as MainPanelId +const runtimes = new Set() + +afterEach(async () => { + try { + for (const runtime of runtimes) await runtime.dispose() + } finally { + runtimes.clear() + cleanup() + } +}) + +async function bench(collapsed = false) { + const runtime = await SlotTestRuntime.create() + runtimes.add(runtime) + const locale = new LocaleRuntime(runtime.ctx) + locale.setLocale('en') + const layout = { + beginNavigation: vi.fn(() => new AbortController().signal), + toggleSidebar: vi.fn(), + selectPanel: vi.fn((activePanelId: MainPanelId | null) => { runtime.panelInfo.set({ activePanelId }) }), + openRightbar: vi.fn(), + closeRightbar: vi.fn(), + } satisfies ILayout + await runtime.mount({ + inject: ['slots'], + apply(ctx: Context) { + ctx.provide('layout', layout) + ctx.provide('uiWorkspace', { startSession: vi.fn() } as never) + ctx.provide('locale', locale) + ctx.effect(() => locale.register('common', { zh: commonZh, en: commonEn }), 'panel test: common locale') + ctx.effect(() => locale.register('sidebar-panel-test', { + zh: { alpha: '甲面板' }, en: { alpha: 'Alpha panel' }, + }), 'panel test: panel locale') + ctx.slots.installLocale(locale) + ctx.slots.inject('main', () => ctx.slots.register({ name: 'main', key: 'conversation' }, () => ( +

    Conversation content

    + ))) + }, + }) + function Frame({ usePanelInfo, renderSlot }: PropsRuntime<'root'> & PropsRenderSlots<'sidebar' | 'main'>) { + const activePanelId = usePanelInfo(info => info.activePanelId) + return ( + <> + +
    {renderSlot('main', {}, { entryKey: activePanelId ?? 'conversation' })}
    + + ) + } + await runtime.root.declare({ + sidebar: { kind: 'single', scope: 'root' }, + main: { kind: 'keyed', scope: 'root' }, + }, Frame) + const sidebar = await runtime.mount({ inject: [...inject], apply }) + const view = runtime.renderRoot() + return { runtime, locale, layout, sidebar, view } +} + +interface TestPanel { + id: MainPanelId + heading: string + order?: number + label?: SlotLabel +} + +async function mountPanel(runtime: SlotTestRuntime, { heading, ...metadata }: TestPanel) { + function Icon({ size, active }: PropsRuntime<'sidebar.panellist'>) { + return ( + + + + ) + } + function Body() { + return ( +
    +

    {heading}

    + + +
    + ) + } + return runtime.mount({ + inject: ['slots'], + apply(ctx: Context) { + ctx.slots.inject('main', () => ctx.slots.register({ name: 'main', key: metadata.id }, Body)) + ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({ name: 'sidebar.panellist', ...metadata }, Icon)) + }, + }) +} + +async function mountPanels(runtime: SlotTestRuntime, locale: LocaleRuntime, betaOrder = 10) { + const t = locale.bind('sidebar-panel-test') + const alpha = await mountPanel(runtime, { id: ALPHA, heading: 'Alpha content', label: () => t('alpha'), order: 20 }) + const beta = await mountPanel(runtime, { id: BETA, heading: 'Beta content', label: 'Beta panel', order: betaOrder }) + return { alpha, beta } +} + +describe('sidebar global panels', () => { + it('adds late registrations, removes each plugin contribution, and leaves no empty panel-list DOM', async () => { + const { runtime, locale, view } = await bench() + expect(runtime.slots.entries('sidebar.panellist')).toEqual([]) + expect(view.queryByRole('navigation')).toBeNull() + expect(view.container.querySelector('[data-slot="sidebar.panellist"]')).toBeNull() + expect(view.getByText('Conversation content')).toBeTruthy() + + const { alpha, beta } = await mountPanels(runtime, locale) + const navigation = await view.findByRole('navigation', { name: 'Global panels' }) + expect(within(navigation).getAllByRole('button')).toHaveLength(2) + fireEvent.click(within(navigation).getByRole('button', { name: 'Alpha panel' })) + expect(view.getByRole('heading', { name: 'Alpha content' })).toBeTruthy() + + await alpha.dispose() + await waitFor(() => { + expect(view.queryByRole('button', { name: 'Alpha panel' })).toBeNull() + expect(view.queryByRole('heading', { name: 'Alpha content' })).toBeNull() + }) + expect(runtime.slots.entries('main').map(entry => entry.options.key)).toEqual(['conversation', BETA]) + fireEvent.click(within(navigation).getByRole('button', { name: 'Beta panel' })) + expect(view.getByRole('heading', { name: 'Beta content' })).toBeTruthy() + + await beta.dispose() + await waitFor(() => { expect(view.queryByRole('navigation')).toBeNull() }) + expect(view.queryByRole('heading', { name: 'Beta content' })).toBeNull() + expect(view.container.querySelector('[data-slot="sidebar.panellist"]')).toBeNull() + expect(runtime.slots.entries('sidebar.panellist')).toEqual([]) + expect(runtime.slots.entries('main').map(entry => entry.options.key)).toEqual(['conversation']) + }) + + it.each([false, true])('renders ordered rows, icon sizes, and tooltip labels with collapsed=%s', async (collapsed) => { + const { runtime, locale, view } = await bench(collapsed) + await mountPanels(runtime, locale) + const navigation = await view.findByRole('navigation', { name: 'Global panels' }) + const rows = within(navigation).getAllByRole('button') + expect(rows.map(row => row.getAttribute('aria-label'))).toEqual(['Beta panel', 'Alpha panel']) + for (const [id, label] of [[BETA, 'Beta panel'], [ALPHA, 'Alpha panel']] as const) { + const row = within(navigation).getByRole('button', { name: label }) + expect(row.getAttribute('aria-current')).toBeNull() + expect(row.textContent).toBe(collapsed ? '' : label) + expect(row.querySelectorAll('svg')).toHaveLength(1) + const icon = within(row).getByTestId(`${id}-icon`) + expect(icon.getAttribute('data-active')).toBe('false') + expect(icon.querySelector('svg')?.getAttribute('width')).toBe(collapsed ? '18' : '16') + expect(icon.querySelector('svg')?.getAttribute('height')).toBe(collapsed ? '18' : '16') + act(() => { row.focus() }) + expect(document.activeElement).toBe(row) + if (collapsed) expect(view.getByRole('tooltip').textContent).toBe(label) + else expect(view.queryByRole('tooltip')).toBeNull() + act(() => { row.blur() }) + expect(document.activeElement).not.toBe(row) + expect(view.queryByRole('tooltip')).toBeNull() + } + }) + + it('keeps registration order when panel rows have equal order', async () => { + const { runtime, locale, view } = await bench() + await mountPanels(runtime, locale, 20) + const navigation = await view.findByRole('navigation', { name: 'Global panels' }) + expect(within(navigation).getAllByRole('button').map(row => row.textContent)).toEqual(['Alpha panel', 'Beta panel']) + }) + + it('selects registered main content and keeps a repeated selection active', async () => { + const { runtime, locale, layout, view } = await bench() + await mountPanels(runtime, locale) + const navigation = await view.findByRole('navigation', { name: 'Global panels' }) + const alpha = within(navigation).getByRole('button', { name: 'Alpha panel' }) + const beta = within(navigation).getByRole('button', { name: 'Beta panel' }) + fireEvent.click(alpha) + expect(layout.selectPanel).toHaveBeenLastCalledWith(ALPHA) + expect(runtime.panelInfo.getSnapshot()).toEqual({ activePanelId: ALPHA }) + expect(alpha.getAttribute('aria-current')).toBe('page') + expect(beta.getAttribute('aria-current')).toBeNull() + expect(within(alpha).getByTestId(`${ALPHA}-icon`).getAttribute('data-active')).toBe('true') + expect(view.getByRole('heading', { name: 'Alpha content' })).toBeTruthy() + expect(view.queryByRole('heading', { name: 'Beta content' })).toBeNull() + + fireEvent.click(alpha) + expect(layout.selectPanel).toHaveBeenLastCalledWith(ALPHA) + expect(alpha.getAttribute('aria-current')).toBe('page') + expect(view.getByRole('heading', { name: 'Alpha content' })).toBeTruthy() + + fireEvent.click(beta) + expect(layout.selectPanel).toHaveBeenLastCalledWith(BETA) + expect(runtime.panelInfo.getSnapshot()).toEqual({ activePanelId: BETA }) + expect(alpha.getAttribute('aria-current')).toBeNull() + expect(beta.getAttribute('aria-current')).toBe('page') + expect(within(alpha).getByTestId(`${ALPHA}-icon`).getAttribute('data-active')).toBe('false') + expect(within(beta).getByTestId(`${BETA}-icon`).getAttribute('data-active')).toBe('true') + expect(view.queryByRole('heading', { name: 'Alpha content' })).toBeNull() + expect(view.getByRole('heading', { name: 'Beta content' })).toBeTruthy() + }) + + it('refreshes locale labels without re-registering icons or changing plain labels', async () => { + const { runtime, locale, view } = await bench() + await mountPanels(runtime, locale) + const navigation = await view.findByRole('navigation', { name: 'Global panels' }) + const entries = runtime.slots.entries('sidebar.panellist') + act(() => { locale.setLocale('zh') }) + await waitFor(() => { + expect(within(navigation).getByRole('button', { name: '甲面板' }).textContent).toBe('甲面板') + }) + expect(within(navigation).getByRole('button', { name: 'Beta panel' }).textContent).toBe('Beta panel') + expect(runtime.slots.entries('sidebar.panellist')).toBe(entries) + }) + + it('uses an omitted label and order as the panel id and order zero', async () => { + const { runtime, view } = await bench() + await mountPanel(runtime, { id: ALPHA, heading: 'Alpha content', label: 'Alpha panel', order: 20 }) + await mountPanel(runtime, { id: BETA, heading: 'Beta content' }) + const navigation = await view.findByRole('navigation', { name: 'Global panels' }) + expect(within(navigation).getAllByRole('button').map(row => row.textContent)).toEqual([BETA, 'Alpha panel']) + fireEvent.click(within(navigation).getByRole('button', { name: BETA })) + expect(view.getByRole('heading', { name: 'Beta content' })).toBeTruthy() + }) + + it.each([false, true])('keeps main selection while DOM focus moves between controls with collapsed=%s', async (collapsed) => { + const { runtime, locale, layout, view } = await bench(collapsed) + await mountPanels(runtime, locale) + const navigation = await view.findByRole('navigation', { name: 'Global panels' }) + const alpha = within(navigation).getByRole('button', { name: 'Alpha panel' }) + const beta = within(navigation).getByRole('button', { name: 'Beta panel' }) + act(() => { alpha.focus() }) + expect(document.activeElement).toBe(alpha) + expect(runtime.panelInfo.getSnapshot()).toEqual({ activePanelId: null }) + expect(alpha.getAttribute('aria-current')).toBeNull() + expect(layout.selectPanel).not.toHaveBeenCalled() + fireEvent.click(alpha) + const selected = runtime.panelInfo.getSnapshot() + const textbox = view.getByRole('textbox', { name: 'Alpha content input' }) + const action = view.getByRole('button', { name: 'Alpha content action' }) + for (const target of [textbox, action, beta, alpha]) { + act(() => { target.focus() }) + expect(document.activeElement).toBe(target) + expect(runtime.panelInfo.getSnapshot()).toBe(selected) + expect(alpha.getAttribute('aria-current')).toBe('page') + expect(beta.getAttribute('aria-current')).toBeNull() + expect(view.getByRole('heading', { name: 'Alpha content' })).toBeTruthy() + expect(view.queryByRole('heading', { name: 'Beta content' })).toBeNull() + } + expect(layout.selectPanel).toHaveBeenCalledExactlyOnceWith(ALPHA) + }) + + it('disposes the sidebar while retaining independently owned main panel bodies', async () => { + const { runtime, locale, sidebar, view } = await bench() + await mountPanels(runtime, locale) + fireEvent.click(await view.findByRole('button', { name: 'Alpha panel' })) + await sidebar.dispose() + expect(view.queryByRole('navigation')).toBeNull() + expect(runtime.slots.spec('sidebar.panellist')).toBeUndefined() + expect(runtime.slots.entries('sidebar.panellist')).toEqual([]) + expect(view.getByRole('heading', { name: 'Alpha content' })).toBeTruthy() + act(() => { locale.setLocale('zh') }) + expect(view.queryByRole('navigation')).toBeNull() + }) +}) diff --git a/packages/client/ui-workspace/tests/workspace-browser.client.spec.tsx b/packages/client/ui-workspace/tests/workspace-browser.client.spec.tsx index 1986a568ba..b25b32e166 100644 --- a/packages/client/ui-workspace/tests/workspace-browser.client.spec.tsx +++ b/packages/client/ui-workspace/tests/workspace-browser.client.spec.tsx @@ -9,6 +9,7 @@ import type { } from '@deepseek-ai/dsh-api-workspace-controller/client' import type { SessionPendingInteractionSnapshot } from '@deepseek-ai/dsh-client-ui-session/client' import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type { MainPanelId } from '@deepseek-ai/dsh-client-ui-layout/client' import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts' import type { WorkspaceBrowserProps } from '../src/client/contract/slots.ts' import { createWorkspaceViewStore, FLAT_SESSION_ORDER_KEY } from '../src/client/stores.ts' @@ -112,6 +113,30 @@ function rerender(b: ReturnType, overrides: Partial { + it('moves focus into Workspace controls without selecting a Session while a main panel is active', () => { + const panelInfo = { activePanelId: 'panel-a' as MainPanelId } + const b = mount({ + usePanelInfo: hook(panelInfo), + useSessions: hook(sessionState([summary('current', 1)], { current: sid('current') })), + useWorkspaces: hook(workspaceState([workspace('alpha', ['current'])])), + }) + fireEvent.click(screen.getByText('alpha')) + const current = screen.getByText('current').closest('[role="treeitem"]') + expect(current?.getAttribute('aria-selected')).toBe('false') + fireEvent.click(screen.getByRole('button', { name: '搜索会话' })) + const input = screen.getByPlaceholderText('搜索会话…') + expect(document.activeElement).toBe(input) + fireEvent.click(screen.getByRole('button', { name: '清除搜索' })) + const add = screen.getByRole('button', { name: '添加工作区' }) + add.focus() + fireEvent.click(add) + expect(document.activeElement).toBe(add) + expect(screen.getByTestId('directory-flow')).toBeTruthy() + expect(panelInfo.activePanelId).toBe('panel-a') + expect(b.props.open).not.toHaveBeenCalled() + expect(b.props.startSession).not.toHaveBeenCalled() + }) + it('workspace hover card shows a POSIX home descendant as ~', () => { vi.useFakeTimers() try { From 8715fd2a870b161cf0f6cd04350330ee00b46cd7 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:36:39 +0800 Subject: [PATCH 41/58] fix(client): preserve the latest panel and session navigation --- .../tests/apply-inject.client.spec.tsx | 6 +- .../ui-chat/tests/chat-apply.client.spec.tsx | 6 +- .../ui-conversation/src/client/apply.ts | 11 +- .../tests/apply-inject.client.spec.tsx | 6 +- .../tests/apply-wiring.client.spec.tsx | 5 +- .../tests/assembly-surfaces.client.spec.tsx | 21 +++- packages/client/ui-layout/src/client/index.ts | 4 +- .../client/ui-layout/src/client/service.ts | 34 ++++- .../ui-layout/tests/apply.client.spec.ts | 5 + .../ui-layout/tests/service.client.spec.ts | 41 ++++++- .../tests/assembly-surfaces.client.spec.tsx | 6 +- .../tests/toolview-slot.client.spec.tsx | 11 +- .../client/ui-workspace/src/client/index.ts | 3 +- .../ui-workspace/src/client/navigation.ts | 40 ++++-- .../ui-workspace/tests/apply.client.spec.ts | 2 +- .../tests/workspaces-service.client.spec.ts | 116 +++++++++++++++++- 16 files changed, 279 insertions(+), 38 deletions(-) diff --git a/packages/client/ui-chat/tests/apply-inject.client.spec.tsx b/packages/client/ui-chat/tests/apply-inject.client.spec.tsx index 91c78811e0..947eb739ed 100644 --- a/packages/client/ui-chat/tests/apply-inject.client.spec.tsx +++ b/packages/client/ui-chat/tests/apply-inject.client.spec.tsx @@ -16,6 +16,7 @@ import { apply as applyChat, inject as injectChat, type ChatViewInjected, } from '@deepseek-ai/dsh-client-ui-chat/client' import { SessionSeq, type SessionId } from '@deepseek-ai/dsh-session/types' +import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types' import { createChatStore } from '../src/client/stores.ts' usePinnedBrowserLanguages('zh-CN') @@ -57,7 +58,10 @@ async function bench() { ) new TestRemote(runtime.ctx, { session: { openWorkspacePath } }) runtime.ctx.provide('uiWorkspace', { - connectWorkspace: vi.fn(async () => ROOT), + openWorkspace: vi.fn(async (_workspaceId: WorkspaceId, beforeOpen: (id: SessionId) => void) => { + beforeOpen(ROOT) + runtime.sessions.open(ROOT) + }), openSession: (id: SessionId) => { runtime.sessions.open(id) }, } as never) const session = sessionFakeFor() diff --git a/packages/client/ui-chat/tests/chat-apply.client.spec.tsx b/packages/client/ui-chat/tests/chat-apply.client.spec.tsx index 6c1ab9c6a0..ac364e4cb7 100644 --- a/packages/client/ui-chat/tests/chat-apply.client.spec.tsx +++ b/packages/client/ui-chat/tests/chat-apply.client.spec.tsx @@ -9,6 +9,7 @@ import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots' import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store' import type { SessionBinding } from '@deepseek-ai/dsh-api-session-controller/client' import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types' import { apply as applyConversation, inject as injectConversation, } from '@deepseek-ai/dsh-client-ui-conversation/client' @@ -44,7 +45,10 @@ async function bench() { runtime.ctx.provide('layout', { openRightbar: vi.fn(), closeRightbar: vi.fn() } as never) runtime.ctx.provide('sidebarRight', { openResource: vi.fn() } as never) runtime.ctx.provide('uiWorkspace', { - connectWorkspace: vi.fn(async () => SID), + openWorkspace: vi.fn(async (_workspaceId: WorkspaceId, beforeOpen: (id: SessionId) => void) => { + beforeOpen(SID) + runtime.sessions.open(SID) + }), openSession: (id: SessionId) => { runtime.sessions.open(id) }, } as never) new TestRemote(runtime.ctx, { diff --git a/packages/client/ui-conversation/src/client/apply.ts b/packages/client/ui-conversation/src/client/apply.ts index 7827186ede..7eba82cfc0 100644 --- a/packages/client/ui-conversation/src/client/apply.ts +++ b/packages/client/ui-conversation/src/client/apply.ts @@ -86,9 +86,10 @@ const ABSENT_FILE_UPLOADS = { interface WorkspaceNavigation { openSession(sessionId: SessionId): void - connectWorkspace( + openWorkspace( workspaceId: Parameters[0], - ): Promise + beforeOpen: (sessionId: SessionId) => void, + ): Promise } /** Resolve the session-scoped Conversation action face, failing loud. */ @@ -232,8 +233,7 @@ export function apply(ctx: Context, config: Config = Config({})): void { hooks: { composerBlock: sessionId === undefined ? ABSENT_BLOCK : composerBlocks.storeFor(sessionId), }, - selectWorkspace: async (workspaceId) => { - const nextId = await workspaceNavigation.connectWorkspace(workspaceId) + selectWorkspace: workspaceId => workspaceNavigation.openWorkspace(workspaceId, (nextId) => { if (sessionId !== undefined && nextId !== sessionId) { const from = inputHub.shell(sessionId) const draft = from.snapshot.draft @@ -253,8 +253,7 @@ export function apply(ctx: Context, config: Config = Config({})): void { } } } - workspaceNavigation.openSession(nextId) - }, + }), }), }, ConversationRoot) diff --git a/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx b/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx index aa50a19ba2..c6d6226bfe 100644 --- a/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx +++ b/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx @@ -50,7 +50,11 @@ async function bench() { runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) const connectWorkspace = vi.fn(async () => ROOT) runtime.ctx.provide('uiWorkspace', { - connectWorkspace, + openWorkspace: async (_workspaceId: WorkspaceId, beforeOpen: (id: SessionId) => void) => { + const id = await connectWorkspace() + beforeOpen(id) + runtime.sessions.open(id) + }, openSession: (id: SessionId) => { runtime.sessions.open(id) }, } as never) const sessionFake = sessionFakeFor() diff --git a/packages/client/ui-conversation/tests/apply-wiring.client.spec.tsx b/packages/client/ui-conversation/tests/apply-wiring.client.spec.tsx index a71103fb32..3c59deb1c2 100644 --- a/packages/client/ui-conversation/tests/apply-wiring.client.spec.tsx +++ b/packages/client/ui-conversation/tests/apply-wiring.client.spec.tsx @@ -15,7 +15,10 @@ const SID = 'session-1' as SessionId async function bench(options: { declareConversation?: boolean } = {}) { const runtime = await SlotTestRuntime.create() runtime.ctx.provide('uiWorkspace', { - connectWorkspace: vi.fn(async () => SID), + openWorkspace: vi.fn(async (_workspaceId: unknown, beforeOpen: (id: SessionId) => void) => { + beforeOpen(SID) + runtime.sessions.open(SID) + }), openSession: (id: SessionId) => { runtime.sessions.open(id) }, } as never) runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) diff --git a/packages/client/ui-conversation/tests/assembly-surfaces.client.spec.tsx b/packages/client/ui-conversation/tests/assembly-surfaces.client.spec.tsx index fc4eccb528..573a732780 100644 --- a/packages/client/ui-conversation/tests/assembly-surfaces.client.spec.tsx +++ b/packages/client/ui-conversation/tests/assembly-surfaces.client.spec.tsx @@ -12,6 +12,7 @@ import { import { InputHub } from '../src/client/input/hub.ts' import { apply, inject, type EmptyWorkspaceOwnerProps } from '@deepseek-ai/dsh-client-ui-conversation/client' import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types' // jsdom implements no Range geometry (Lexical's scroll-into-view measures the // caret with one once the surface is genuinely contenteditable). @@ -61,7 +62,10 @@ function WorkspaceProbe({ open }: EmptyWorkspaceOwnerProps) { async function bench(opts?: { blank?: boolean }) { const runtime = await SlotTestRuntime.create() runtime.ctx.provide('uiWorkspace', { - connectWorkspace: vi.fn(async () => SID), + openWorkspace: vi.fn(async (_workspaceId: WorkspaceId, beforeOpen: (id: SessionId) => void) => { + beforeOpen(SID) + runtime.sessions.open(SID) + }), openSession: (id: SessionId) => { runtime.sessions.open(id) }, } as never) runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) @@ -86,7 +90,10 @@ describe('resident composer', () => { it('renders the locked view state while no session exists at all', async () => { const runtime = await SlotTestRuntime.create() runtime.ctx.provide('uiWorkspace', { - connectWorkspace: vi.fn(async () => SID), + openWorkspace: vi.fn(async (_workspaceId: WorkspaceId, beforeOpen: (id: SessionId) => void) => { + beforeOpen(SID) + runtime.sessions.open(SID) + }), openSession: (id: SessionId) => { runtime.sessions.open(id) }, } as never) runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) @@ -116,7 +123,10 @@ describe('resident composer', () => { it('keeps the complete Hero tree mounted when the first Workspace session appears', async () => { const runtime = await SlotTestRuntime.create() runtime.ctx.provide('uiWorkspace', { - connectWorkspace: vi.fn(async () => SID), + openWorkspace: vi.fn(async (_workspaceId: WorkspaceId, beforeOpen: (id: SessionId) => void) => { + beforeOpen(SID) + runtime.sessions.open(SID) + }), openSession: (id: SessionId) => { runtime.sessions.open(id) }, } as never) runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) @@ -184,7 +194,10 @@ describe('prompt rejection through the assembled composer', () => { it('renders the promptError alert strip and keeps the draft in the machine', async () => { const runtime = await SlotTestRuntime.create() runtime.ctx.provide('uiWorkspace', { - connectWorkspace: vi.fn(async () => SID), + openWorkspace: vi.fn(async (_workspaceId: WorkspaceId, beforeOpen: (id: SessionId) => void) => { + beforeOpen(SID) + runtime.sessions.open(SID) + }), openSession: (id: SessionId) => { runtime.sessions.open(id) }, } as never) runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) diff --git a/packages/client/ui-layout/src/client/index.ts b/packages/client/ui-layout/src/client/index.ts index 83439c9d8e..ffbbd68bb9 100644 --- a/packages/client/ui-layout/src/client/index.ts +++ b/packages/client/ui-layout/src/client/index.ts @@ -133,7 +133,8 @@ export function apply(ctx: ClientContext): void { const handle = createLayoutStore() const instance = handle.create() const store: typeof handle = { ...handle, create: () => instance } - const layout = new LayoutController(instance.actions) + const layout = new LayoutController(instance.actions, id => + ctx.slots.entries('main').some(entry => entry.options.key === id)) const retainMainPanels = (): void => { instance.actions.retainMainPanels(ctx.slots.entries('main').flatMap(entry => entry.options.key === undefined ? [] : [entry.options.key])) @@ -158,6 +159,7 @@ export function apply(ctx: ClientContext): void { const disposePanels = ctx.slots.subscribe('main', retainMainPanels) retainMainPanels() return () => { + layout.dispose() disposePanels() disposeRegistration() disposePanelInfo() diff --git a/packages/client/ui-layout/src/client/service.ts b/packages/client/ui-layout/src/client/service.ts index 7835f3aa20..14a3924d63 100644 --- a/packages/client/ui-layout/src/client/service.ts +++ b/packages/client/ui-layout/src/client/service.ts @@ -29,8 +29,14 @@ export interface ILayout { /** * Select a global central panel without changing the current Session. * @param panelId - registered main key, or null to show the Conversation. + * @throws if the selected main key is not registered; preserves the current selection. */ selectPanel(panelId: MainPanelId | null): void + /** + * Start an asynchronous navigation, superseding any earlier pending navigation. + * @returns a signal aborted by the next navigation or layout disposal; check it before committing UI state. + */ + beginNavigation(): AbortSignal /** Toggle the sidebar panel (closed ⟷ contract default width). */ toggleSidebar(): void /** @@ -47,14 +53,38 @@ export interface ILayout { /** Cross-plugin panel-action face (ctx.layout). */ export class LayoutController implements ILayout { - /** @param panels - actions of the instance shared with the root entry. */ - constructor(private readonly panels: PanelActions) {} + private navigation = new AbortController() + + /** + * @param panels - actions of the instance shared with the root entry. + * @param hasMainPanel - checks the live main-slot registry for a panel id. + */ + constructor( + private readonly panels: PanelActions, + private readonly hasMainPanel: (id: MainPanelId) => boolean, + ) {} /** Select a global panel or return to the Conversation. */ selectPanel(panelId: MainPanelId | null): void { + if (panelId !== null && !this.hasMainPanel(panelId)) { + throw new Error(`layout.selectPanel: main panel "${panelId}" is not registered`) + } + this.navigation.abort() this.panels.selectPanel(panelId) } + /** @returns the new pending navigation's cancellation signal. */ + beginNavigation(): AbortSignal { + this.navigation.abort() + this.navigation = new AbortController() + return this.navigation.signal + } + + /** Invalidate pending navigations when the layout owner is unloaded. */ + dispose(): void { + this.navigation.abort() + } + /** Toggle the sidebar panel (closed ⟷ contract default width). */ toggleSidebar(): void { this.panels.toggleSidebar() diff --git a/packages/client/ui-layout/tests/apply.client.spec.ts b/packages/client/ui-layout/tests/apply.client.spec.ts index 94124c2805..74c58022c9 100644 --- a/packages/client/ui-layout/tests/apply.client.spec.ts +++ b/packages/client/ui-layout/tests/apply.client.spec.ts @@ -101,6 +101,7 @@ describe('ui-layout client apply', () => { const instance = handle.create() expect(handle.create()).toBe(instance) const layout = ctx.get('layout') as LayoutController + expect(() => { layout.selectPanel('missing' as MainPanelId) }).toThrow('main panel "missing" is not registered') layout.toggleSidebar() expect(instance.getSnapshot().layoutInfo.sidebar).toBe(0) const host = rendererHost() @@ -113,7 +114,11 @@ describe('ui-layout client apply', () => { expect(panelInfo.getSnapshot()).toEqual({ activePanelId: panelId }) disposePanel() await vi.waitFor(() => { expect(panelInfo.getSnapshot()).toEqual({ activePanelId: null }) }) + expect(() => { layout.selectPanel(panelId) }).toThrow('main panel "panel-a" is not registered') expect(instance.getSnapshot().layoutInfo.sidebar).toBe(0) + const pending = layout.beginNavigation() + await fiber.dispose() + expect(pending.aborted).toBe(true) }) it('theme presenter applies the initial snapshot, follows theme/change, and unwinds on dispose', async () => { diff --git a/packages/client/ui-layout/tests/service.client.spec.ts b/packages/client/ui-layout/tests/service.client.spec.ts index 14c3fb479e..7ccadb5c3c 100644 --- a/packages/client/ui-layout/tests/service.client.spec.ts +++ b/packages/client/ui-layout/tests/service.client.spec.ts @@ -18,7 +18,7 @@ function fakePanels(): PanelActions { describe('LayoutController', () => { it('forwards right column transitions to the constructor-supplied actions', () => { const panels = fakePanels() - const service = new LayoutController(panels) + const service = new LayoutController(panels, () => true) service.openRightbar(true, false) service.openRightbar(true, true) @@ -35,7 +35,7 @@ describe('LayoutController', () => { it('can toggle the sidebar immediately after construction', () => { const panels = fakePanels() - const service = new LayoutController(panels) + const service = new LayoutController(panels, () => true) service.toggleSidebar() @@ -45,7 +45,7 @@ describe('LayoutController', () => { it('forwards panel selection and returning to the Conversation without changing geometry', () => { const panels = fakePanels() - const service = new LayoutController(panels) + const service = new LayoutController(panels, () => true) const panelId = 'panel-a' as MainPanelId service.selectPanel(panelId) service.selectPanel(panelId) @@ -61,8 +61,8 @@ describe('LayoutController', () => { it('keeps separately constructed controllers bound to their own instances', () => { const first = fakePanels() const second = fakePanels() - const firstService = new LayoutController(first) - const secondService = new LayoutController(second) + const firstService = new LayoutController(first, () => true) + const secondService = new LayoutController(second, () => true) firstService.toggleSidebar() expect(first.toggleSidebar).toHaveBeenCalledTimes(1) expect(second.toggleSidebar).not.toHaveBeenCalled() @@ -70,4 +70,35 @@ describe('LayoutController', () => { expect(first.closeRightbar).not.toHaveBeenCalled() expect(second.closeRightbar).toHaveBeenCalledTimes(1) }) + + it('rejects an absent main entry without changing selection or cancelling pending navigation', () => { + const panels = fakePanels() + const present = new Set(['panel-a']) + const service = new LayoutController(panels, id => present.has(id)) + service.selectPanel('panel-a' as MainPanelId) + const navigation = service.beginNavigation() + present.delete('panel-a') + expect(() => { service.selectPanel('panel-a' as MainPanelId) }).toThrow('main panel "panel-a" is not registered') + expect(panels.selectPanel).toHaveBeenCalledOnce() + expect(navigation.aborted).toBe(false) + service.selectPanel(null) + expect(navigation.aborted).toBe(true) + }) + + it('supersedes asynchronous navigation on another request, any valid selection, and disposal', () => { + const service = new LayoutController(fakePanels(), () => true) + const first = service.beginNavigation() + const second = service.beginNavigation() + expect(first.aborted).toBe(true) + expect(second.aborted).toBe(false) + service.selectPanel('panel-a' as MainPanelId) + expect(second.aborted).toBe(true) + const repeated = service.beginNavigation() + service.selectPanel('panel-a' as MainPanelId) + expect(repeated.aborted).toBe(true) + const pending = service.beginNavigation() + service.dispose() + expect(pending.aborted).toBe(true) + service.dispose() + }) }) diff --git a/packages/client/ui-tool/tests/assembly-surfaces.client.spec.tsx b/packages/client/ui-tool/tests/assembly-surfaces.client.spec.tsx index 99ee175099..7959b9c60a 100644 --- a/packages/client/ui-tool/tests/assembly-surfaces.client.spec.tsx +++ b/packages/client/ui-tool/tests/assembly-surfaces.client.spec.tsx @@ -6,6 +6,7 @@ import { cleanup, fireEvent, waitFor } from '@testing-library/react' import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client' import type { ISession } from '@deepseek-ai/dsh-api-session-controller/client' import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types' import type { TodoItem } from '@deepseek-ai/dsh-client-ui-conversation/client' import { apply as applyChat, inject as injectChat, type ToolResultNode, @@ -79,7 +80,10 @@ async function bench(nodes: ToolResultNode[]) { runtime.ctx.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() }) runtime.ctx.provide('sidebarRight', { openResource: vi.fn() } as never) runtime.ctx.provide('uiWorkspace', { - connectWorkspace: vi.fn(async () => SID), + openWorkspace: vi.fn(async (_workspaceId: WorkspaceId, beforeOpen: (id: SessionId) => void) => { + beforeOpen(SID) + runtime.sessions.open(SID) + }), openSession: (id: SessionId) => { runtime.sessions.open(id) }, } as never) const locale = new LocaleRuntime(runtime.ctx) diff --git a/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx b/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx index 2f72a5d2b7..67417ba4a3 100644 --- a/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx +++ b/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx @@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { cleanup } from '@testing-library/react' import type { ISession } from '@deepseek-ai/dsh-api-session-controller/client' import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types' import { apply as applyChat, inject as injectChat, type ToolResultNode, } from '@deepseek-ai/dsh-client-ui-chat/client' @@ -66,7 +67,10 @@ async function bench(nodes: ToolResultNode[]) { const sidebarRight = { openResource: vi.fn<(address: string) => void>() } runtime.ctx.provide('sidebarRight', sidebarRight as never) runtime.ctx.provide('uiWorkspace', { - connectWorkspace: vi.fn(async () => SID), + openWorkspace: vi.fn(async (_workspaceId: WorkspaceId, beforeOpen: (id: SessionId) => void) => { + beforeOpen(SID) + runtime.sessions.open(SID) + }), openSession: (id: SessionId) => { runtime.sessions.open(id) }, } as never) const locale = new LocaleRuntime(runtime.ctx) @@ -213,7 +217,10 @@ describe('registrant declaration injection', () => { runtime.ctx.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() }) runtime.ctx.provide('sidebarRight', { openResource: vi.fn() } as never) runtime.ctx.provide('uiWorkspace', { - connectWorkspace: vi.fn(async () => SID), + openWorkspace: vi.fn(async (_workspaceId: WorkspaceId, beforeOpen: (id: SessionId) => void) => { + beforeOpen(SID) + runtime.sessions.open(SID) + }), openSession: (id: SessionId) => { runtime.sessions.open(id) }, } as never) const locale = new LocaleRuntime(runtime.ctx) diff --git a/packages/client/ui-workspace/src/client/index.ts b/packages/client/ui-workspace/src/client/index.ts index 0633e96b85..9204fe7774 100644 --- a/packages/client/ui-workspace/src/client/index.ts +++ b/packages/client/ui-workspace/src/client/index.ts @@ -115,8 +115,7 @@ export function apply(ctx: Context): void { if (!result.ok) throw new Error(result.error.message) }, forkSession: (sessionId) => { - sessions.fork({ sessionId, increaseTitle: true }) - .then(openSession) + uiWorkspace.forkSession(sessionId) .catch(() => { // Fork or child-rename failure keeps the current selection. }) diff --git a/packages/client/ui-workspace/src/client/navigation.ts b/packages/client/ui-workspace/src/client/navigation.ts index c9dcc5e984..6d86beb826 100644 --- a/packages/client/ui-workspace/src/client/navigation.ts +++ b/packages/client/ui-workspace/src/client/navigation.ts @@ -19,6 +19,19 @@ export interface UiWorkspace { * @param sessionId - listed or retained Session to display. */ openSession(sessionId: SessionId): void + /** + * Connect a Workspace and open its Session unless a later navigation supersedes it. + * @param workspaceId - target Workspace. + * @param beforeOpen - optional synchronous preparation for the selected Session, skipped after supersession. + * @returns completion; a superseded request may create a Session but does not open it. + */ + openWorkspace(workspaceId: WorkspaceId, beforeOpen?: (sessionId: SessionId) => void): Promise + /** + * Fork a Session and open the child unless a later navigation supersedes it. + * @param sessionId - source Session. + * @returns completion; a superseded request leaves its child available without selecting it. + */ + forkSession(sessionId: SessionId): Promise /** * Resolve the reusable or newly created blank Session for a Workspace. * @param workspaceId - target Workspace. @@ -76,6 +89,7 @@ export class DirectoryBrowseError extends Error { /** Implements Workspace archive and directory UI operations. */ class UiWorkspaceService extends Service implements UiWorkspace { private readonly connecting = new Map>() + private readonly lifetime = new AbortController() /** * @param ctx - Client root Context. @@ -122,6 +136,20 @@ class UiWorkspaceService extends Service implements UiWorkspace { this.ctx.layout.selectPanel(null) } + async openWorkspace(workspaceId: WorkspaceId, beforeOpen?: (sessionId: SessionId) => void): Promise { + const navigation = AbortSignal.any([this.ctx.layout.beginNavigation(), this.lifetime.signal]) + const sessionId = await this.connectWorkspace(workspaceId) + if (navigation.aborted) return + beforeOpen?.(sessionId) + if (!navigation.aborted) this.openSession(sessionId) + } + + async forkSession(sessionId: SessionId): Promise { + const navigation = AbortSignal.any([this.ctx.layout.beginNavigation(), this.lifetime.signal]) + const childId = await this.sessions.fork({ sessionId, increaseTitle: true }) + if (!navigation.aborted) this.openSession(childId) + } + startSession(workspaceId?: WorkspaceId): void { const workspace = this.workspaces.list.getSnapshot() const sessions = this.sessions.list.getSnapshot() @@ -138,8 +166,7 @@ class UiWorkspaceService extends Service implements UiWorkspace { this.ctx.layout.selectPanel(null) return } - void this.connectWorkspace(target).then( - (sessionId) => { this.openSession(sessionId) }, + void this.openWorkspace(target).catch( (reason: unknown) => { console.warn('new session failed:', reason) }, ) } @@ -168,9 +195,8 @@ class UiWorkspaceService extends Service implements UiWorkspace { private watchNavigation(): () => void { let initial: 'waiting' | 'connecting' | 'done' = 'waiting' - let disposed = false const reconcile = (): void => { - if (disposed) return + if (this.lifetime.signal.aborted) return if (this.clearArchivedCurrent()) return if (initial !== 'waiting') return const workspace = this.workspaces.list.getSnapshot() @@ -188,14 +214,14 @@ class UiWorkspaceService extends Service implements UiWorkspace { initial = 'connecting' void this.connectWorkspace(target).then( (sessionId) => { - if (disposed) return + if (this.lifetime.signal.aborted) return if (this.sessions.list.getSnapshot().current === undefined) { this.sessions.open(sessionId) } initial = 'done' }, (reason: unknown) => { - if (disposed) return + if (this.lifetime.signal.aborted) return initial = 'waiting' console.warn('initial workspace selection failed:', reason) }, @@ -205,7 +231,7 @@ class UiWorkspaceService extends Service implements UiWorkspace { const disposeSessions = this.sessions.list.subscribe(reconcile) reconcile() return () => { - disposed = true + this.lifetime.abort() disposeSessions() disposeWorkspaces() } diff --git a/packages/client/ui-workspace/tests/apply.client.spec.ts b/packages/client/ui-workspace/tests/apply.client.spec.ts index d35e29c81a..8035b099a1 100644 --- a/packages/client/ui-workspace/tests/apply.client.spec.ts +++ b/packages/client/ui-workspace/tests/apply.client.spec.ts @@ -22,7 +22,7 @@ async function bench() { const open = vi.fn() const clear = vi.fn() const selectPanel = vi.fn() - ctx.provide('layout', { selectPanel }) + ctx.provide('layout', { selectPanel, beginNavigation: () => new AbortController().signal }) const search = vi.fn(async () => ({ ok: true as const, value: { items: [{ sessionId: 'session' as never, snippet: 'match' }], hasMore: false }, diff --git a/packages/client/ui-workspace/tests/workspaces-service.client.spec.ts b/packages/client/ui-workspace/tests/workspaces-service.client.spec.ts index 7bc3b537c3..52f106f0f0 100644 --- a/packages/client/ui-workspace/tests/workspaces-service.client.spec.ts +++ b/packages/client/ui-workspace/tests/workspaces-service.client.spec.ts @@ -10,6 +10,8 @@ import type { ClientRemote, DirectoryListing } from '@deepseek-ai/dsh-api-remote import { RemoteError } from '@deepseek-ai/dsh-client-test-runtime' import type { RemoteResult } from '@deepseek-ai/dsh-api-remotes/client' import { SessionId } from '@deepseek-ai/dsh-session/types' +import { LayoutController } from '@deepseek-ai/dsh-client-ui-layout/client' +import type { MainPanelId } from '@deepseek-ai/dsh-client-ui-layout/client' import { DirectoryBrowseError, UiWorkspaceService } from '../src/client/navigation.ts' const sid = (id: string): SessionId => SessionId(id) @@ -108,6 +110,7 @@ class FakeSessions { readonly create: ReturnType> readonly open: ReturnType void>> readonly clear: ReturnType void>> + readonly fork = vi.fn(async () => sid('forked')) constructor(initial: SessionListState) { this.list = new MutableSource(initial) @@ -189,8 +192,14 @@ interface BenchOptions { function bench(options: BenchOptions = {}) { const ctx = new Context() - const selectPanel = vi.fn() - ctx.provide('layout', { selectPanel }) + const layout = new LayoutController({ + selectPanel: vi.fn(), retainMainPanels: vi.fn(), + setSidebar: vi.fn(), toggleSidebar: vi.fn(), setViewportWidth: vi.fn(), + setRightbar: vi.fn(), openRightbar: vi.fn(), closeRightbar: vi.fn(), + }, () => true) + const selectPanel = vi.spyOn(layout, 'selectPanel') + ctx.provide('layout', layout) + ctx.effect(() => () => { layout.dispose() }) const directoryPicker = new FakeDirectoryPicker() const workspaces = new FakeWorkspaces(options.workspaces ?? workspaceState([], [], 'pending')) const sessions = new FakeSessions(options.sessions ?? sessionState([], undefined, 'pending')) @@ -200,7 +209,7 @@ function bench(options: BenchOptions = {}) { workspaces, sessions as unknown as ISessions, ) - return { ctx, directoryPicker, sessions, uiWorkspace, workspaces, selectPanel } + return { ctx, directoryPicker, sessions, uiWorkspace, workspaces, layout, selectPanel } } async function flush(): Promise { @@ -228,6 +237,107 @@ describe('UiWorkspaceService', () => { expect(b.selectPanel).not.toHaveBeenCalled() }) + it('leaves a later panel selection in place when New Session finishes', async () => { + const b = bench({ + sessions: sessionState([summary('current')], sid('current')), + workspaces: workspaceState([workspace('alpha')]), + }) + const created = Promise.withResolvers() + b.sessions.create.mockReturnValue(created.promise) + const opening = vi.spyOn(b.uiWorkspace, 'openWorkspace') + b.uiWorkspace.startSession(wid('alpha')) + b.layout.selectPanel('panel-a' as MainPanelId) + created.resolve(sid('late')) + await opening.mock.results[0]!.value + expect(b.sessions.open).not.toHaveBeenCalled() + expect(b.selectPanel).toHaveBeenCalledExactlyOnceWith('panel-a') + expect(b.sessions.list.getSnapshot().current).toBe(sid('current')) + }) + + it('opens only the latest Workspace request when creation completes out of order', async () => { + const b = bench({ + sessions: sessionState([summary('current')], sid('current')), + workspaces: workspaceState([workspace('alpha'), workspace('beta')]), + }) + const older = Promise.withResolvers() + const newer = Promise.withResolvers() + b.sessions.create.mockImplementation(options => options?.workspaceId === wid('alpha') ? older.promise : newer.promise) + const oldDraft = vi.fn() + const newDraft = vi.fn() + const first = b.uiWorkspace.openWorkspace(wid('alpha'), oldDraft) + const second = b.uiWorkspace.openWorkspace(wid('beta'), newDraft) + newer.resolve(sid('newer')) + await second + older.resolve(sid('older')) + await first + expect(oldDraft).not.toHaveBeenCalled() + expect(newDraft).toHaveBeenCalledExactlyOnceWith(sid('newer')) + expect(b.sessions.open).toHaveBeenCalledExactlyOnceWith(sid('newer')) + }) + + it('does not move drafts or reopen a Workspace after reselecting the current Session', async () => { + const b = bench({ + sessions: sessionState([summary('current')], sid('current')), + workspaces: workspaceState([workspace('alpha')]), + }) + const created = Promise.withResolvers() + b.sessions.create.mockReturnValue(created.promise) + const moveDraft = vi.fn() + const pending = b.uiWorkspace.openWorkspace(wid('alpha'), moveDraft) + b.uiWorkspace.openSession(sid('current')) + created.resolve(sid('late')) + await pending + expect(moveDraft).not.toHaveBeenCalled() + expect(b.sessions.open).toHaveBeenCalledExactlyOnceWith(sid('current')) + }) + + it('keeps navigation performed by preparation and propagates preparation failures', async () => { + const b = bench({ + sessions: sessionState([summary('current')], sid('current')), + workspaces: workspaceState([workspace('alpha')]), + }) + await b.uiWorkspace.openWorkspace(wid('alpha'), () => { b.layout.selectPanel('panel-a' as MainPanelId) }) + expect(b.sessions.open).not.toHaveBeenCalled() + await expect(b.uiWorkspace.openWorkspace(wid('alpha'), () => { throw new Error('draft failed') })).rejects.toThrow('draft failed') + expect(b.sessions.open).not.toHaveBeenCalled() + }) + + it('opens a fork only while its navigation is current and preserves fork failures', async () => { + const b = bench() + await b.uiWorkspace.forkSession(sid('source')) + expect(b.sessions.fork).toHaveBeenCalledWith({ sessionId: sid('source'), increaseTitle: true }) + expect(b.sessions.open).toHaveBeenCalledExactlyOnceWith(sid('forked')) + const forked = Promise.withResolvers() + b.sessions.fork.mockReturnValueOnce(forked.promise) + const pending = b.uiWorkspace.forkSession(sid('source')) + b.layout.selectPanel('panel-a' as MainPanelId) + forked.resolve(sid('late-fork')) + await pending + expect(b.sessions.open).toHaveBeenCalledOnce() + b.sessions.fork.mockRejectedValueOnce(new Error('fork failed')) + await expect(b.uiWorkspace.forkSession(sid('source'))).rejects.toThrow('fork failed') + expect(b.sessions.open).toHaveBeenCalledOnce() + }) + + it('does not open a pending Workspace or fork after the navigation owner is disposed', async () => { + for (const kind of ['workspace', 'fork'] as const) { + const b = bench({ + sessions: sessionState([summary('current')], sid('current')), + workspaces: workspaceState([workspace('alpha')]), + }) + const resolved = Promise.withResolvers() + b.sessions.create.mockReturnValue(resolved.promise) + b.sessions.fork.mockReturnValue(resolved.promise) + const pending = kind === 'workspace' + ? b.uiWorkspace.openWorkspace(wid('alpha')) + : b.uiWorkspace.forkSession(sid('current')) + await b.ctx.fiber.dispose() + resolved.resolve(sid('late')) + await pending + expect(b.sessions.open).not.toHaveBeenCalled() + } + }) + it('reuses only an unarchived member blank and coalesces concurrent creation', async () => { const b = bench() const memberBlank = sid('member-blank') From 385f651c5ff047b87bd6b6027a161bd3c84a7166 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:36:39 +0800 Subject: [PATCH 42/58] fix(test): release default slot runtime sources symmetrically --- .../test-support/client-runtime/src/index.ts | 3 +- .../tests/helpers.client.spec.tsx | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/test-support/client-runtime/src/index.ts b/packages/test-support/client-runtime/src/index.ts index caad7ac5d1..23bc01bdfb 100644 --- a/packages/test-support/client-runtime/src/index.ts +++ b/packages/test-support/client-runtime/src/index.ts @@ -423,7 +423,7 @@ export class SlotTestRuntime { /** * Tear down: unmount React trees first, then dispose feature fibers, the - * root registration, minted session scopes, and persisted test state. + * root registration and standard sources, minted session scopes, and persisted test state. * Idempotent. * @returns completion of the teardown. */ @@ -434,6 +434,7 @@ export class SlotTestRuntime { for (const view of this.views.splice(0)) view.unmount() for (const handle of this.handles.splice(0)) await handle.dispose() this.root.release() + this.disposeWorkspaceSource() this.disposePanelInfoSource() await this.sessions.disposeScopes() localStorage.clear() diff --git a/packages/test-support/client-runtime/tests/helpers.client.spec.tsx b/packages/test-support/client-runtime/tests/helpers.client.spec.tsx index e5efdc20f4..389c1a0d90 100644 --- a/packages/test-support/client-runtime/tests/helpers.client.spec.tsx +++ b/packages/test-support/client-runtime/tests/helpers.client.spec.tsx @@ -46,6 +46,35 @@ function entry(seq: number): SessionLiveEventEntry { } describe('fixture helpers', () => { + it.each([false, true])('retracts default root sources without removing replacements (release first: %s)', async (releaseFirst) => { + const runtime = await SlotTestRuntime.create() + const hooks = { workspaces: runtime.workspaces.list, panelInfo: runtime.panelInfo } + let releaseReplacement: (() => void) | undefined + try { + if (releaseFirst) { + runtime.releaseWorkspaceSource() + runtime.releasePanelInfoSource() + } else { + await runtime.dispose() + } + releaseReplacement = runtime.slots.provideRoot({ hooks }) + await runtime.dispose() + await runtime.dispose() + for (const key of ['workspaces', 'panelInfo'] as const) { + expect(() => runtime.slots.provideRoot({ hooks: { [key]: hooks[key] } })) + .toThrow(`duplicate root standard hook '${key}'`) + } + } finally { + try { + releaseReplacement?.() + runtime.releaseWorkspaceSource() + runtime.releasePanelInfoSource() + } finally { + await runtime.dispose() + } + } + }) + it('drives panel hooks, retains keyed selection on owner updates, and releases the default source', async () => { const runtime = await SlotTestRuntime.create() try { From 713b1aacad688d5c1fdf77c979ede6d21b3dda9d Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:36:39 +0800 Subject: [PATCH 43/58] docs(client): synchronize panel navigation APIs and contracts --- .../2026-09-08-global-main-panels.i18n.yaml | 4 +- .../2026-09-08-global-main-panels.md | 10 +++-- .../2026-09-08-global-main-panels.zh.md | 10 +++-- docs/subsystems/sidebar-right.i18n.yaml | 4 +- docs/subsystems/sidebar-right.md | 2 +- docs/subsystems/sidebar-right.zh.md | 2 +- docs/subsystems/slots.i18n.yaml | 4 +- docs/subsystems/slots.md | 1 - docs/subsystems/slots.zh.md | 1 - .../client/ui-conversation/README.i18n.yaml | 4 +- packages/client/ui-conversation/README.md | 2 + packages/client/ui-conversation/README.zh.md | 2 + packages/client/ui-layout/README.i18n.yaml | 4 +- packages/client/ui-layout/README.md | 2 + packages/client/ui-layout/README.zh.md | 2 + .../ui-sidebar-right/src/client/service.ts | 4 +- packages/client/ui-sidebar/README.i18n.yaml | 4 +- packages/client/ui-sidebar/README.md | 4 +- packages/client/ui-sidebar/README.zh.md | 4 +- packages/client/ui-workspace/README.i18n.yaml | 4 +- packages/client/ui-workspace/README.md | 2 +- packages/client/ui-workspace/README.zh.md | 2 +- .../src/client/api-catalog.ts | 33 +++++++++++++++ .../src/client/slot-catalog.ts | 41 ++----------------- .../tests/api-catalog.client.spec.ts | 11 +++++ .../client-runtime/README.i18n.yaml | 4 +- .../test-support/client-runtime/README.md | 2 +- .../test-support/client-runtime/README.zh.md | 2 +- scripts/gen-cordis-inspect-catalog.ts | 4 +- 29 files changed, 99 insertions(+), 76 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-09-08-global-main-panels.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-08-global-main-panels.i18n.yaml index 550bfbea6e..f5b5d88586 100644 --- a/.agents/notes/implemented/architecture/2026-09-08-global-main-panels.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-08-global-main-panels.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-08-global-main-panels.md -2026-09-08-global-main-panels.md: 510b869b9bd32f4edd054a982a77baa57dace9c3 -2026-09-08-global-main-panels.zh.md: 226f46918132237f2049844e8647e01e6d2e63c4 +2026-09-08-global-main-panels.md: 75be68ac1bf6dceb812a5aedbca74ce58df929b3 +2026-09-08-global-main-panels.zh.md: 343f183367a5cfbd13127e32542f689c50306c89 diff --git a/.agents/notes/implemented/architecture/2026-09-08-global-main-panels.md b/.agents/notes/implemented/architecture/2026-09-08-global-main-panels.md index 510b869b9b..75be68ac1b 100644 --- a/.agents/notes/implemented/architecture/2026-09-08-global-main-panels.md +++ b/.agents/notes/implemented/architecture/2026-09-08-global-main-panels.md @@ -12,11 +12,13 @@ Plugins need application-wide views that do not belong to a Session. A Session-s The layout declares a root-scoped keyed `main` slot. The reserved `conversation` key belongs to the Conversation plugin, whose `main.conversation` child retains optional-Session binding. Other main entries receive no implicit Session binding. -The sidebar owns the root-scoped `sidebar.panellist` list and `sidebar.panellist.title` keyed slot. Each list entry supplies its icon and an id matching its main entry; its label provides ordinary text and the accessible name. A title registration can replace the visible label with React content. The shipped composition registers no panel entry, so the empty list has no DOM or spacing. +The sidebar owns the root-scoped `sidebar.panellist` list. Each list entry supplies its icon and an id matching its main entry; its string or locale-aware label provides plain visible text, the accessible name, and the collapsed tooltip. The shipped composition registers no panel entry, so the empty list has no DOM or spacing. Selection validates the live main entry and rejects a missing key without replacing the current panel. One eagerly created root store is shared by the renderer and layout controller. Its `panelInfo` and `layoutInfo` objects preserve independent references. The framework supplies `usePanelInfo`; individual rows and main content subscribe to their required selection values, while AppFrame reads only layout information. The right Sidebar's root controller decides whether to mount its Session subtree and reports the resulting track requirements to the frame. -`uiWorkspace.openSession(id)` selects the Session before returning the main area to the Conversation, including when the same Session is selected again. New Session and workspace navigation use that operation. Panel navigation neither cancels the retained Session nor writes a Session event. +`uiWorkspace.openSession(id)` selects the Session before returning the main area to the Conversation, including when the same Session is selected again. `openWorkspace` and `forkSession` use the layout's `beginNavigation()` abort signal and their own service lifetime to commit only the latest navigation. The Workspace preparation callback moves drafts synchronously only while the request remains current. Supersession prevents a late UI commit, not Session creation. Panel navigation neither cancels the retained Session nor writes a Session event. + +DOM focus is not navigation selection. Search and directory-picker controls can receive focus while the global panel and its selected sidebar row remain visible; opening a Session changes the main selection. ## Alternatives considered @@ -24,10 +26,12 @@ One eagerly created root store is shared by the renderer and layout controller. **A second navigation stack.** Back buttons and saved return destinations are unnecessary when New Session and workspace Session rows already provide explicit destinations. +**React title slots.** Navigation entries use the same plain label for visible text and accessibility; a separate title registration is outside that presentation. + **Flat selection and layout state with shallow comparison.** Separating the two stored objects preserves reference equality directly and avoids allocating and comparing a fresh layout projection on every panel selection. ## Consequences The default sidebar snapshots remain unchanged. Extension panels have no right Sidebar, and selecting a different global panel does not change layout preferences. Switching between a Conversation with a visible right Sidebar and a global panel still changes the required column widths; this is not a promise of zero browser layout work. -Panel selection is transient and resets on reload. Plugin disposal removes its contributions; removing the selected main entry returns the main area to the Conversation. Tests cover independent stored references, explicit Session navigation, declaration lifetimes, and the empty default sidebar. The [Slots reference](../../../../docs/subsystems/slots.md) owns the composition API. +Panel selection is transient and resets on reload. Plugin disposal removes its contributions; removing the selected main entry returns the main area to the Conversation. Tests register real temporary panels and cover row interaction, focus, independent stored references, invalid ids, superseded asynchronous navigation, declaration lifetimes, and the empty default sidebar. The [Slots reference](../../../../docs/subsystems/slots.md) owns the composition API. diff --git a/.agents/notes/implemented/architecture/2026-09-08-global-main-panels.zh.md b/.agents/notes/implemented/architecture/2026-09-08-global-main-panels.zh.md index 226f469181..343f183367 100644 --- a/.agents/notes/implemented/architecture/2026-09-08-global-main-panels.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-08-global-main-panels.zh.md @@ -12,11 +12,13 @@ Status: implemented 布局声明 root 作用域的 keyed `main` slot。保留的 `conversation` key 属于 Conversation 插件,其 `main.conversation` 子 slot 保留可选的会话绑定。其他主面板条目不获得隐式会话绑定。 -侧栏拥有 root 作用域的 `sidebar.panellist` list 和 `sidebar.panellist.title` keyed slot。每个 list 条目提供图标,以及与主面板条目匹配的 id;标签提供普通文字和无障碍名称。标题注册可用 React 内容替换可见标签。默认组合不注册面板条目,因此空列表没有 DOM 或间距。 +侧栏拥有 root 作用域的 `sidebar.panellist` list。每个 list 条目提供图标,以及与主面板条目匹配的 id;字符串或随语言变化的标签提供普通可见文字、无障碍名称和折叠提示。默认组合不注册面板条目,因此空列表没有 DOM 或间距。选中操作检查实时主面板条目,对缺失的 key 报错而不替换当前面板。 渲染器与布局控制器共享一个直接创建的 root 存储。其 `panelInfo` 和 `layoutInfo` 对象保持独立的引用。框架提供 `usePanelInfo`;各行和中央内容订阅所需的选中态值,AppFrame 仅读取布局信息。右侧 Sidebar 的 root 控制器决定是否挂载其会话子树,并把最终所需的列宽报告给框架。 -`uiWorkspace.openSession(id)` 先选中会话,再将中央区域切回 Conversation,包括再次选中同一个会话的情况。新会话和工作区导航使用该操作。面板导航既不取消保留的会话,也不写入会话事件。 +`uiWorkspace.openSession(id)` 先选中会话,再将中央区域切回 Conversation,包括再次选中同一个会话的情况。`openWorkspace` 和 `forkSession` 使用布局的 `beginNavigation()` abort signal 与自身 service 生命周期,只提交最新导航。工作区准备回调仅在请求仍有效时同步搬移草稿。请求过期会阻止晚到的 UI 提交,但不阻止会话创建。面板导航既不取消保留的会话,也不写入会话事件。 + +DOM 焦点不是导航选中态。搜索和目录选择控件可以获得焦点,同时保留全局面板及其侧栏行的选中态;打开会话才改变中央区域的选中态。 ## 考虑过的替代方案 @@ -24,10 +26,12 @@ Status: implemented **另一套导航栈。** 新会话和工作区会话行已经提供明确目标,不需要返回按钮或保存返回目的地。 +**React 标题 slot。** 导航条目的可见文字与无障碍名称使用同一个普通标签;独立的标题注册不属于这一呈现方式。 + **平铺选中态和布局状态,再做浅比较。** 将两者存为独立对象可以直接保持引用相等,避免每次选择面板都分配并比较新的布局投影。 ## 后果 默认侧栏快照保持不变。扩展面板没有右侧 Sidebar,选择另一个全局面板不会改变布局偏好。在显示右侧 Sidebar 的 Conversation 与全局面板之间切换时,所需列宽仍会变化;这并不保证浏览器完全不计算布局。 -面板选中态是瞬时状态,刷新后重置。插件 dispose(资源释放)会移除其贡献;移除当前选中的主面板条目会使中央区域回到 Conversation。测试覆盖存储引用的独立性、显式会话导航、声明生命周期和默认空侧栏。[Slots 参考](../../../../docs/subsystems/slots.zh.md)拥有组合 API 的说明。 +面板选中态是瞬时状态,刷新后重置。插件 dispose(资源释放)会移除其贡献;移除当前选中的主面板条目会使中央区域回到 Conversation。测试注册真实临时面板,覆盖行交互、焦点、存储引用的独立性、无效 id、过期异步导航、声明生命周期和默认空侧栏。[Slots 参考](../../../../docs/subsystems/slots.zh.md)拥有组合 API 的说明。 diff --git a/docs/subsystems/sidebar-right.i18n.yaml b/docs/subsystems/sidebar-right.i18n.yaml index 1b16e2a532..98c8a3a7b2 100644 --- a/docs/subsystems/sidebar-right.i18n.yaml +++ b/docs/subsystems/sidebar-right.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/sidebar-right.md -sidebar-right.md: db4103a282287d8ea163d66b19efec27e0a0630f -sidebar-right.zh.md: 1cb0b1e67e99acd7cb6aa12272cd57a5fdd207e0 +sidebar-right.md: 59eda91862282d240d49764d53c17f531ff4bf21 +sidebar-right.zh.md: 3ecb5e9ef496471316414b6aeb0c683a78188025 diff --git a/docs/subsystems/sidebar-right.md b/docs/subsystems/sidebar-right.md index db4103a282..59eda91862 100644 --- a/docs/subsystems/sidebar-right.md +++ b/docs/subsystems/sidebar-right.md @@ -8,7 +8,7 @@ This page is the reference for the subsystem's contracts: addresses, tab-type re ## Position and ownership -One docking surface exists per Session, held in a session-scoped slot store and drawn by the `rightbar` seat; a reload returns every session to the collapsed default, and switching sessions keeps each surface where it was ([state](../../packages/client/ui-sidebar-right/README.md#state)). The surface's every change is one recorded history entry computed by the kit's pure planners; a docked pane never stays empty, and the last pane reseeds the guide tab. +One docking surface exists per Session, held in a session-scoped slot store and drawn by `rightbar.session`. The root-scoped `rightbar` controller mounts that seat only while Conversation is selected; a reload returns every session to the collapsed default, and switching sessions keeps each surface where it was ([state](../../packages/client/ui-sidebar-right/README.md#state)). The surface's every change is one recorded history entry computed by the kit's pure planners; a docked pane never stays empty, and the last pane reseeds the guide tab. A tab type is two registrations that share one `kind`: a static definition in `ctx.sidebarRightTabs` saying which addresses the type opens, and a keyed slot registration supplying its body. The framework injects `useTabInfo()` for live Sidebar, pane and tab information; each type keeps its own state in its slot store. Packages import each other's declarations only as types. diff --git a/docs/subsystems/sidebar-right.zh.md b/docs/subsystems/sidebar-right.zh.md index 1cb0b1e67e..3ecb5e9ef4 100644 --- a/docs/subsystems/sidebar-right.zh.md +++ b/docs/subsystems/sidebar-right.zh.md @@ -8,7 +8,7 @@ ## 定位与归属 -每个会话恰有一个停靠面,保存在会话作用域的 slot store 里、由 `rightbar` 席位绘制;刷新页面后每个会话回到折叠的默认态,切换会话时各自的面保持原状([状态](../../packages/client/ui-sidebar-right/README.zh.md#state))。面的每一次变化都是 kit 纯规划器算出的一条历史记录;停靠的 pane 从不空着,最后一个 pane 会重新种入引导 tab。 +每个会话恰有一个停靠面,保存在会话作用域的 slot store 里、由 `rightbar.session` 绘制。root 作用域的 `rightbar` 控制器仅在选中 Conversation 时挂载该席位;刷新页面后每个会话回到折叠的默认态,切换会话时各自的面保持原状([状态](../../packages/client/ui-sidebar-right/README.zh.md#state))。面的每一次变化都是 kit 纯规划器算出的一条历史记录;停靠的 pane 从不空着,最后一个 pane 会重新种入引导 tab。 一个 tab 类型是共用一个 `kind` 的两次注册:在 `ctx.sidebarRightTabs` 里的静态定义说明该类型打开哪些地址,一次 keyed slot 注册提供它的正文。框架注入 `useTabInfo()` 以读取 Sidebar、窗格和标签的实时信息;各类型把自身状态放在 slot store 里。各包之间只以类型形式引用彼此的声明。 diff --git a/docs/subsystems/slots.i18n.yaml b/docs/subsystems/slots.i18n.yaml index 6a214a8670..d829845174 100644 --- a/docs/subsystems/slots.i18n.yaml +++ b/docs/subsystems/slots.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/slots.md -slots.md: 92206d2deb3d1681e6c1d230d1f37146af6b720b -slots.zh.md: 47028aec897adcfdca499a19513162ad825928b4 +slots.md: e659a0ccff548dcf1d9aa6f422d1773a838a62ef +slots.zh.md: 92d1af68f8b67637342055d3c11aea29194048ad diff --git a/docs/subsystems/slots.md b/docs/subsystems/slots.md index 92206d2deb..e659a0ccff 100644 --- a/docs/subsystems/slots.md +++ b/docs/subsystems/slots.md @@ -114,7 +114,6 @@ root │ ├─ sidebar.brand.mark │ ├─ sidebar.brand.name │ ├─ sidebar.panellist -│ ├─ sidebar.panellist.title │ ├─ sidebar.footer.action │ ├─ sidebar.workspaces │ │ └─ sidebar.workspaces.directoryFlow diff --git a/docs/subsystems/slots.zh.md b/docs/subsystems/slots.zh.md index 47028aec89..92d1af68f8 100644 --- a/docs/subsystems/slots.zh.md +++ b/docs/subsystems/slots.zh.md @@ -114,7 +114,6 @@ root │ ├─ sidebar.brand.mark │ ├─ sidebar.brand.name │ ├─ sidebar.panellist -│ ├─ sidebar.panellist.title │ ├─ sidebar.footer.action │ ├─ sidebar.workspaces │ │ └─ sidebar.workspaces.directoryFlow diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index f5dab9a40a..a1eea2e97a 100644 --- a/packages/client/ui-conversation/README.i18n.yaml +++ b/packages/client/ui-conversation/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-conversation/README.md -README.md: 633a35b3a66056628a0eeaff0261c97ab5e93c33 -README.zh.md: f86dd863ecc338cca63956ae6e75bf9c7c5fea6a +README.md: 19a210bb200dae359920e85ff944b68242f45b4b +README.zh.md: c5a56871eb99607c924b2b2e048de72d918e6a8b diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index 633a35b3a6..19a210bb20 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -36,6 +36,8 @@ Target packages declaration-merge their snapshot and Location data maps, then re ## Shell and standard props +Workspace selection uses `uiWorkspace.openWorkspace` to prepare the target and commit navigation. Draft text and attachments move in its synchronous preparation callback only while that request is current; later navigation or owner disposal leaves the original draft intact. + The package occupies the root-scoped `main` key `conversation`, whose wrapper declares the optional-Session `main.conversation` shell. It registers strict Session header/body entries, View list, composer chain and bar, input regions, Hero regions, queue dock, draft persistence, and phase calculation. `ctx.uiSession.provide()` materializes the Conversation and input sources from the same Session binding and supplies `inputActions` as a stable standard prop. View selection is deterministic: a registered persisted selection wins, otherwise registered `chat` wins, otherwise no View renders. It never chooses the first registered View. Shell phase combines Session lifecycle with the active-target set; no target-specific snapshot is read by the shell. diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index f86dd863ec..c5a56871eb 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -36,6 +36,8 @@ target package 通过 declaration merge 扩展 snapshot 与 Location data map, ## Shell 与标准 props +工作区选择使用 `uiWorkspace.openWorkspace` 准备目标并提交导航。草稿文字和附件仅在请求仍有效时,通过它的同步准备回调搬移;后续导航或所有者释放会保留原草稿。 + 本包占据 root 作用域 `main` 中的 `conversation` key,其包装层声明 optional-Session `main.conversation` shell。本包注册strict Session header/body、View list、composer chain 与 bar、输入区域、Hero 区域、queue dock、草稿持久化和 phase 计算。`ctx.uiSession.provide()` 从同一个 Session binding 物化 Conversation 与 input source,并将 `inputActions` 作为稳定标准 prop 提供。 View 选择规则固定:有效且已注册的持久化选择优先,其次是已注册的 `chat`,否则不渲染 View;绝不选择第一个已注册 View。Shell phase 只组合 Session lifecycle 与 active-target set,不读取任何 target-specific snapshot。 diff --git a/packages/client/ui-layout/README.i18n.yaml b/packages/client/ui-layout/README.i18n.yaml index bf8dc86fe1..8d680bc72f 100644 --- a/packages/client/ui-layout/README.i18n.yaml +++ b/packages/client/ui-layout/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-layout/README.md -README.md: 50fef3853cdab83c7c04edd46d73a31967a495b7 -README.zh.md: d649643155dd5e23ee185ff282aa14011ed73793 +README.md: 4fe19fb9ec0a549dc76ca311d0564cbe86c9f5f9 +README.zh.md: 7a61e7808ffb3d81214388a2792a901ced8d9564 diff --git a/packages/client/ui-layout/README.md b/packages/client/ui-layout/README.md index 50fef3853c..4fe19fb9ec 100644 --- a/packages/client/ui-layout/README.md +++ b/packages/client/ui-layout/README.md @@ -41,6 +41,8 @@ The presenter consumes resolved theme snapshots and projects them onto the docum
    Implementation internals — click to expand +`selectPanel(id)` checks the live `main` registry before changing selection; an absent key throws and leaves the current panel intact. `beginNavigation()` returns an abort signal for an asynchronous UI navigation. A later call, a valid panel selection (including repeated selection), or layout disposal aborts that signal without cancelling underlying Session creation. Consumers check the signal before committing navigation or moving drafts. + One registration declares four child slots and binds `ctx.layout` methods `selectPanel`, `toggleSidebar`, `openRightbar(track, fullscreen)`, and `closeRightbar`. One root store separates `panelInfo` selection from `layoutInfo` measurements, width preferences, and presentation reports. `usePanelInfo` subscribes to the stable selection object; AppFrame subscribes to the stable layout object. The `rightbar` owner supplies actual `width`, `viewportWidth`, and normal-presentation eligibility `canShow`; insufficient room causes a deterministic close, never automatic reopening on widening. Fullscreen hides the width handle without releasing a track the occupant retains. AppFrame keeps the column containers mounted. The right column's root controller renders `rightbar.session` through `SessionProvider` only while the Conversation is selected; its unmount report releases the track. The independent title component uses the selected Session title only while the Conversation is visible, with the build-configured product title or localized `common.brand.localBuild` as its fallback; locale revisions update that fallback. The theme presenter is a second effect: pure DOM writes from resolved snapshots — initial state through the getter once, then event-driven only, with no React path. It applies palette, font-size, and token variables before measuring the rendered background as the single color authority. Fullscreen presentation suppresses grid and handle transitions; its occupant reports the new columns only after covering the frame. Fullscreen exit keeps transitions suppressed while the frame installs its destination geometry: close removes the right track, and restore retains it. Subsequent normal geometry actions restore ordinary transitions.
    diff --git a/packages/client/ui-layout/README.zh.md b/packages/client/ui-layout/README.zh.md index d649643155..7a61e7808f 100644 --- a/packages/client/ui-layout/README.zh.md +++ b/packages/client/ui-layout/README.zh.md @@ -41,6 +41,8 @@ kind: "package-reference"
    实现细节——点击展开 +`selectPanel(id)` 在改变选中态前检查实时 `main` 注册表;缺失的 key 会抛错并保留当前面板。`beginNavigation()` 为异步 UI 导航返回 abort signal。后续调用、有效面板选择(包括重复选择)或布局释放会中止该 signal,但不取消底层会话创建。消费者在提交导航或搬移草稿前检查 signal。 + 一次注册声明四个子slot并绑定 `ctx.layout` 的 `selectPanel`、`toggleSidebar`、`openRightbar(track, fullscreen)` 与 `closeRightbar`。同一个 root 存储把 `panelInfo` 选中态与 `layoutInfo` 测量、宽度偏好、呈现报告分开。`usePanelInfo` 订阅引用稳定的选中态对象,AppFrame 订阅引用稳定的布局对象。`rightbar` 的owner参数为实际 `width`、`viewportWidth` 与普通呈现的 `canShow`;占用方在空间不足时执行确定性的收起,变宽不自行重新展开。全屏隐藏宽度手柄,但不自行释放占用方要求保留的轨道。AppFrame 保持各列容器挂载。右栏的 root 控制器仅在选中会话界面时,经 `SessionProvider` 渲染 `rightbar.session`;内容卸载时的报告释放列宽。独立的标题组件仅在会话界面可见时使用所选会话标题,以构建配置的产品标题或本地化 `common.brand.localBuild` 为回退值;语言变化会更新该回退值。主题呈现器是第二个 effect:从解析后的快照做纯 DOM 写入——初始状态经 getter 读取一次,此后仅事件驱动,不经过 React。它先应用调色板、字号与 token 变量,再把渲染出的背景测量为唯一的颜色依据。 全屏呈现禁用网格和手柄过渡;占用方完全覆盖框架后才报告新的列宽。 退出全屏时,框架先保持无过渡并安装目标布局:关闭移除右轨道,恢复保留右轨道。后续普通几何操作恢复正常过渡。
    diff --git a/packages/client/ui-sidebar-right/src/client/service.ts b/packages/client/ui-sidebar-right/src/client/service.ts index 25e702b612..ca5ad487a3 100644 --- a/packages/client/ui-sidebar-right/src/client/service.ts +++ b/packages/client/ui-sidebar-right/src/client/service.ts @@ -24,8 +24,8 @@ * hand the store one settled intent and record the navigation in the Tab * domain. Placement is the caller's option, never a type's property. * - * Wiring follows `LayoutController.attachPanels`: the registration hands the - * service its store actions, and the service is the face other plugins hold. + * The registration adopts Session stores and injects the mounted seat binding; + * callers use the service's navigation methods. */ import type { FloatRect, PaneId, TabId, TabRecord } from '@deepseek-ai/dsh-client-ui-dockkit' import { activeDockPaneId, canSplit, dockPaneIds, findTabPane, getPane } from '@deepseek-ai/dsh-client-ui-dockkit' diff --git a/packages/client/ui-sidebar/README.i18n.yaml b/packages/client/ui-sidebar/README.i18n.yaml index 3a87b39288..061d06f859 100644 --- a/packages/client/ui-sidebar/README.i18n.yaml +++ b/packages/client/ui-sidebar/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-sidebar/README.md -README.md: 00c0ffce05f19c86bff86d0eff01154b98d24aeb -README.zh.md: 4e4c9777a30680a5015e69c11c8cb57b187f1b0a +README.md: 1b520213809ca98a6819483a4ee5ea4f451841d2 +README.zh.md: 8caae9f14eb4b7a70e674b9cef7245aaf077b378 diff --git a/packages/client/ui-sidebar/README.md b/packages/client/ui-sidebar/README.md index 00c0ffce05..1b52021380 100644 --- a/packages/client/ui-sidebar/README.md +++ b/packages/client/ui-sidebar/README.md @@ -33,7 +33,7 @@ The expanded brand row renders `sidebar.brand.mark` and `sidebar.brand.name` as ### Global panel entries -Plugins add an icon component to the root-scoped `sidebar.panellist` list with an `id`, optional `order`, and a string or locale-aware `label`. The same id addresses the component registered in the layout's root-scoped `main` keyed slot. An optional `sidebar.panellist.title` keyed registration replaces the label's visible content; the label remains the accessible name. Each row reads its own selected state through `usePanelInfo`. With no registrations, neither the list nor spacing for it is rendered. The shipped composition registers no example panel. +Plugins add an icon component to the root-scoped `sidebar.panellist` list with an `id`, optional `order`, and a string or locale-aware `label`. The same id addresses the component registered in the layout's root-scoped `main` keyed slot; selecting a missing main entry throws without changing the current selection. The label supplies plain visible text, the accessible name, and the collapsed tooltip. Each row reads its own selected state through `usePanelInfo`; moving DOM focus to search or a directory picker does not change the displayed panel or its selected row. With no registrations, neither the list nor spacing for it is rendered. The shipped composition registers no example panel. ### Collapse behavior @@ -104,4 +104,4 @@ None.
    -**Runtime invariant:** No companion is published. A pure-consumer plugin deriving its rows in-component from the standard useSessions delivery — it emits no cordis events and owns no cross-plugin mutable state; derivation and interaction behavior are asserted directly by this package's tree/component specs. +**Runtime invariant:** No companion is published. Panel metadata is a read-only presentation projection of the Slot registry and locale, with no independent write API. The registry owns entry identity and disposal; this package's assembly tests assert the projection after registration and locale notifications settle. The shell owns no separate navigation state to reconcile with those sources. diff --git a/packages/client/ui-sidebar/README.zh.md b/packages/client/ui-sidebar/README.zh.md index 4e4c9777a3..8caae9f14e 100644 --- a/packages/client/ui-sidebar/README.zh.md +++ b/packages/client/ui-sidebar/README.zh.md @@ -33,7 +33,7 @@ dsh Web 客户端的侧边栏让用户识别当前构建、启动新会话、将 ### 全局面板入口 -插件在 root 作用域的 `sidebar.panellist` list 中注册图标组件,提供 `id`、可选 `order`,以及字符串或随语言变化的 `label`。同一个 id 寻址布局中 root 作用域 `main` keyed slot 的组件。可选的 `sidebar.panellist.title` keyed 注册替换标签的可见内容;`label` 仍用作无障碍名称。每一行通过 `usePanelInfo` 读取自己的选中态。没有注册项时,列表及其间距均不渲染。默认组合不注册示例面板。 +插件在 root 作用域的 `sidebar.panellist` list 中注册图标组件,提供 `id`、可选 `order`,以及字符串或随语言变化的 `label`。同一个 id 寻址布局中 root 作用域 `main` keyed slot 的组件;选择不存在的主面板条目会抛错,并保留当前选中态。标签提供普通可见文字、无障碍名称和折叠提示。每一行通过 `usePanelInfo` 读取自己的选中态;DOM 焦点移到搜索框或目录选择器时,显示的面板及其列表项选中态不变。没有注册项时,列表及其间距均不渲染。默认组合不注册示例面板。 ### 折叠行为 @@ -104,4 +104,4 @@ dsh Web 客户端的侧边栏让用户识别当前构建、启动新会话、将
    -**运行时不变式:** 不发布伴生入口。这是从标准 `useSessions` delivery 直接派生行的纯消费插件,不发出 Cordis 事件,也不持有跨插件可变状态。 +**运行时不变式:** 不发布伴生入口。面板元数据是 Slot 注册表与 locale 的只读呈现投影,没有独立写入 API。注册表拥有条目身份和释放;本包的装配测试在注册和语言通知完成后断言投影。外壳没有需要与这些来源协调的独立导航状态。 diff --git a/packages/client/ui-workspace/README.i18n.yaml b/packages/client/ui-workspace/README.i18n.yaml index b19f293b9c..a79ee51c23 100644 --- a/packages/client/ui-workspace/README.i18n.yaml +++ b/packages/client/ui-workspace/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-workspace/README.md -README.md: 231cff649addac22c4383f372201c2ce79a22100 -README.zh.md: e8bac20531bee009cb61db42749597a415de4279 +README.md: 857b38c2ca08ca4e9078d8f9dfcfe8462be50722 +README.zh.md: cf6f2d273f00add793cc5ac588db968c22007256 diff --git a/packages/client/ui-workspace/README.md b/packages/client/ui-workspace/README.md index 231cff649a..857b38c2ca 100644 --- a/packages/client/ui-workspace/README.md +++ b/packages/client/ui-workspace/README.md @@ -51,7 +51,7 @@ The value is intentionally best effort for cold Sessions. An identity-matching u ----- -`ctx.uiWorkspace.openSession(id)` selects the Session and returns the main area to the Conversation as one UI navigation action, including when that Session was already current. New Session uses the same path after resolving its target; selection failure leaves a global panel visible. Session rows read `usePanelInfo` to suppress their selected appearance while a global panel is active. +`ctx.uiWorkspace.openSession(id)` selects the Session and returns the main area to the Conversation as one UI navigation action, including when that Session was already current. `openWorkspace(id, beforeOpen?)` and `forkSession(id)` open their result only if no later navigation has superseded the request; New Session uses `openWorkspace`. The optional synchronous preparation callback runs only for a current Workspace request, so superseded requests do not move composer drafts. Navigation or owner disposal suppresses the late UI commit, not the underlying Session creation. Selection failure leaves a global panel visible. Session rows read `usePanelInfo` to suppress their selected appearance while a global panel is active; search and directory-picker focus alone do not leave that panel. ## Understand the implementation diff --git a/packages/client/ui-workspace/README.zh.md b/packages/client/ui-workspace/README.zh.md index e8bac20531..cf6f2d273f 100644 --- a/packages/client/ui-workspace/README.zh.md +++ b/packages/client/ui-workspace/README.zh.md @@ -51,7 +51,7 @@ Session 行渲染运行时的实时 `pendingInteraction` 分类:审批显示** ----- -`ctx.uiWorkspace.openSession(id)` 将选中会话与中央区域返回会话界面作为一次 UI 导航操作,包括目标会话已经是当前会话的情况。新会话操作解析目标后使用同一路径;选中失败时保留当前全局面板。会话行读取 `usePanelInfo`,在全局面板活跃时不显示会话选中样式。 +`ctx.uiWorkspace.openSession(id)` 将选中会话与中央区域返回会话界面作为一次 UI 导航操作,包括目标会话已经是当前会话的情况。`openWorkspace(id, beforeOpen?)` 和 `forkSession(id)` 仅在请求未被后续导航替代时打开结果;新会话使用 `openWorkspace`。可选的同步准备回调仅对仍有效的工作区请求执行,因此过期请求不会搬移 composer 草稿。后续导航或所有者释放会阻止晚到的 UI 提交,但不取消底层会话创建。选中失败时保留当前全局面板。会话行读取 `usePanelInfo`,在全局面板活跃时不显示会话选中样式;仅把焦点移到搜索框或目录选择器不会离开该面板。 ## 理解实现 diff --git a/packages/extensions/cordis-client-runner/src/client/api-catalog.ts b/packages/extensions/cordis-client-runner/src/client/api-catalog.ts index 8f4ea2f887..dfc66c56bf 100644 --- a/packages/extensions/cordis-client-runner/src/client/api-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/api-catalog.ts @@ -86,6 +86,18 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ summary: 'Panel navigation and geometry actions exposed through ctx.layout.', description: 'Panel navigation and geometry actions exposed through ctx.layout.', methods: [ + { + signature: 'selectPanel(panelId: MainPanelId | null): void', + description: 'Select a global central panel without changing the current Session.', + parameters: [{ name: 'panelId', description: 'registered main key, or null to show the Conversation.' }], + throws: ['if the selected main key is not registered; preserves the current selection.'], + }, + { + signature: 'beginNavigation(): AbortSignal', + description: 'Start an asynchronous navigation, superseding any earlier pending navigation.', + parameters: [], + returns: 'a signal aborted by the next navigation or layout disposal; check it before committing UI state.', + }, { signature: 'toggleSidebar(): void', description: 'Toggle the sidebar panel (closed ⟷ contract default width).', @@ -314,6 +326,23 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ summary: 'Workspace archive and directory operations consumed by Client UI domains.', description: 'Workspace archive and directory operations consumed by Client UI domains.', methods: [ + { + signature: 'openSession(sessionId: SessionId): void', + description: 'Select a Session and show its Conversation as one UI navigation action.', + parameters: [{ name: 'sessionId', description: 'listed or retained Session to display.' }], + }, + { + signature: 'openWorkspace(workspaceId: WorkspaceId, beforeOpen?: (sessionId: SessionId) => void): Promise', + description: 'Connect a Workspace and open its Session unless a later navigation supersedes it.', + parameters: [{ name: 'workspaceId', description: 'target Workspace.' }, { name: 'beforeOpen', description: 'optional synchronous preparation for the selected Session, skipped after supersession.' }], + returns: 'completion; a superseded request may create a Session but does not open it.', + }, + { + signature: 'forkSession(sessionId: SessionId): Promise', + description: 'Fork a Session and open the child unless a later navigation supersedes it.', + parameters: [{ name: 'sessionId', description: 'source Session.' }], + returns: 'completion; a superseded request leaves its child available without selecting it.', + }, { signature: 'connectWorkspace(workspaceId: WorkspaceId): Promise', description: 'Resolve the reusable or newly created blank Session for a Workspace.', @@ -609,6 +638,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'LocaleSnapshot', declaration: 'export interface LocaleSnapshot {\n active: LocaleId;\n locales: readonly LocaleDefinition[];\n revision: number;\n}', }, + { + name: 'MainPanelId', + declaration: 'export type MainPanelId = Branded<\'MainPanelId\'>;', + }, { name: 'MatchedShare', declaration: 'export type MatchedShare = E[\'kind\'] extends \'chain\' ? {\n matched: M;\n} : object;', diff --git a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts index 691bc619fc..d6232bd312 100644 --- a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts @@ -2173,7 +2173,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.footer.action\', () => ctx.slots.register(\n { name: \'sidebar.footer.action\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:52', + source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:50', }, { key: 'sidebar.panellist', @@ -2222,41 +2222,6 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.panellist\', () => ctx.slots.register(\n { name: \'sidebar.panellist\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:32', }, - { - key: 'sidebar.panellist.title', - kind: 'keyed', - scope: 'root', - summary: 'Global panel titles keyed by list id, with the list label as fallback.', - doc: 'Global panel titles keyed by list id, with the list label as fallback.', - registerOptions: [ - { - name: 'key', - requirement: 'required', - type: 'string', - doc: 'Your cell key: the entry renders where the owner dispatches this exact key. Registering an already-occupied key replaces that occupant.', - }, - ], - ownerProps: [ - '/** Title presentation supplied by the global panel row. */\nexport interface SidebarPanelTitleOwnerProps {\n /** Whether this panel is selected in the main column. */\n active: boolean\n}', - ], - ownerPropsReferences: [], - standardProps: [ - 'useResource: UseResource', - 'useWorkspaces: SnapshotSelectorHook', - 'usePanelInfo: UsePanelInfo', - 'useSessions: UseSessions', - 'useSessionPendingInteraction: UseSessionPendingInteraction', - 'useWorkspaces: SnapshotSelectorHook', - ], - keyDomain: 'open: any string the owner dispatches (no compile-time key set), none are taken yet', - hookContext: '', - slotInject: '', - declaredBy: 'an entry in \'sidebar\' (client-ui-sidebar), so it exists while that entry is mounted', - occupants: [], - replaceRisk: 'none', - example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.panellist.title\', () => ctx.slots.register(\n { name: \'sidebar.panellist.title\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:34', - }, { key: 'sidebar.right.pane.tab', kind: 'keyed', @@ -2469,7 +2434,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'shadows-shipped-ui', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.settings\', () => ctx.slots.register(\n { name: \'sidebar.settings\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:47', + source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:45', }, { key: 'sidebar.workspaces', @@ -2499,7 +2464,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'shadows-shipped-ui', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.workspaces\', () => ctx.slots.register(\n { name: \'sidebar.workspaces\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:41', + source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:39', }, { key: 'sidebar.workspaces.directoryFlow', diff --git a/packages/extensions/cordis-client-runner/tests/api-catalog.client.spec.ts b/packages/extensions/cordis-client-runner/tests/api-catalog.client.spec.ts index 9e2e106b9e..e291d7c82e 100644 --- a/packages/extensions/cordis-client-runner/tests/api-catalog.client.spec.ts +++ b/packages/extensions/cordis-client-runner/tests/api-catalog.client.spec.ts @@ -3,6 +3,14 @@ import { EVENT_API, queryServiceApi, SERVICE_API } from '../src/client/api-catal describe('Client Cordis inspect catalog', () => { it('publishes the split Workspace Controller and UI navigation services', () => { + expect(SERVICE_API.find(service => service.key === 'layout')?.methods.map(method => method.signature)) + .toEqual([ + 'selectPanel(panelId: MainPanelId | null): void', + 'beginNavigation(): AbortSignal', + 'toggleSidebar(): void', + 'openRightbar(track: boolean, fullscreen: boolean): void', + 'closeRightbar(): void', + ]) expect(SERVICE_API.find(service => service.key === 'workspaces')?.methods.map(method => method.signature)) .toEqual([ 'create(input: { path: string }): Promise', @@ -13,6 +21,9 @@ describe('Client Cordis inspect catalog', () => { ]) expect(SERVICE_API.find(service => service.key === 'uiWorkspace')?.methods.map(method => method.signature)) .toEqual([ + 'openSession(sessionId: SessionId): void', + 'openWorkspace(workspaceId: WorkspaceId, beforeOpen?: (sessionId: SessionId) => void): Promise', + 'forkSession(sessionId: SessionId): Promise', 'connectWorkspace(workspaceId: WorkspaceId): Promise', 'startSession(workspaceId?: WorkspaceId): void', 'archiveSession(sessionId: SessionId): Promise', diff --git a/packages/test-support/client-runtime/README.i18n.yaml b/packages/test-support/client-runtime/README.i18n.yaml index 1bdb09785b..67a7969a90 100644 --- a/packages/test-support/client-runtime/README.i18n.yaml +++ b/packages/test-support/client-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/test-support/client-runtime/README.md -README.md: 38b0c1b305d83920a819c6eddf9b54ecc895ef7f -README.zh.md: 301e3633d25a7fa7599eb9283d141c641163d588 +README.md: 0b6caf7237520ca1a10fe0bbdf3c12c6d793461f +README.zh.md: 40d27ac534f0f93d539101e2006a37a918318a81 diff --git a/packages/test-support/client-runtime/README.md b/packages/test-support/client-runtime/README.md index 38b0c1b305..0b6caf7237 100644 --- a/packages/test-support/client-runtime/README.md +++ b/packages/test-support/client-runtime/README.md @@ -42,7 +42,7 @@ await runtime.dispose() `mount` prechecks required services and fails loud when one is missing — `provide(name, value)` supplies an extra service first. The runtime provides an unavailable `fileUpload` stub so assemblies can mount; replace `runtime.fileUpload.upload` before mounting when a test exercises upload behavior. `storeOf(key, scopeKey)` returns the live store instance the renderer hands a slot's component for identity and action-driven-write assertions. -The optional render options select a keyed entry with `entryKey` or a list item with `only`; `view.update(owner)` retains that selection. `runtime.panelInfo` supplies the default `usePanelInfo` source with no global panel selected. Release it with `releasePanelInfoSource()` before mounting the production Layout owner. +The optional render options select a keyed entry with `entryKey` or a list item with `only`; `view.update(owner)` retains that selection. `runtime.panelInfo` supplies the default `usePanelInfo` source with no global panel selected. Release it with `releasePanelInfoSource()` before mounting the production Layout owner. `dispose()` releases both default Workspace and panel-info root sources; early release is idempotent and does not remove replacement owners. ### Local DOM snapshots diff --git a/packages/test-support/client-runtime/README.zh.md b/packages/test-support/client-runtime/README.zh.md index 301e3633d2..40d27ac534 100644 --- a/packages/test-support/client-runtime/README.zh.md +++ b/packages/test-support/client-runtime/README.zh.md @@ -42,7 +42,7 @@ await runtime.dispose() `mount` 会预检必需服务,缺失时自明报错——先用 `provide(name, value)` 提供额外服务。运行时会提供不可用的 `fileUpload` 替身,使装配可以挂载;测试上传行为时,需要在挂载前替换 `runtime.fileUpload.upload`。`storeOf(key, scopeKey)` 返回渲染器交给 slot 组件的实时 store 实例,用于身份与动作驱动写入断言。 -可选渲染参数通过 `entryKey` 选择 keyed 条目,或通过 `only` 选择 list 条目;`view.update(owner)` 保留该选择。`runtime.panelInfo` 提供默认的 `usePanelInfo` 数据源,初始不选中全局面板。挂载生产 Layout 所有者之前,先调用 `releasePanelInfoSource()` 释放该数据源。 +可选渲染参数通过 `entryKey` 选择 keyed 条目,或通过 `only` 选择 list 条目;`view.update(owner)` 保留该选择。`runtime.panelInfo` 提供默认的 `usePanelInfo` 数据源,初始不选中全局面板。挂载生产 Layout 所有者之前,先调用 `releasePanelInfoSource()` 释放该数据源。`dispose()` 同时释放默认的工作区与面板信息根数据源;提前释放是幂等的,不会移除替代它们的所有者。 ### 局部 DOM 快照 diff --git a/scripts/gen-cordis-inspect-catalog.ts b/scripts/gen-cordis-inspect-catalog.ts index 552ac07733..4d0b46661f 100644 --- a/scripts/gen-cordis-inspect-catalog.ts +++ b/scripts/gen-cordis-inspect-catalog.ts @@ -10,13 +10,13 @@ const root = resolve(import.meta.dirname, '..') const CLIENT_OUT = 'packages/extensions/cordis-client-runner/src/client/api-catalog.ts' const CLIENT_SERVICES: Readonly> = { - layout: ['toggleSidebar', 'openRightbar', 'closeRightbar'], + layout: ['selectPanel', 'beginNavigation', 'toggleSidebar', 'openRightbar', 'closeRightbar'], locale: ['getLocale', 'getSnapshot', 'subscribe', 'setLocale', 'addLanguage', 'register', 'bind'], sessions: ['open', 'openSubagent', 'setSubagentCatalogOpen', 'refreshSubagents', 'search', 'fork', 'scope', 'binding'], slots: ['register', 'inject'], theme: ['getTheme', 'setTheme', 'setFontSize', 'register', 'overrideTokens'], uiWorkspace: [ - 'connectWorkspace', 'startSession', 'archiveSession', 'pickDirectory', 'listDirectory', + 'openSession', 'openWorkspace', 'forkSession', 'connectWorkspace', 'startSession', 'archiveSession', 'pickDirectory', 'listDirectory', 'createDirectory', ], workspaces: ['create', 'rename', 'delete', 'insertSessionBefore', 'archiveSession'], From e807447288648dfb25d4598af0e458ef2b3eaf59 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:47:20 +0800 Subject: [PATCH 44/58] fix(client): re-read navigation cancellation after preparation --- packages/client/ui-workspace/src/client/navigation.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/client/ui-workspace/src/client/navigation.ts b/packages/client/ui-workspace/src/client/navigation.ts index 6d86beb826..c968595d8a 100644 --- a/packages/client/ui-workspace/src/client/navigation.ts +++ b/packages/client/ui-workspace/src/client/navigation.ts @@ -138,10 +138,11 @@ class UiWorkspaceService extends Service implements UiWorkspace { async openWorkspace(workspaceId: WorkspaceId, beforeOpen?: (sessionId: SessionId) => void): Promise { const navigation = AbortSignal.any([this.ctx.layout.beginNavigation(), this.lifetime.signal]) + const isCurrent = (): boolean => !navigation.aborted const sessionId = await this.connectWorkspace(workspaceId) - if (navigation.aborted) return + if (!isCurrent()) return beforeOpen?.(sessionId) - if (!navigation.aborted) this.openSession(sessionId) + if (isCurrent()) this.openSession(sessionId) } async forkSession(sessionId: SessionId): Promise { From 58144ccf785c63fe7678bd7b0397a0bbff4b9cfc Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Tue, 8 Sep 2026 21:56:52 +0800 Subject: [PATCH 45/58] test(web): await command and settings readiness before assertions --- apps/web/tests/lifecycle-chrome.e2e.ts | 18 ++++++++---- apps/web/tests/settings-chrome.e2e.ts | 40 +++++++++++++++++--------- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/apps/web/tests/lifecycle-chrome.e2e.ts b/apps/web/tests/lifecycle-chrome.e2e.ts index 30d9f922be..f42337be37 100644 --- a/apps/web/tests/lifecycle-chrome.e2e.ts +++ b/apps/web/tests/lifecycle-chrome.e2e.ts @@ -15,7 +15,7 @@ import { fileURLToPath } from 'node:url' import { join } from 'node:path' import type { Browser, Page, WebSocketRoute } from 'playwright' import { chromium } from 'playwright' -import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' +import { afterAll, beforeAll, describe, expect, it, onTestFailed, onTestFinished } from 'vitest' import type { SessionEvent } from '@deepseek-ai/dsh-session' import { acknowledgeReloadConnectionLoss, assertFixtureInventory, captureExpandedTurnProcessAria, @@ -72,10 +72,17 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', () it.skipIf(MODE === 'record')('opens the shared slash menu from plus with only Command candidates', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-command-menu-launcher')) + const input = page.locator('[data-composer-input]').first() + onTestFinished(async () => { + await input.press('Escape') + await writeComposerDraft(page, input, '') + await page.getByRole('listbox', { name: 'Trigger suggestions' }).waitFor({ state: 'hidden' }) + }) const launcher = page.getByRole('button', { name: 'Commands' }) await launcher.click() const menu = page.getByRole('listbox', { name: 'Trigger suggestions' }) - await menu.waitFor({ timeout: 10_000 }) + await menu.getByRole('option').first().waitFor({ timeout: 10_000 }) + await menu.getByRole('status').waitFor({ state: 'hidden', timeout: 10_000 }) const snapshot = await captureStableAria(page, '[role="listbox"]', scaffold.workspaceCwd) await compareOrRefreshGolden(COMMAND_MENU_EXPECTED, snapshot, MODE) expect(snapshot).toContain('text: Commands') @@ -84,9 +91,9 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', () const launchedBox = await menu.boundingBox() await page.locator('[data-composer-input]').first().press('Escape') await expect.poll(() => menu.count()).toBe(0) - const input = page.locator('[data-composer-input]').first() await writeComposerDraft(page, input, '/') - await menu.waitFor({ timeout: 10_000 }) + await menu.getByRole('option').first().waitFor({ timeout: 10_000 }) + await menu.getByRole('status').waitFor({ state: 'hidden', timeout: 10_000 }) const typedBox = await menu.boundingBox() expect(launchedBox).not.toBeNull() expect(typedBox).not.toBeNull() @@ -114,7 +121,8 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', () const launcher = zhPage.getByRole('button', { name: '指令' }) await launcher.click() const menu = zhPage.getByRole('listbox', { name: '触发候选建议' }) - await menu.waitFor({ timeout: 10_000 }) + await menu.getByRole('option').first().waitFor({ timeout: 10_000 }) + await menu.getByRole('status').waitFor({ state: 'hidden', timeout: 10_000 }) const snapshot = await captureStableAria(zhPage, '[role="listbox"]', scaffold.workspaceCwd) await compareOrRefreshGolden(COMMAND_MENU_ZH_EXPECTED, snapshot, MODE) expect(zhTripwire.pageErrors).toEqual([]) diff --git a/apps/web/tests/settings-chrome.e2e.ts b/apps/web/tests/settings-chrome.e2e.ts index e617c9aead..8ef2af7048 100644 --- a/apps/web/tests/settings-chrome.e2e.ts +++ b/apps/web/tests/settings-chrome.e2e.ts @@ -12,7 +12,7 @@ import { readFile } from 'node:fs/promises' import { fileURLToPath } from 'node:url' import type { Browser, Locator, Page } from 'playwright' import { chromium } from 'playwright' -import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' +import { afterAll, beforeAll, describe, expect, it, onTestFailed, onTestFinished } from 'vitest' import { join } from 'node:path' import { SessionId } from '@deepseek-ai/dsh-session' import { @@ -378,6 +378,10 @@ describe('web e2e: settings modal and General preferences', () => { it('steps the content font size, applies it to body, and persists across reload', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-font-size')) + onTestFinished(async () => { + await page.keyboard.press('Escape') + await page.getByRole('dialog', { name: '设置', exact: true }).waitFor({ state: 'hidden' }) + }) const readFontSize = async (target: Page = page): Promise => await target.evaluate( () => document.body.style.getPropertyValue('--dsh-content-font-size'), ) @@ -392,6 +396,24 @@ describe('web e2e: settings modal and General preferences', () => { probe.remove() return size }) + // The displayed value is optimistic; wait for the write before the next step. + const stepFontSize = async (button: Locator, px: number): Promise => { + const [response] = await Promise.all([ + page.waitForResponse((reply) => { + if (new URL(reply.url()).pathname !== '/api/settings/mutate' || reply.request().method() !== 'POST') return false + const request = reply.request().postDataJSON() as { payload: { args: { ns: string } } } + return request.payload.args.ns === 'ui-theme' + }), + button.click(), + ]) + expect(await response.finished()).toBeNull() + const envelope = await response.json() as { result: { ok: boolean } } + expect(envelope.result.ok).toBe(true) + await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 }) + .toMatch(new RegExp(`ui-theme:\n(?:\\s+\\w+: .*\n)*?\\s+fontSize: ${px}`)) + await page.getByRole('dialog', { name: '设置' }).getByText(String(px), { exact: true }).waitFor({ timeout: 5_000 }) + await expect.poll(readFontSize, { timeout: 5_000 }).toBe(`${px}px`) + } expect(await readFontSize()).toBe('14px') expect(await readSecondaryFontSize()).toBe('13px') await page.getByRole('button', { name: '设置', exact: true }).click() @@ -400,17 +422,12 @@ describe('web e2e: settings modal and General preferences', () => { // The stepper reveals its arrows on hover; the up arrow steps 14 → 15 → 16. await dialog.getByText('14', { exact: true }).hover() const increase = dialog.getByRole('button', { name: '增大字号' }) - await increase.click() - await dialog.getByText('15', { exact: true }).waitFor({ timeout: 5_000 }) + await stepFontSize(increase, 15) // 15 is the piecewise boundary: the secondary tier holds at 13px (−2) // where the ≤14 branch would have given 14px (−1). await expect.poll(readSecondaryFontSize, { timeout: 5_000 }).toBe('13px') - await increase.click() - await dialog.getByText('16', { exact: true }).waitFor({ timeout: 5_000 }) - await expect.poll(readFontSize, { timeout: 5_000 }).toBe('16px') + await stepFontSize(increase, 16) await expect.poll(readSecondaryFontSize, { timeout: 5_000 }).toBe('14px') - await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 }) - .toMatch(/ui-theme:\n(?:\s+\w+: .*\n)*?\s+fontSize: 16/) await page.keyboard.press('Escape') // Reload: the boot script embeds the durable size and ThemeRuntime seeds @@ -429,11 +446,8 @@ describe('web e2e: settings modal and General preferences', () => { await restored.waitFor({ timeout: 10_000 }) await restored.getByText('16', { exact: true }).hover() const decrease = restored.getByRole('button', { name: '减小字号' }) - await decrease.click() - await restored.getByText('15', { exact: true }).waitFor({ timeout: 5_000 }) - await decrease.click() - await restored.getByText('14', { exact: true }).waitFor({ timeout: 5_000 }) - await expect.poll(readFontSize, { timeout: 5_000 }).toBe('14px') + await stepFontSize(decrease, 15) + await stepFontSize(decrease, 14) await page.keyboard.press('Escape') expect(tripwire.pageErrors).toEqual([]) }, 90_000) From f3d7ef2b9596889f03ace066b76bf16ca5a55c1c Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Tue, 8 Sep 2026 16:35:12 +0800 Subject: [PATCH 46/58] docs(client): define browser third-party build inputs --- ...30-generated-third-party-notices.i18n.yaml | 4 +-- ...026-07-30-generated-third-party-notices.md | 2 +- ...-07-30-generated-third-party-notices.zh.md | 2 +- ...08-26-published-dependency-faces.i18n.yaml | 4 +-- .../2026-08-26-published-dependency-faces.md | 2 +- ...026-08-26-published-dependency-faces.zh.md | 2 +- ...browser-third-party-build-inputs.i18n.yaml | 6 ++++ ...-09-08-browser-third-party-build-inputs.md | 35 +++++++++++++++++++ ...-08-browser-third-party-build-inputs.zh.md | 35 +++++++++++++++++++ packages/client/AGENTS.md | 4 +-- packages/client/store/README.i18n.yaml | 4 +-- packages/client/store/README.md | 1 + packages/client/store/README.zh.md | 1 + packages/client/ui-dockkit/README.i18n.yaml | 4 +-- packages/client/ui-dockkit/README.md | 2 ++ packages/client/ui-dockkit/README.zh.md | 2 ++ .../client/ui-primitives/README.i18n.yaml | 4 +-- packages/client/ui-primitives/README.md | 2 ++ packages/client/ui-primitives/README.zh.md | 2 ++ 19 files changed, 102 insertions(+), 16 deletions(-) create mode 100644 .agents/notes/implemented/process/2026-09-08-browser-third-party-build-inputs.i18n.yaml create mode 100644 .agents/notes/implemented/process/2026-09-08-browser-third-party-build-inputs.md create mode 100644 .agents/notes/implemented/process/2026-09-08-browser-third-party-build-inputs.zh.md diff --git a/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.i18n.yaml b/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.i18n.yaml index 91c1bc8481..95ff31f9f1 100644 --- a/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-30-generated-third-party-notices.md -2026-07-30-generated-third-party-notices.md: b81f386f0d0e820b0361c775fb4c45a9e633d04b -2026-07-30-generated-third-party-notices.zh.md: d3b3e1686bfdf8901902f995205c1dbdd6dfbf38 +2026-07-30-generated-third-party-notices.md: 41ac0c75ca55c81f2055c867bd029ee7e4a350f9 +2026-07-30-generated-third-party-notices.zh.md: 4097831828287e7180cf37d713fa4694ae028b31 diff --git a/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.md b/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.md index b81f386f0d..41ac0c75ca 100644 --- a/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.md +++ b/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.md @@ -20,7 +20,7 @@ One trigger gap is accepted rather than worked around: lefthook inspects only fi The file discloses **direct** dependencies by default. The complete npm closure with pinned versions already lives in `pnpm-lock.yaml` (`pnpm licenses list` renders it) and the Python closure in `python/sdk/uv.lock`; re-materializing either as prose would be a second, worse copy. The one explicit transitive disclosure is the official Claude platform payload set declared by `@anthropic-ai/claude-agent-sdk` through `optionalDependencies`, because those packages carry the distributed Claude Code executable rather than ordinary library implementation detail. -**Tiering is by declaring area, not by manifest section.** A package is a runtime dependency when any manifest outside `DEV_ONLY_AREAS` — the root manifest, `packages/test-support/`, `packages/test-support/client-runtime/`, `website/`, `native/` — names it under `dependencies` or `optionalDependencies`. Section names alone are wrong in both directions: a test-support package declares `vitest` under `dependencies` without shipping it, and the root source-run scripts execute through `tsx`, which no manifest declares as a runtime dependency at all (the generator marks it runtime explicitly). +**Tiering follows distribution, not manifest section.** Installed runtime libraries are identified by `dependencies` or `optionalDependencies` outside `DEV_ONLY_AREAS` — the root manifest, `packages/test-support/`, `packages/test-support/client-runtime/`, `website/`, `native/`. Browser inputs resolved by the shipping tsdown and Vite configurations also count as runtime, even in `devDependencies`; [browser third-party build inputs](2026-09-08-browser-third-party-build-inputs.md) owns that classification. Test-support dependencies do not ship merely because their manifest says `dependencies`, and the generator explicitly discloses `tsx` because source launches execute through its ESM hook. The runtime tier deliberately covers **every mountable plugin**, not just what the CLI, Web UI, and Python runtime load by default. Source execution can mount any plugin package from a user's `cordis.yml`; `@modelcontextprotocol/sdk` and the OpenTelemetry packages therefore reach real users even though no default assembly imports them. Under-disclosure is the costly direction for a legal notice. diff --git a/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.zh.md b/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.zh.md index d3b3e1686b..4097831828 100644 --- a/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.zh.md +++ b/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.zh.md @@ -20,7 +20,7 @@ Status: implemented 文件默认只披露**直接**依赖。完整的 npm 闭包连同锁定版本已记录在 `pnpm-lock.yaml`(`pnpm licenses list` 可渲染),Python 闭包记录在 `python/sdk/uv.lock`;再用散文誊一遍只会得到一份更差的副本。唯一明确披露的传递依赖,是 `@anthropic-ai/claude-agent-sdk` 通过 `optionalDependencies` 声明的官方 Claude 平台载荷集合,因为这些包承载随产品分发的 Claude Code 可执行文件,而非普通的库实现细节。 -**分层依据是声明方所在区域,而非 manifest 字段名。** 只要 `DEV_ONLY_AREAS` 之外的任一 manifest——即根 manifest、`packages/test-support/`、`packages/test-support/client-runtime/`、`website/`、`native/` 之外——在 `dependencies` 或 `optionalDependencies` 里点名某个包,它就是运行时依赖。单看字段名在两个方向上都会出错:测试支撑包把 `vitest` 写在 `dependencies` 里却并不交付它;而根目录的源码运行脚本通过 `tsx` 执行,根本没有任何 manifest 把它声明为运行时依赖,只能由生成器显式标记。 +**分层依据是分发内容,而非 manifest 字段名。** 安装的运行时库由 `DEV_ONLY_AREAS` 之外的 `dependencies` 或 `optionalDependencies` 识别;排除区域为根 manifest、`packages/test-support/`、`packages/test-support/client-runtime/`、`website/`、`native/`。发布所用的 tsdown 与 Vite 配置解析到的浏览器输入也属于运行时,即使它们位于 `devDependencies`;[浏览器第三方构建输入](2026-09-08-browser-third-party-build-inputs.zh.md)拥有这项分类。测试支撑依赖不会仅因字段写成 `dependencies` 就被交付,而生成器显式披露 `tsx`,因为源码启动通过其 ESM 钩子执行。 运行时层刻意覆盖**所有可挂载的插件**,而不止 CLI、Web UI 与 Python 运行时默认加载的那些。从源码运行时,用户可以通过 `cordis.yml` 挂载任何插件包;因此,`@modelcontextprotocol/sdk` 与 OpenTelemetry 系列即使没有任何默认装配引入,也会触达真实用户。对法务披露而言,披露不足才是代价更高的那个方向。 diff --git a/.agents/notes/implemented/process/2026-08-26-published-dependency-faces.i18n.yaml b/.agents/notes/implemented/process/2026-08-26-published-dependency-faces.i18n.yaml index d44c9594bb..d24d629c9f 100644 --- a/.agents/notes/implemented/process/2026-08-26-published-dependency-faces.i18n.yaml +++ b/.agents/notes/implemented/process/2026-08-26-published-dependency-faces.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-26-published-dependency-faces.md -2026-08-26-published-dependency-faces.md: 25e9f2ce139a7cd4efb64dbe71d49d8c9f88c24b -2026-08-26-published-dependency-faces.zh.md: ccc198b164b7450b6840862faf23b546c99fa2a6 +2026-08-26-published-dependency-faces.md: 1a94ccedcf6f614c7853c96c329e88d037167460 +2026-08-26-published-dependency-faces.zh.md: 4ac3f461018c21047af6359ca5e27d050da5a2c4 diff --git a/.agents/notes/implemented/process/2026-08-26-published-dependency-faces.md b/.agents/notes/implemented/process/2026-08-26-published-dependency-faces.md index 25e9f2ce13..1a94ccedcf 100644 --- a/.agents/notes/implemented/process/2026-08-26-published-dependency-faces.md +++ b/.agents/notes/implemented/process/2026-08-26-published-dependency-faces.md @@ -28,7 +28,7 @@ A workspace package reached by a runtime value import from the Host entry closur An export whose constructor identity or module state must be shared appears in `peerRequiredHostExports`; importing one such export keeps the whole package edge in matching `peerDependencies` and `devDependencies`. Each export-table key is an exact module specifier and each value is a reviewed export set. The verifier follows runtime local imports from the Host entry, records named and default imports and re-exports, and rejects exports covered by neither the package list nor an export table; namespace, dynamic, and side-effect imports remain unbounded unless the complete exact entry is package-classified. -Workspace imports used by the Client bundle, type-only imports, module augmentations, `dsh.client.inject`, invariant companions, and existing metadata-only peers belong only in `devDependencies`. Ordinary third-party packages imported by the Host runtime belong in `dependencies`; other third-party relationships keep their declared section. Workspace references use `workspace:^`. +Workspace imports used by the Client bundle, type-only imports, module augmentations, `dsh.client.inject`, and existing metadata-only peers belong only in `devDependencies`. Host runtime imports, including additional Node entries, follow the Host classification. [Browser third-party build inputs](2026-09-08-browser-third-party-build-inputs.md) governs ordinary third-party declarations; it partially supersedes their preservation in this decision. Workspace references use `workspace:^`. Some development relationships exist only in `dsh.client.inject` or TypeScript project references. The policy's `configurationOnlyDevDependencies` table names only those reviewed edges and keeps them in `devDependencies`. diff --git a/.agents/notes/implemented/process/2026-08-26-published-dependency-faces.zh.md b/.agents/notes/implemented/process/2026-08-26-published-dependency-faces.zh.md index ccc198b164..4ac3f46101 100644 --- a/.agents/notes/implemented/process/2026-08-26-published-dependency-faces.zh.md +++ b/.agents/notes/implemented/process/2026-08-26-published-dependency-faces.zh.md @@ -28,7 +28,7 @@ Host 入口闭包中的运行期 value import 所到达的 workspace 包,只 constructor 身份或模块状态必须共享的导出列入 `peerRequiredHostExports`;一旦使用这类导出,整条包依赖边就保留在范围一致的 `peerDependencies` 与 `devDependencies` 中。每个导出表的 key 都是精确 module specifier,每个 value 都是经审查的导出集合。验证器从 Host 入口沿运行期本地 import 扫描,记录具名与默认 import 和 re-export,并拒绝既没有包级分类、也没有导出级分类的导出;除非完整的精确入口已按包分类,否则 namespace、dynamic 和 side-effect import 仍无法限定范围。 -Client bundle 使用的 workspace import、纯类型 import、模块扩充、`dsh.client.inject`、invariant companion 和仅有元数据的现存 peer 只属于 `devDependencies`。Host 运行时导入的普通第三方包属于 `dependencies`;其他第三方关系保持原区段。Workspace 引用使用 `workspace:^`。 +Client bundle 使用的 workspace import、纯类型 import、模块扩充、`dsh.client.inject` 和仅有元数据的现存 peer 只属于 `devDependencies`。Host 运行时 import(包括额外 Node 入口)遵循 Host 分类。[浏览器第三方构建输入](2026-09-08-browser-third-party-build-inputs.zh.md)规定普通第三方声明,部分取代本决策对它们原区段的保留。Workspace 引用使用 `workspace:^`。 部分开发期关系只存在于 `dsh.client.inject` 或 TypeScript project reference 中。策略的 `configurationOnlyDevDependencies` 表只列出这些已评审的依赖边,并将它们保留在 `devDependencies` 中。 diff --git a/.agents/notes/implemented/process/2026-09-08-browser-third-party-build-inputs.i18n.yaml b/.agents/notes/implemented/process/2026-09-08-browser-third-party-build-inputs.i18n.yaml new file mode 100644 index 0000000000..7fc8e364f2 --- /dev/null +++ b/.agents/notes/implemented/process/2026-09-08-browser-third-party-build-inputs.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-09-08-browser-third-party-build-inputs.md +2026-09-08-browser-third-party-build-inputs.md: 97e61f3b99955cb402f9e622b2d40e3ee2953d79 +2026-09-08-browser-third-party-build-inputs.zh.md: e23c4e9d4615c7c5bc788beafcf6b94699f211ce diff --git a/.agents/notes/implemented/process/2026-09-08-browser-third-party-build-inputs.md b/.agents/notes/implemented/process/2026-09-08-browser-third-party-build-inputs.md new file mode 100644 index 0000000000..97e61f3b99 --- /dev/null +++ b/.agents/notes/implemented/process/2026-09-08-browser-third-party-build-inputs.md @@ -0,0 +1,35 @@ +# Agent Note: Browser third-party libraries as build inputs + +Status: implemented + +English | [中文](2026-09-08-browser-third-party-build-inputs.zh.md) + +## Problem + +Prebuilt browser plugins distribute their third-party implementations inside JavaScript, but production npm dependencies still make installers download those libraries separately and resolve their peers. When React is declared only for development, installers can select a different React version for those extra dependencies than the browser artifact uses. Both `use-sync-external-store@1.2.0` and `@tanstack/react-virtual@3.14.9` support the current React 18; this problem does not require a React upgrade. + +## Decision + +Browser-only third-party dependencies belong in `devDependencies`, including implementations inlined into dynamic plugins, static browser-library inputs, and React shared by the Web shell. This partially supersedes the preservation of ordinary third-party declarations in [published dependency faces](2026-08-26-published-dependency-faces.md); that note continues to govern package selection, Host value dependencies, and Cordis identity. + +The dependency classifier collects build inputs from source imports and JSX. Third-party libraries reachable by the Host runtime take precedence as `dependencies`; additional Node build entries must also be checked. Type-only source references do not create Host runtime dependencies. Existing configuration-metadata and shared-Host-export classifications remain unchanged. + +Npm sections do not select browser bundling behavior. Dynamic plugins inline private libraries and obtain React and other shared modules from the platform table; static browser libraries retain bare imports and styles for the final Vite build. Static packages are Web-shell build inputs, not independently installed libraries with every rebundling dependency provided. Source builds need development dependencies; installed published Web artifacts do not. + +License classification follows distributed content. Dependency resolution through the real browser build configurations covers dynamic plugins and the Web shell; resolved third-party implementations remain [runtime disclosures](2026-07-30-generated-third-party-notices.md) even when manifests declare them for development. Test tools, erased type imports, and build tools do not become distributed code merely by appearing in `devDependencies`. + +## Alternatives considered + +**Declare another production React or override peer resolution.** This retains an extra installed graph that the browser does not use, without making that installed copy the browser's shared instance. + +**Inline every static-library dependency early.** This changes Vite's third-party chunks, caching, and CSS handling; dependency classification does not require those build changes. + +**Move every third-party dependency of a Client-bearing package.** Dual-face packages still load Host libraries, including `fflate` for ZIP output and `zod` for RPC validation; those installation relationships must remain. + +**Classify license disclosures directly by manifest section.** Distributing browser code and having an installer download a same-named package are different facts; that distinction cannot remove license checks on distributed code. + +## Consequences + +Production dependencies do not install third-party libraries a second time solely for browser implementations. React and React DOM retain one build version, and plugins consume the Web shell's shared instance. Classification tests constrain browser dev-only inputs, Host precedence, and idempotent repair; publication checks reject React installation leaks, while browser artifact verification independently covers module loading. + +Classification remains source-based. License resolution likewise must not depend on existing `lib/` files or write build outputs; its tests cover real resolution, type erasure, asset references, and missing dependencies. Developers independently consuming static packages or public types supply the corresponding build dependencies themselves; this decision adds no standalone browser-library support promise. diff --git a/.agents/notes/implemented/process/2026-09-08-browser-third-party-build-inputs.zh.md b/.agents/notes/implemented/process/2026-09-08-browser-third-party-build-inputs.zh.md new file mode 100644 index 0000000000..e23c4e9d46 --- /dev/null +++ b/.agents/notes/implemented/process/2026-09-08-browser-third-party-build-inputs.zh.md @@ -0,0 +1,35 @@ +# Agent Note: 浏览器第三方库作为构建输入 + +Status: implemented + +[English](2026-09-08-browser-third-party-build-inputs.md) | 中文 + +## 问题 + +预构建浏览器插件的第三方实现已经随 JavaScript 分发,但生产 NPM 依赖仍让安装器另外下载这些库并解析其对等依赖(peer dependency)。React 只在构建期声明时,安装器可以为这些额外依赖选择与浏览器产物不同的 React 版本。`use-sync-external-store@1.2.0` 与 `@tanstack/react-virtual@3.14.9` 都支持当前 React 18;问题不要求升级 React。 + +## 决策 + +浏览器专用第三方依赖属于 `devDependencies`,包括动态插件内联的实现、静态浏览器库的输入,以及由 Web 壳提供共享实例的 React。该规则部分取代[已发布依赖分类](2026-08-26-published-dependency-faces.zh.md)对普通第三方声明的保留策略;该文档继续规定包的选择范围、Host 值依赖与 Cordis 实例身份。 + +依赖分类器从源码导入和 JSX 收集构建输入。Host 运行时可达的第三方库优先归入 `dependencies`;额外 Node 构建入口也必须纳入检查。源码中的纯类型引用不产生 Host 运行时依赖。配置元数据与共享 Host 导出的既有分类保持不变。 + +NPM 字段不决定浏览器打包方式。动态插件继续内联私有库,并从平台模块表读取 React 等共享模块;静态浏览器库继续保留裸导入及样式,由最终 Vite 构建处理。静态包是 Web 壳的构建输入,不承诺独立安装后具备二次打包所需的全部依赖。源码构建需要开发依赖;安装已发布的 Web 产物不需要它们。 + +许可证分类以交付内容为准。真实浏览器构建配置的依赖解析同时覆盖动态插件与 Web 壳;解析到的第三方实现仍计入[运行时披露](2026-07-30-generated-third-party-notices.zh.md),即使清单将其列为开发依赖。测试工具、纯类型导入和构建工具不会仅因处于 `devDependencies` 而算作分发代码。 + +## 考虑过的替代方案 + +**为生产安装额外声明 React,或强制覆盖对等依赖。** 这保留了浏览器不使用的额外安装图,且并未使安装副本成为浏览器共享实例。 + +**把静态库的所有依赖提前内联。** 这会改变 Vite 对第三方分块、缓存和 CSS 的处理;依赖分类不需要改变这些构建行为。 + +**把带 Client 的包的全部第三方依赖移走。** 双面包仍可能在 Host 加载库,例如 ZIP 输出所用的 `fflate` 和 RPC 校验所用的 `zod`;这些安装关系必须保留。 + +**按清单字段直接划分许可证披露。** 浏览器代码的交付与安装器是否下载同名包不是同一事实,不能因此取消对分发代码的许可证检查。 + +## 结果 + +生产依赖不再为纯浏览器实现重复安装第三方库。React 与 React DOM 保持同一构建版本,插件继续消费 Web 壳的共享实例。分类测试同时约束浏览器 dev-only、Host 优先和修复幂等;发布检查拒绝 React 安装泄漏,浏览器产物验证独立覆盖实际模块加载。 + +分类检查保留源码执行方式。许可证解析也不能依赖已有 `lib/` 或写入构建输出;其测试覆盖真实解析、纯类型擦除、资源引用与缺失依赖。独立消费静态包或公开类型的开发者需要自己提供相应构建依赖;本决策不增加独立浏览器库的支持承诺。 diff --git a/packages/client/AGENTS.md b/packages/client/AGENTS.md index 0d1633b902..91e57ed5e9 100644 --- a/packages/client/AGENTS.md +++ b/packages/client/AGENTS.md @@ -60,9 +60,9 @@ Npm sections describe installation and development relationships; each build fac 1. **Every client package keeps Cordis in matching `peerDependencies` and `devDependencies`.** This includes the static packages because their Node face participates in the same Cordis plugin contract. 2. **A package under `packages/client/` is always covered; `dsh.client` marks a Client/Host package outside that directory.** Explicit include/exclude entries handle exceptions. Every covered package's Host entry is scanned, while a `./client` export alone does not select dependency policy. -3. **Browser and type relationships are development-only.** Client imports, type-only imports, module augmentations, TypeScript project references, `dsh.client.inject`, invariant companions, and metadata-only peers belong only in `devDependencies`. Configuration-only entries that Knip cannot infer from imports are listed in the dependency policy and projected into `knip.json` by `--fix`. +3. **Browser and type relationships are development-only.** Workspace and third-party Client imports, type-only imports, module augmentations, TypeScript project references, `dsh.client.inject`, and metadata-only peers belong only in `devDependencies`. Configuration-only entries absent from imports are listed in the dependency policy; Node companion value imports follow the Host rules below. 4. **Host value imports require classified exports.** A workspace value reached from the package's Host entry belongs only in `dependencies` when its exact module specifier and runtime export appear in `safeHostDependencyExports`. Exports whose identity or module state must be shared appear in `peerRequiredHostExports` and keep the whole package edge in matching `peerDependencies` and `devDependencies`. The verifier rejects unclassified exports before `--fix` writes manifests. -5. **Ordinary installed libraries stay in `dependencies`.** This includes private implementation libraries bundled into `lib/client.js` and bare imports left in a statically linked `lib/index.js`; the final Vite host, not the library build, merges and splits the latter. +5. **Only Host runtime libraries require installation dependencies.** Browser-only third-party implementations belong in `devDependencies`, whether inlined into `lib/client.js` or retained as bare imports in a static browser library. Static libraries are Web-shell build inputs; the final Vite build merges their imports and styles. Distributed browser code remains subject to runtime license disclosure ([decision](../../.agents/notes/implemented/process/2026-09-08-browser-third-party-build-inputs.md)). 6. **Browser and Node build faces declare externality independently.** A dynamic browser half uses the baseline plus `dsh.client.external`; a statically linked face externalizes every bare specifier; a Node face externalizes its production dependencies ([`tsdown.client.ts`](tsdown.client.ts)). Moving a name between npm sections must not silently change bundle contents. 7. **Keep the published payload closed.** Every relative runtime import and emitted asset must be covered by `files`; the repository publint pass checks the exact publication view. diff --git a/packages/client/store/README.i18n.yaml b/packages/client/store/README.i18n.yaml index d6af2417f1..657b1a396d 100644 --- a/packages/client/store/README.i18n.yaml +++ b/packages/client/store/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/store/README.md -README.md: 9d675c1189ae194b913293c89904b4c4f39c879e -README.zh.md: 3ff170b71c320688e50c02c20479ee1d94061a87 +README.md: 0d4525ab6eb2a58bdea89673b96f9aaf5e0e2cf8 +README.zh.md: b66a2e31675c82d933526d40ed9eba20bc5077d9 diff --git a/packages/client/store/README.md b/packages/client/store/README.md index 9d675c1189..0d4525ab6e 100644 --- a/packages/client/store/README.md +++ b/packages/client/store/README.md @@ -32,6 +32,7 @@ None; the stores neither assemble nor send model requests. - **Persistence is browser-local** — persisted stores use JSON in `localStorage`; non-browser runtimes disable persistence, and the package provides no cross-device synchronization. +- **Web-shell build input** — the static ESM retains third-party imports for Vite; independent consumers supply its development dependencies ([dependency rules](../AGENTS.md#dependency-declaration)). diff --git a/packages/client/store/README.zh.md b/packages/client/store/README.zh.md index 3ff170b71c..b66a2e3167 100644 --- a/packages/client/store/README.zh.md +++ b/packages/client/store/README.zh.md @@ -32,6 +32,7 @@ kind: "package-library" - **持久化仅限浏览器本地**——持久化 store 使用 `localStorage` 中的 JSON;非浏览器运行时会禁用持久化,本包也不提供跨设备同步。 +- **Web 壳构建输入**——静态 ESM 为 Vite 保留第三方导入;独立消费方自行提供开发依赖([依赖规则](../AGENTS.md#dependency-declaration))。 diff --git a/packages/client/ui-dockkit/README.i18n.yaml b/packages/client/ui-dockkit/README.i18n.yaml index bd0993ceba..19c343ed54 100644 --- a/packages/client/ui-dockkit/README.i18n.yaml +++ b/packages/client/ui-dockkit/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-dockkit/README.md -README.md: e012295852ad3c957695fcfbd0568a45b3f54856 -README.zh.md: 2423887d60460651a224ffe1cb0fc9b61363c0e2 +README.md: 330bff66577464a26eac50d92ee54cc0cdefc8df +README.zh.md: f5259400055a28ea99f2205f9ba0a28d2cc13bd2 diff --git a/packages/client/ui-dockkit/README.md b/packages/client/ui-dockkit/README.md index e012295852..330bff6657 100644 --- a/packages/client/ui-dockkit/README.md +++ b/packages/client/ui-dockkit/README.md @@ -74,6 +74,8 @@ These are not stylistic; each one fixes a defect found in a real browser. ## Build shape +Its static ESM retains third-party imports for the Web shell's Vite build; independent consumers supply its development dependencies ([dependency rules](../AGENTS.md#dependency-declaration)). + The package is statically linked: tsdown's `staticLinked` preset emits one browser ESM bundle at `lib/index.js` (every bare specifier stays an import, sourcemaps chain to the sources) and ships the stylesheet under `lib/` at its `src`-relative path, and the Web shell resolves the package name and bundles that artifact itself, so vite stays the only owner of class hashing. One consequence is load-bearing — the kit keeps **one** stylesheet, `dockkit.module.css`, because a consumer de-duplicates injected sheets by file name and a collision would drop one silently. diff --git a/packages/client/ui-dockkit/README.zh.md b/packages/client/ui-dockkit/README.zh.md index 2423887d60..f525940005 100644 --- a/packages/client/ui-dockkit/README.zh.md +++ b/packages/client/ui-dockkit/README.zh.md @@ -74,6 +74,8 @@ tab 的 `kind` 是不透明字符串。种子 tab 是工厂(`DockControllerOpt ## 构建形态 +静态 ESM 为 Web 壳的 Vite 构建保留第三方导入;独立消费方自行提供开发依赖([依赖规则](../AGENTS.md#dependency-declaration))。 + 本包静态链接:tsdown 的 `staticLinked` 预设在 `lib/index.js` 产出一个浏览器 ESM bundle(所有裸说明符保持为 import,sourcemap 链回源码),并把样式表按其相对 `src` 的路径放到 `lib/` 下;Web 外壳按包名解析并自行打包该产物,因此 vite 仍是 class 哈希的唯一拥有者。有一个后果是承重的——套件只保留**一张**样式表 `dockkit.module.css`,因为消费方按文件名去重注入的样式表,撞名会静默丢掉一张。 diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml index 4bda8cc5f7..6dd7e78078 100644 --- a/packages/client/ui-primitives/README.i18n.yaml +++ b/packages/client/ui-primitives/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-primitives/README.md -README.md: 18b136c78539d8ba6da3b17239112967faf63602 -README.zh.md: f91435e44a5ca79b245fde3dd025a732d35e54e9 +README.md: 3e0922690cc68a477e4b1d5ca1ed142d713369f0 +README.zh.md: de8487499ad31affc95986125c910b368ae39b95 diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md index 18b136c785..3e0922690c 100644 --- a/packages/client/ui-primitives/README.md +++ b/packages/client/ui-primitives/README.md @@ -25,6 +25,8 @@ Use `dsh-client-ui-primitives` to build web-client controls and render agent out ## Use this package +This package is a Web-shell build input. Its static ESM retains third-party imports and styles for Vite; independent consumers supply its development dependencies ([dependency rules](../AGENTS.md#dependency-declaration)). + Compose feature UI from these atoms whenever the web client needs a standard control or an agent-output renderer. They render through React only and take `--dsw-*` design tokens from the theme, so they fit any plugin without importing the theme or the slot system. diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md index f91435e44a..de8487499a 100644 --- a/packages/client/ui-primitives/README.zh.md +++ b/packages/client/ui-primitives/README.zh.md @@ -25,6 +25,8 @@ kind: "package-library" ## 使用本包 +本包是 Web 壳的构建输入。静态 ESM 为 Vite 保留第三方导入和样式;独立消费方自行提供开发依赖([依赖规则](../AGENTS.md#dependency-declaration))。 + 只要 Web 客户端需要标准控件或 agent 输出渲染器,就用这些原子组件拼装功能 UI。它们只经 React 渲染,并从主题取得 `--dsw-*` 设计 token,因此无需导入主题或 slot 系统即可适配任意插件。 From f39a37a523e312f1fdcfcd2bb4e846f3f389d7fb Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Tue, 8 Sep 2026 16:37:39 +0800 Subject: [PATCH 47/58] fix(client): keep browser dependencies out of production installs --- THIRD_PARTY_NOTICES.md | 8 +- apps/web/vite.config.ts | 6 + lefthook.yml | 2 +- packages/client/store/package.json | 8 +- packages/client/ui-attachment/package.json | 6 +- packages/client/ui-commands/package.json | 6 +- packages/client/ui-conversation/package.json | 16 +- .../ui-directory-picker-browse/package.json | 6 +- packages/client/ui-dockkit/package.json | 9 +- packages/client/ui-input-trigger/package.json | 6 +- .../client/ui-model-selection/package.json | 8 +- packages/client/ui-primitives/package.json | 44 +- packages/client/ui-renderer/package.json | 6 +- .../client/ui-settings-general/package.json | 6 +- .../client/ui-settings-plugins/package.json | 8 +- packages/client/ui-sidebar-files/package.json | 10 +- packages/client/ui-sidebar-right/package.json | 12 +- .../ui-sidebar-textpreview/package.json | 10 +- packages/client/ui-sidebar/package.json | 6 +- packages/client/ui-theme/package.json | 4 +- packages/client/ui-tool/package.json | 6 +- packages/client/ui-trajectory/package.json | 8 +- .../client/ui-user-questions/package.json | 6 +- packages/client/ui-workspace/package.json | 6 +- pnpm-lock.yaml | 220 +++++---- scripts/browser-bundled-externals.spec.ts | 127 ++++++ scripts/browser-bundled-externals.ts | 178 ++++++++ scripts/gen-third-party-notices.spec.ts | 39 +- scripts/gen-third-party-notices.ts | 61 +-- scripts/verify-npm-install-layout.spec.ts | 13 + scripts/verify-npm-install-layout.ts | 3 + scripts/verify-package-dependencies.spec.ts | 423 +++++++++++++++++- scripts/verify-package-dependencies.ts | 175 ++++++-- 33 files changed, 1151 insertions(+), 301 deletions(-) create mode 100644 scripts/browser-bundled-externals.spec.ts create mode 100644 scripts/browser-bundled-externals.ts diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 43c4263136..9004b1906f 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -27,7 +27,7 @@ The Cordis framework and its foundation libraries are source-vendored into this ## Runtime npm dependencies -External packages that a workspace package resolves at runtime. The tier covers every plugin a user can mount from `cordis.yml` — not only what the `dsh` CLI, Web UI, and Python SDK runtime load by default. +External packages installed for runtime use or distributed inside the prebuilt browser artifacts. Browser inputs are resolved through the shipping tsdown and Vite configurations, independently of npm dependency sections. The tier covers every plugin a user can mount from `cordis.yml` — not only what the `dsh` CLI, Web UI, and Python SDK runtime load by default. | Package | License | | --- | --- | @@ -55,7 +55,6 @@ External packages that a workspace package resolves at runtime. The tier covers | [`@shikijs/langs`](https://github.com/shikijs/shiki) | MIT | | [`@standard-schema/spec`](https://github.com/standard-schema/standard-schema) | MIT | | [`@tanstack/react-virtual`](https://github.com/TanStack/virtual) | MIT | -| [`@types/mdast`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@vscode/ripgrep`](https://github.com/microsoft/vscode-ripgrep) | MIT | | [`@xterm/headless`](https://github.com/xtermjs/xterm.js) | MIT | | [`@yarnpkg/parsers`](https://github.com/yarnpkg/berry) | BSD-2-Clause | @@ -88,7 +87,6 @@ External packages that a workspace package resolves at runtime. The tier covers | [`micromark-util-classify-character`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-classify-character) | MIT | | [`micromark-util-sanitize-uri`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-sanitize-uri) | MIT | | [`micromark-util-symbol`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-symbol) | MIT | -| [`micromark-util-types`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-types) | MIT | | [`mime-types`](https://github.com/jshttp/mime-types) | MIT | | [`negotiator`](https://github.com/jshttp/negotiator) | MIT | | [`node-addon-require-builtin`](https://www.npmjs.com/package/node-addon-require-builtin) | MIT | @@ -138,7 +136,7 @@ The installed SDK 0.3.263 declares the following optional platform packages. Eac ## Development-only npm dependencies -External packages **directly declared** only by repository tooling, test infrastructure, the documentation site, the demo leaves, or the native launcher's build workspace. No shipped surface names them itself. A package here may still be pulled in transitively by a runtime dependency — `pnpm-lock.yaml` is the authority on the full closure — so this tier records who declares a package, not what a build ultimately bundles. +External packages **directly declared** for development, tests, types, or tooling, without a runtime installation or browser-build relationship. A package here may still be pulled in transitively by a runtime dependency — `pnpm-lock.yaml` is the authority on that full closure. | Package | License | | --- | --- | @@ -155,6 +153,7 @@ External packages **directly declared** only by repository tooling, test infrast | [`@types/compression`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@types/js-yaml`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@types/jsdom`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | +| [`@types/mdast`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@types/mime-types`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@types/negotiator`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@types/node`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | @@ -190,6 +189,7 @@ External packages **directly declared** only by repository tooling, test infrast | [`lefthook`](https://github.com/evilmartians/lefthook) | MIT | | [`lightningcss`](https://github.com/parcel-bundler/lightningcss) | MPL-2.0 | | [`mermaid`](https://github.com/mermaid-js/mermaid) | MIT | +| [`micromark-util-types`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-types) | MIT | | [`msgpackr`](http://github.com/kriszyp/msgpackr) | MIT | | [`oxlint`](https://github.com/oxc-project/oxc) | MIT | | [`oxlint-tsgolint`](https://github.com/oxc-project/tsgolint) | MIT | diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index dff22a99ec..0e15b4f088 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -46,15 +46,21 @@ function rejectStandaloneServe(): Plugin { */ function emitPreviewPage(): Plugin { let bootstrapFile: string | undefined + let write = true return { name: 'dsh-emit-preview-page', + configResolved(config) { + write = config.build.write + }, generateBundle(_options, bundle) { + if (!write) return for (const item of Object.values(bundle)) { if (item.type === 'chunk' && item.isEntry && item.name === 'bootstrap') bootstrapFile = item.fileName } if (bootstrapFile === undefined) throw new Error('vite: preview bootstrap entry missing from the bundle') }, async closeBundle() { + if (!write) return // A build that failed before generateBundle has no page to splice. if (bootstrapFile === undefined) return const page = await readFile(src('./dist/index.html'), 'utf8') diff --git a/lefthook.yml b/lefthook.yml index 1c2a693c88..237202e7e0 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -28,7 +28,7 @@ pre-commit: # lefthook only inspects files present on disk — so that one case still # falls through to the freshness assertion in the test lane. - name: third-party notices (staged) - glob: '{package.json,*/package.json,*/*/package.json,*/*/*/package.json,*/*/*/*/package.json,pnpm-workspace.yaml,*/*/pnpm-workspace.yaml,pnpm-lock.yaml,vendor/README.md,python/*/pyproject.toml,scripts/gen-third-party-notices.ts,scripts/build-exe-for-python-sdk.ts}' + glob: '{package.json,*/package.json,*/*/package.json,*/*/*/package.json,*/*/*/*/package.json,pnpm-workspace.yaml,*/*/pnpm-workspace.yaml,pnpm-lock.yaml,vendor/README.md,python/*/pyproject.toml,scripts/gen-third-party-notices.ts,scripts/browser-bundled-externals.ts,scripts/build-exe-for-python-sdk.ts,tsconfig.base*.json,packages/*/*/src/**/*,packages/*/*/tsdown.config.ts,packages/client/tsdown.client.ts,apps/*/src/**/*,apps/*/vite.config.ts}' run: node_modules/.bin/tsx scripts/gen-third-party-notices.ts && git add THIRD_PARTY_NOTICES.md - name: whitespace (staged) diff --git a/packages/client/store/package.json b/packages/client/store/package.json index 172439a947..6bb0c0a212 100644 --- a/packages/client/store/package.json +++ b/packages/client/store/package.json @@ -22,15 +22,13 @@ "./package.json": "./package.json" }, "license": "MIT", - "dependencies": { - "immer": "^10.1.1", - "zustand": "~4.4.7" - }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:^" }, "devDependencies": { - "@deepseek-ai/cordis": "workspace:^" + "@deepseek-ai/cordis": "workspace:^", + "zustand": "~4.4.7", + "immer": "^10.1.1" }, "files": [ "lib/index.js", diff --git a/packages/client/ui-attachment/package.json b/packages/client/ui-attachment/package.json index 7679b48621..9adf41af13 100644 --- a/packages/client/ui-attachment/package.json +++ b/packages/client/ui-attachment/package.json @@ -42,9 +42,6 @@ "watch": "tsdown --watch" }, "license": "MIT", - "dependencies": { - "clsx": "^2.0.0" - }, "devDependencies": { "@deepseek-ai/cordis": "workspace:^", "@types/react": "~18.3.1", @@ -58,7 +55,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "@deepseek-ai/dsh-attachment": "workspace:^", - "@deepseek-ai/dsh-client-ui-tool": "workspace:^" + "@deepseek-ai/dsh-client-ui-tool": "workspace:^", + "clsx": "^2.0.0" }, "files": [ "lib/index.js", diff --git a/packages/client/ui-commands/package.json b/packages/client/ui-commands/package.json index bc7b069162..40ce5a3bff 100644 --- a/packages/client/ui-commands/package.json +++ b/packages/client/ui-commands/package.json @@ -41,9 +41,6 @@ "watch": "tsdown --watch" }, "license": "MIT", - "dependencies": { - "clsx": "^2.0.0" - }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:^" }, @@ -64,7 +61,8 @@ "@deepseek-ai/dsh-client-store": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-client-ui-renderer": "workspace:^", - "@deepseek-ai/dsh-client-ui-session": "workspace:^" + "@deepseek-ai/dsh-client-ui-session": "workspace:^", + "clsx": "^2.0.0" }, "files": [ "lib/index.js", diff --git a/packages/client/ui-conversation/package.json b/packages/client/ui-conversation/package.json index 8902063d3c..987aa9574f 100644 --- a/packages/client/ui-conversation/package.json +++ b/packages/client/ui-conversation/package.json @@ -46,13 +46,7 @@ }, "license": "MIT", "dependencies": { - "clsx": "^2.0.0", - "@deepseek-ai/schemastery": "workspace:^", - "@lexical/history": "^0.49.0", - "@lexical/plain-text": "^0.49.0", - "@lexical/text": "^0.49.0", - "@lexical/utils": "^0.49.0", - "lexical": "^0.49.0" + "@deepseek-ai/schemastery": "workspace:^" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:^" @@ -93,7 +87,13 @@ "react-dom": "^18.2.0", "@types/react-dom": "~18.3.0", "zod": "^4.4.3", - "@deepseek-ai/dsh-settings": "workspace:^" + "@deepseek-ai/dsh-settings": "workspace:^", + "lexical": "^0.49.0", + "clsx": "^2.0.0", + "@lexical/utils": "^0.49.0", + "@lexical/text": "^0.49.0", + "@lexical/plain-text": "^0.49.0", + "@lexical/history": "^0.49.0" }, "files": [ "lib/index.js", diff --git a/packages/client/ui-directory-picker-browse/package.json b/packages/client/ui-directory-picker-browse/package.json index aaf735a27f..ec0a9d8ed0 100644 --- a/packages/client/ui-directory-picker-browse/package.json +++ b/packages/client/ui-directory-picker-browse/package.json @@ -41,9 +41,6 @@ "watch": "tsdown --watch" }, "license": "MIT", - "dependencies": { - "clsx": "^2.0.0" - }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:^" }, @@ -59,7 +56,8 @@ "@types/react": "~18.3.1", "@deepseek-ai/cordis": "workspace:^", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "clsx": "^2.0.0" }, "files": [ "lib/index.js", diff --git a/packages/client/ui-dockkit/package.json b/packages/client/ui-dockkit/package.json index 8e7a97e4dd..968d1ba505 100644 --- a/packages/client/ui-dockkit/package.json +++ b/packages/client/ui-dockkit/package.json @@ -22,18 +22,15 @@ "./package.json": "./package.json" }, "license": "MIT", - "dependencies": { - "clsx": "^2.0.0", - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, "devDependencies": { "@deepseek-ai/cordis": "workspace:^", "@deepseek-ai/dsh-brand": "workspace:^", "@testing-library/react": "^16.1.0", "@types/react": "~18.3.1", "@types/react-dom": "~18.3.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react": "^18.2.0", + "clsx": "^2.0.0" }, "files": [ "lib/index.js", diff --git a/packages/client/ui-input-trigger/package.json b/packages/client/ui-input-trigger/package.json index a5d7b679d9..65e30abb16 100644 --- a/packages/client/ui-input-trigger/package.json +++ b/packages/client/ui-input-trigger/package.json @@ -41,9 +41,6 @@ "watch": "tsdown --watch" }, "license": "MIT", - "dependencies": { - "clsx": "^2.0.0" - }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:^" }, @@ -61,7 +58,8 @@ "@deepseek-ai/dsh-api-session-controller": "workspace:^", "@deepseek-ai/dsh-client-store": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", - "@deepseek-ai/dsh-client-ui-session": "workspace:^" + "@deepseek-ai/dsh-client-ui-session": "workspace:^", + "clsx": "^2.0.0" }, "files": [ "lib/index.js", diff --git a/packages/client/ui-model-selection/package.json b/packages/client/ui-model-selection/package.json index 4d87ef6ca0..d3c89480e8 100644 --- a/packages/client/ui-model-selection/package.json +++ b/packages/client/ui-model-selection/package.json @@ -63,14 +63,12 @@ "@deepseek-ai/dsh-client-store": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-client-ui-renderer": "workspace:^", - "@deepseek-ai/dsh-client-ui-session": "workspace:^" + "@deepseek-ai/dsh-client-ui-session": "workspace:^", + "clsx": "^2.1.1" }, "files": [ "lib/index.js", "lib/client.js", "lib/types/**/*.d.ts" - ], - "dependencies": { - "clsx": "^2.1.1" - } + ] } diff --git a/packages/client/ui-primitives/package.json b/packages/client/ui-primitives/package.json index 41fbdb4ebe..2613c3aa98 100644 --- a/packages/client/ui-primitives/package.json +++ b/packages/client/ui-primitives/package.json @@ -22,32 +22,30 @@ "./package.json": "./package.json" }, "license": "MIT", - "dependencies": { - "@shikijs/langs": "^4.3.1", - "@types/mdast": "^4.0.4", - "anser": "^2.3.5", - "clsx": "^2.0.0", - "katex": "^0.16.47", - "mdast-util-from-markdown": "^2.0.3", - "mdast-util-gfm": "^3.1.0", - "mdast-util-math": "^3.0.0", - "micromark-core-commonmark": "^2.0.3", - "micromark-extension-gfm": "^3.0.0", - "micromark-extension-math": "^3.1.0", - "micromark-factory-space": "^2.0.1", - "micromark-util-character": "^2.1.1", - "micromark-util-classify-character": "^2.0.1", - "micromark-util-sanitize-uri": "^2.0.1", - "micromark-util-symbol": "^2.0.1", - "micromark-util-types": "^2.0.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "shiki": "^4.3.1" - }, "devDependencies": { "@types/react": "~18.3.1", "@types/react-dom": "~18.3.0", - "@deepseek-ai/cordis": "workspace:^" + "@deepseek-ai/cordis": "workspace:^", + "react": "^18.2.0", + "clsx": "^2.0.0", + "react-dom": "^18.2.0", + "anser": "^2.3.5", + "katex": "^0.16.47", + "micromark-core-commonmark": "^2.0.3", + "micromark-util-character": "^2.1.1", + "micromark-util-classify-character": "^2.0.1", + "micromark-util-symbol": "^2.0.1", + "micromark-util-types": "^2.0.2", + "shiki": "^4.3.1", + "@shikijs/langs": "^4.3.1", + "@types/mdast": "^4.0.4", + "micromark-factory-space": "^2.0.1", + "micromark-extension-math": "^3.1.0", + "mdast-util-from-markdown": "^2.0.3", + "mdast-util-gfm": "^3.1.0", + "mdast-util-math": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "micromark-util-sanitize-uri": "^2.0.1" }, "files": [ "lib/index.js", diff --git a/packages/client/ui-renderer/package.json b/packages/client/ui-renderer/package.json index 32b616d5a2..2aefdb4b1e 100644 --- a/packages/client/ui-renderer/package.json +++ b/packages/client/ui-renderer/package.json @@ -40,9 +40,6 @@ "watch": "tsdown --watch" }, "license": "MIT", - "dependencies": { - "use-sync-external-store": "1.2.0" - }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:^" }, @@ -55,7 +52,8 @@ "@types/react-dom": "~18.3.0", "@types/use-sync-external-store": "^1.5.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "use-sync-external-store": "1.2.0" }, "files": [ "lib/index.js", diff --git a/packages/client/ui-settings-general/package.json b/packages/client/ui-settings-general/package.json index a31d2dad8d..2d33e995f4 100644 --- a/packages/client/ui-settings-general/package.json +++ b/packages/client/ui-settings-general/package.json @@ -43,8 +43,7 @@ }, "license": "MIT", "dependencies": { - "@deepseek-ai/schemastery": "workspace:^", - "clsx": "^2.0.0" + "@deepseek-ai/schemastery": "workspace:^" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:^" @@ -64,7 +63,8 @@ "react": "^18.2.0", "@deepseek-ai/dsh-client-ui-renderer": "workspace:^", "@deepseek-ai/dsh-client-ui-session": "workspace:^", - "@deepseek-ai/dsh-settings": "workspace:^" + "@deepseek-ai/dsh-settings": "workspace:^", + "clsx": "^2.0.0" }, "files": [ "lib/index.js", diff --git a/packages/client/ui-settings-plugins/package.json b/packages/client/ui-settings-plugins/package.json index 9f82713d90..c4b27fae88 100644 --- a/packages/client/ui-settings-plugins/package.json +++ b/packages/client/ui-settings-plugins/package.json @@ -54,14 +54,12 @@ "@deepseek-ai/dsh-client-ui-slots": "workspace:^", "@types/react": "~18.3.1", "react": "^18.2.0", - "@deepseek-ai/dsh-client-ui-renderer": "workspace:^" + "@deepseek-ai/dsh-client-ui-renderer": "workspace:^", + "clsx": "^2.0.0" }, "files": [ "lib/index.js", "lib/client.js", "lib/types/**/*.d.ts" - ], - "dependencies": { - "clsx": "^2.0.0" - } + ] } diff --git a/packages/client/ui-sidebar-files/package.json b/packages/client/ui-sidebar-files/package.json index 9e1a7f0663..ce97ee1ffc 100644 --- a/packages/client/ui-sidebar-files/package.json +++ b/packages/client/ui-sidebar-files/package.json @@ -41,11 +41,6 @@ "watch": "tsdown --watch" }, "license": "MIT", - "dependencies": { - "clsx": "^2.0.0", - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:^" }, @@ -66,7 +61,10 @@ "@deepseek-ai/dsh-util-workspace-path": "workspace:^", "@testing-library/react": "^16.1.0", "@types/react": "~18.3.1", - "@types/react-dom": "~18.3.0" + "@types/react-dom": "~18.3.0", + "react": "^18.2.0", + "clsx": "^2.0.0", + "react-dom": "^18.2.0" }, "files": [ "lib/index.js", diff --git a/packages/client/ui-sidebar-right/package.json b/packages/client/ui-sidebar-right/package.json index 699bebc01f..c8fad0215c 100644 --- a/packages/client/ui-sidebar-right/package.json +++ b/packages/client/ui-sidebar-right/package.json @@ -42,12 +42,6 @@ "watch": "tsdown --watch" }, "license": "MIT", - "dependencies": { - "clsx": "^2.0.0", - "picomatch": "^4.0.4", - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:^" }, @@ -69,7 +63,11 @@ "@testing-library/react": "^16.1.0", "@types/picomatch": "^4.0.2", "@types/react": "~18.3.1", - "@types/react-dom": "~18.3.0" + "@types/react-dom": "~18.3.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "picomatch": "^4.0.4", + "clsx": "^2.0.0" }, "files": [ "lib/index.js", diff --git a/packages/client/ui-sidebar-textpreview/package.json b/packages/client/ui-sidebar-textpreview/package.json index 756b6a3dab..7801a14e1d 100644 --- a/packages/client/ui-sidebar-textpreview/package.json +++ b/packages/client/ui-sidebar-textpreview/package.json @@ -41,11 +41,6 @@ "watch": "tsdown --watch" }, "license": "MIT", - "dependencies": { - "clsx": "^2.0.0", - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:^" }, @@ -67,7 +62,10 @@ "@deepseek-ai/dsh-util-workspace-path": "workspace:^", "@testing-library/react": "^16.1.0", "@types/react": "~18.3.1", - "@types/react-dom": "~18.3.0" + "@types/react-dom": "~18.3.0", + "react": "^18.2.0", + "clsx": "^2.0.0", + "react-dom": "^18.2.0" }, "files": [ "lib/index.js", diff --git a/packages/client/ui-sidebar/package.json b/packages/client/ui-sidebar/package.json index a6f1b9d4a3..7c1d5531fd 100644 --- a/packages/client/ui-sidebar/package.json +++ b/packages/client/ui-sidebar/package.json @@ -43,9 +43,6 @@ "watch": "tsdown --watch" }, "license": "MIT", - "dependencies": { - "clsx": "^2.0.0" - }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:^" }, @@ -61,7 +58,8 @@ "@deepseek-ai/dsh-client-ui-workspace": "workspace:^", "@types/react": "~18.3.1", "@deepseek-ai/cordis": "workspace:^", - "react": "^18.2.0" + "react": "^18.2.0", + "clsx": "^2.0.0" }, "files": [ "lib/index.js", diff --git a/packages/client/ui-theme/package.json b/packages/client/ui-theme/package.json index b642fe86a4..5f9dd4940c 100644 --- a/packages/client/ui-theme/package.json +++ b/packages/client/ui-theme/package.json @@ -56,7 +56,8 @@ "@deepseek-ai/dsh-host-webserver": "workspace:^", "@types/react": "~18.3.1", "react": "^18.2.0", - "@deepseek-ai/dsh-settings": "workspace:^" + "@deepseek-ai/dsh-settings": "workspace:^", + "clsx": "^2.0.0" }, "files": [ "lib/index.js", @@ -69,7 +70,6 @@ "watch": "tsdown --watch" }, "dependencies": { - "clsx": "^2.0.0", "@deepseek-ai/schemastery": "workspace:^" } } diff --git a/packages/client/ui-tool/package.json b/packages/client/ui-tool/package.json index 453d7867bb..3a3722d64d 100644 --- a/packages/client/ui-tool/package.json +++ b/packages/client/ui-tool/package.json @@ -41,9 +41,6 @@ "watch": "tsdown --watch" }, "license": "MIT", - "dependencies": { - "clsx": "^2.0.0" - }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:^" }, @@ -73,7 +70,8 @@ "@deepseek-ai/dsh-client-ui-renderer": "workspace:^", "@deepseek-ai/dsh-client-ui-session": "workspace:^", "@deepseek-ai/dsh-util-workspace-path": "workspace:^", - "@deepseek-ai/dsh-attachment": "workspace:^" + "@deepseek-ai/dsh-attachment": "workspace:^", + "clsx": "^2.0.0" }, "files": [ "lib/index.js", diff --git a/packages/client/ui-trajectory/package.json b/packages/client/ui-trajectory/package.json index 53965b1cb7..7f9752a6a8 100644 --- a/packages/client/ui-trajectory/package.json +++ b/packages/client/ui-trajectory/package.json @@ -42,10 +42,6 @@ "watch": "tsdown --watch" }, "license": "MIT", - "dependencies": { - "@tanstack/react-virtual": "^3.14.9", - "diff": "^9.0.0" - }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:^" }, @@ -70,7 +66,9 @@ "@deepseek-ai/dsh-client-ui-renderer": "workspace:^", "@deepseek-ai/dsh-client-ui-session": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", - "@deepseek-ai/dsh-attachment": "workspace:^" + "@deepseek-ai/dsh-attachment": "workspace:^", + "@tanstack/react-virtual": "^3.14.9", + "diff": "^9.0.0" }, "files": [ "lib/index.js", diff --git a/packages/client/ui-user-questions/package.json b/packages/client/ui-user-questions/package.json index 58b6a1c836..b32352c522 100644 --- a/packages/client/ui-user-questions/package.json +++ b/packages/client/ui-user-questions/package.json @@ -43,9 +43,6 @@ "watch": "tsdown --watch" }, "license": "MIT", - "dependencies": { - "clsx": "^2.0.0" - }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:^" }, @@ -68,7 +65,8 @@ "react": "^18.2.0", "@deepseek-ai/dsh-client-ui-conversation": "workspace:^", "@deepseek-ai/dsh-client-connection": "workspace:^", - "@deepseek-ai/dsh-session": "workspace:^" + "@deepseek-ai/dsh-session": "workspace:^", + "clsx": "^2.0.0" }, "files": [ "lib/index.js", diff --git a/packages/client/ui-workspace/package.json b/packages/client/ui-workspace/package.json index 72876b6d9e..fc4c8d2855 100644 --- a/packages/client/ui-workspace/package.json +++ b/packages/client/ui-workspace/package.json @@ -46,9 +46,6 @@ "watch": "tsdown --watch" }, "license": "MIT", - "dependencies": { - "clsx": "^2.0.0" - }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:^" }, @@ -72,7 +69,8 @@ "@deepseek-ai/dsh-util-workspace-path": "workspace:^", "@types/react": "~18.3.1", "@deepseek-ai/cordis": "workspace:^", - "react": "^18.2.0" + "react": "^18.2.0", + "clsx": "^2.0.0" }, "files": [ "lib/index.js", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c3122497b..78a9c7877a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2192,17 +2192,16 @@ importers: version: link:../../typert/protocol packages/client/store: - dependencies: + devDependencies: + '@deepseek-ai/cordis': + specifier: workspace:^ + version: link:../../../vendor/cordis immer: specifier: ^10.1.1 version: 10.2.0 zustand: specifier: ~4.4.7 version: 4.4.7(@types/react@18.3.31)(immer@10.2.0)(react@18.3.1) - devDependencies: - '@deepseek-ai/cordis': - specifier: workspace:^ - version: link:../../../vendor/cordis packages/client/ui-agent-preset: devDependencies: @@ -2307,10 +2306,6 @@ importers: version: 18.3.1 packages/client/ui-attachment: - dependencies: - clsx: - specifier: ^2.0.0 - version: 2.1.1 devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -2345,6 +2340,9 @@ importers: '@types/react-dom': specifier: ~18.3.0 version: 18.3.7(@types/react@18.3.31) + clsx: + specifier: ^2.0.0 + version: 2.1.1 react: specifier: ^18.2.0 version: 18.3.1 @@ -2492,10 +2490,6 @@ importers: version: 18.3.1(react@18.3.1) packages/client/ui-commands: - dependencies: - clsx: - specifier: ^2.0.0 - version: 2.1.1 devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -2545,6 +2539,9 @@ importers: '@types/react': specifier: ~18.3.1 version: 18.3.31 + clsx: + specifier: ^2.0.0 + version: 2.1.1 react: specifier: ^18.2.0 version: 18.3.1 @@ -2554,24 +2551,6 @@ importers: '@deepseek-ai/schemastery': specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery - '@lexical/history': - specifier: ^0.49.0 - version: 0.49.0(typescript@6.0.3) - '@lexical/plain-text': - specifier: ^0.49.0 - version: 0.49.0(typescript@6.0.3) - '@lexical/text': - specifier: ^0.49.0 - version: 0.49.0(typescript@6.0.3) - '@lexical/utils': - specifier: ^0.49.0 - version: 0.49.0(typescript@6.0.3) - clsx: - specifier: ^2.0.0 - version: 2.1.1 - lexical: - specifier: ^0.49.0 - version: 0.49.0(typescript@6.0.3) devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -2666,12 +2645,30 @@ importers: '@lexical/headless': specifier: ^0.49.0 version: 0.49.0(typescript@6.0.3) + '@lexical/history': + specifier: ^0.49.0 + version: 0.49.0(typescript@6.0.3) + '@lexical/plain-text': + specifier: ^0.49.0 + version: 0.49.0(typescript@6.0.3) + '@lexical/text': + specifier: ^0.49.0 + version: 0.49.0(typescript@6.0.3) + '@lexical/utils': + specifier: ^0.49.0 + version: 0.49.0(typescript@6.0.3) '@types/react': specifier: ~18.3.1 version: 18.3.31 '@types/react-dom': specifier: ~18.3.0 version: 18.3.7(@types/react@18.3.31) + clsx: + specifier: ^2.0.0 + version: 2.1.1 + lexical: + specifier: ^0.49.0 + version: 0.49.0(typescript@6.0.3) react: specifier: ^18.2.0 version: 18.3.1 @@ -2731,10 +2728,6 @@ importers: version: 18.3.1 packages/client/ui-directory-picker-browse: - dependencies: - clsx: - specifier: ^2.0.0 - version: 2.1.1 devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -2766,6 +2759,9 @@ importers: '@types/react': specifier: ~18.3.1 version: 18.3.31 + clsx: + specifier: ^2.0.0 + version: 2.1.1 react: specifier: ^18.2.0 version: 18.3.1 @@ -2798,16 +2794,6 @@ importers: version: 18.3.1(react@18.3.1) packages/client/ui-dockkit: - dependencies: - clsx: - specifier: ^2.0.0 - version: 2.1.1 - react: - specifier: ^18.2.0 - version: 18.3.1 - react-dom: - specifier: ^18.2.0 - version: 18.3.1(react@18.3.1) devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -2824,6 +2810,15 @@ importers: '@types/react-dom': specifier: ~18.3.0 version: 18.3.7(@types/react@18.3.31) + clsx: + specifier: ^2.0.0 + version: 2.1.1 + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) packages/client/ui-goal: devDependencies: @@ -2886,10 +2881,6 @@ importers: version: 18.3.1(react@18.3.1) packages/client/ui-input-trigger: - dependencies: - clsx: - specifier: ^2.0.0 - version: 2.1.1 devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -2930,6 +2921,9 @@ importers: '@types/react': specifier: ~18.3.1 version: 18.3.31 + clsx: + specifier: ^2.0.0 + version: 2.1.1 react: specifier: ^18.2.0 version: 18.3.1 @@ -3061,10 +3055,6 @@ importers: version: 18.3.1(react@18.3.1) packages/client/ui-model-selection: - dependencies: - clsx: - specifier: ^2.1.1 - version: 2.1.1 devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -3117,6 +3107,9 @@ importers: '@types/react-dom': specifier: ~18.3.0 version: 18.3.7(@types/react@18.3.31) + clsx: + specifier: ^2.1.1 + version: 2.1.1 react: specifier: ^18.2.0 version: 18.3.1 @@ -3263,13 +3256,22 @@ importers: version: 18.3.1 packages/client/ui-primitives: - dependencies: + devDependencies: + '@deepseek-ai/cordis': + specifier: workspace:^ + version: link:../../../vendor/cordis '@shikijs/langs': specifier: ^4.3.1 version: 4.3.1 '@types/mdast': specifier: ^4.0.4 version: 4.0.4 + '@types/react': + specifier: ~18.3.1 + version: 18.3.31 + '@types/react-dom': + specifier: ~18.3.0 + version: 18.3.7(@types/react@18.3.31) anser: specifier: ^2.3.5 version: 2.3.5 @@ -3324,16 +3326,6 @@ importers: shiki: specifier: ^4.3.1 version: 4.3.1 - devDependencies: - '@deepseek-ai/cordis': - specifier: workspace:^ - version: link:../../../vendor/cordis - '@types/react': - specifier: ~18.3.1 - version: 18.3.31 - '@types/react-dom': - specifier: ~18.3.0 - version: 18.3.7(@types/react@18.3.31) packages/client/ui-reference: devDependencies: @@ -3378,10 +3370,6 @@ importers: version: link:../../util/workspace-path packages/client/ui-renderer: - dependencies: - use-sync-external-store: - specifier: 1.2.0 - version: 1.2.0(react@18.3.1) devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -3410,6 +3398,9 @@ importers: react-dom: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) + use-sync-external-store: + specifier: 1.2.0 + version: 1.2.0(react@18.3.1) packages/client/ui-schedule: devDependencies: @@ -3527,9 +3518,6 @@ importers: '@deepseek-ai/schemastery': specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery - clsx: - specifier: ^2.0.0 - version: 2.1.1 devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -3573,6 +3561,9 @@ importers: '@types/react': specifier: ~18.3.1 version: 18.3.31 + clsx: + specifier: ^2.0.0 + version: 2.1.1 react: specifier: ^18.2.0 version: 18.3.1 @@ -3662,10 +3653,6 @@ importers: version: 18.3.1(react@18.3.1) packages/client/ui-settings-plugins: - dependencies: - clsx: - specifier: ^2.0.0 - version: 2.1.1 devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -3697,15 +3684,14 @@ importers: '@types/react': specifier: ~18.3.1 version: 18.3.31 + clsx: + specifier: ^2.0.0 + version: 2.1.1 react: specifier: ^18.2.0 version: 18.3.1 packages/client/ui-sidebar: - dependencies: - clsx: - specifier: ^2.0.0 - version: 2.1.1 devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -3740,21 +3726,14 @@ importers: '@types/react': specifier: ~18.3.1 version: 18.3.31 - react: - specifier: ^18.2.0 - version: 18.3.1 - - packages/client/ui-sidebar-files: - dependencies: clsx: specifier: ^2.0.0 version: 2.1.1 react: specifier: ^18.2.0 version: 18.3.1 - react-dom: - specifier: ^18.2.0 - version: 18.3.1(react@18.3.1) + + packages/client/ui-sidebar-files: devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -3807,21 +3786,17 @@ importers: '@types/react-dom': specifier: ~18.3.0 version: 18.3.7(@types/react@18.3.31) - - packages/client/ui-sidebar-right: - dependencies: clsx: specifier: ^2.0.0 version: 2.1.1 - picomatch: - specifier: ^4.0.4 - version: 4.0.4 react: specifier: ^18.2.0 version: 18.3.1 react-dom: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) + + packages/client/ui-sidebar-right: devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -3877,18 +3852,20 @@ importers: '@types/react-dom': specifier: ~18.3.0 version: 18.3.7(@types/react@18.3.31) - - packages/client/ui-sidebar-textpreview: - dependencies: clsx: specifier: ^2.0.0 version: 2.1.1 + picomatch: + specifier: ^4.0.4 + version: 4.0.4 react: specifier: ^18.2.0 version: 18.3.1 react-dom: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) + + packages/client/ui-sidebar-textpreview: devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -3944,6 +3921,15 @@ importers: '@types/react-dom': specifier: ~18.3.0 version: 18.3.7(@types/react@18.3.31) + clsx: + specifier: ^2.0.0 + version: 2.1.1 + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) packages/client/ui-skill: devDependencies: @@ -4070,9 +4056,6 @@ importers: '@deepseek-ai/schemastery': specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery - clsx: - specifier: ^2.0.0 - version: 2.1.1 devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -4113,15 +4096,14 @@ importers: '@types/react': specifier: ~18.3.1 version: 18.3.31 + clsx: + specifier: ^2.0.0 + version: 2.1.1 react: specifier: ^18.2.0 version: 18.3.1 packages/client/ui-tool: - dependencies: - clsx: - specifier: ^2.0.0 - version: 2.1.1 devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -4195,6 +4177,9 @@ importers: '@types/react': specifier: ~18.3.1 version: 18.3.31 + clsx: + specifier: ^2.0.0 + version: 2.1.1 react: specifier: ^18.2.0 version: 18.3.1 @@ -4203,13 +4188,6 @@ importers: version: 18.3.1(react@18.3.1) packages/client/ui-trajectory: - dependencies: - '@tanstack/react-virtual': - specifier: ^3.14.9 - version: 3.14.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - diff: - specifier: ^9.0.0 - version: 9.0.0 devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -4262,12 +4240,18 @@ importers: '@deepseek-ai/dsh-tools': specifier: workspace:^ version: link:../../core/tools + '@tanstack/react-virtual': + specifier: ^3.14.9 + version: 3.14.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/react': specifier: ~18.3.1 version: 18.3.31 '@types/react-dom': specifier: ~18.3.0 version: 18.3.7(@types/react@18.3.31) + diff: + specifier: ^9.0.0 + version: 9.0.0 react: specifier: ^18.2.0 version: 18.3.1 @@ -4276,10 +4260,6 @@ importers: version: 18.3.1(react@18.3.1) packages/client/ui-user-questions: - dependencies: - clsx: - specifier: ^2.0.0 - version: 2.1.1 devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -4335,6 +4315,9 @@ importers: '@types/react': specifier: ~18.3.1 version: 18.3.31 + clsx: + specifier: ^2.0.0 + version: 2.1.1 react: specifier: ^18.2.0 version: 18.3.1 @@ -4391,10 +4374,6 @@ importers: version: 18.3.1 packages/client/ui-workspace: - dependencies: - clsx: - specifier: ^2.0.0 - version: 2.1.1 devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -4453,6 +4432,9 @@ importers: '@types/react': specifier: ~18.3.1 version: 18.3.31 + clsx: + specifier: ^2.0.0 + version: 2.1.1 react: specifier: ^18.2.0 version: 18.3.1 diff --git a/scripts/browser-bundled-externals.spec.ts b/scripts/browser-bundled-externals.spec.ts new file mode 100644 index 0000000000..b4d95cf96d --- /dev/null +++ b/scripts/browser-bundled-externals.spec.ts @@ -0,0 +1,127 @@ +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join, resolve } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { browserBundledExternals, browserPackageOfFile, browserSourceAliases } from './browser-bundled-externals.ts' + +const roots: string[] = [] +const repositoryRoot = resolve(import.meta.dirname, '..') + +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) +}) + +function fixture(): string { + const root = mkdtempSync(join(tmpdir(), 'dsh-browser-notices-')) + roots.push(root) + write(root, 'package.json', '{"type":"module"}') + write(root, 'tsconfig.base.json', JSON.stringify({ + compilerOptions: { + module: 'esnext', target: 'es2022', jsx: 'react-jsx', + paths: { '@fixture/static': ['./packages/client/static/src/index.ts'] }, + }, + })) + write(root, 'tsconfig.base.client.json', '{"extends":"./tsconfig.base.json"}') + return root +} + +function write(root: string, path: string, text: string): void { + const target = join(root, path) + mkdirSync(dirname(target), { recursive: true }) + writeFileSync(target, text) +} + +function library(root: string, name: string, source = 'export const value = 1'): void { + write(root, `node_modules/${name}/package.json`, JSON.stringify({ name, type: 'module', exports: './index.js' })) + write(root, `node_modules/${name}/index.js`, source) +} + +function dynamicPlugin(root: string, source: string): void { + write(root, 'packages/client/dynamic/package.json', JSON.stringify({ + name: '@fixture/dynamic', dsh: { client: { platform: 'web' } }, + })) + write(root, 'packages/client/dynamic/tsdown.config.ts', [ + 'export default {', + ' name: "@fixture/dynamic/client",', + ' entry: { client: "src/client/index.ts" },', + ' inputOptions: { resolve: { conditionNames: ["browser", "import", "default"] } },', + '}', + ].join('\n')) + write(root, 'packages/client/dynamic/src/client/index.ts', source) +} + +describe('browser dependency discovery', () => { + it('records runtime imports through the client config without following upstream dependencies or erased types', async () => { + const root = fixture() + library(root, 'browser-lib', 'export { value } from "transitive-lib"') + library(root, 'transitive-lib') + dynamicPlugin(root, [ + 'import type { MissingType } from "type-only-lib"', + 'import { value } from "browser-lib"', + 'export const output: MissingType = value', + ].join('\n')) + + expect(await browserBundledExternals(root)).toEqual(new Set(['browser-lib'])) + expect(existsSync(join(root, 'packages/client/dynamic/lib'))).toBe(false) + }) + + it('rejects an unresolved runtime library', async () => { + const root = fixture() + dynamicPlugin(root, 'export { value } from "missing-browser-lib"') + await expect(browserBundledExternals(root)).rejects.toThrow('cannot resolve missing-browser-lib') + }) + + it('rejects a declared client without its browser config', async () => { + const root = fixture() + dynamicPlugin(root, 'export const value = 1') + write(root, 'packages/client/dynamic/tsdown.config.ts', 'export default { name: "@fixture/dynamic", entry: "src/index.ts" }') + await expect(browserBundledExternals(root)).rejects.toThrow('has no browser build config') + }) + + it('follows shell workspace aliases, CSS assets and lazy imports without writing output', async () => { + const root = fixture() + library(root, 'shell-lib') + library(root, 'lazy-lib') + library(root, 'asset-lib') + write(root, 'node_modules/asset-lib/package.json', JSON.stringify({ + name: 'asset-lib', exports: { './theme.css': './theme.css' }, + })) + write(root, 'node_modules/asset-lib/theme.css', '.fixture { color: red }') + write(root, 'packages/client/static/package.json', '{"name":"@fixture/static"}') + write(root, 'packages/client/static/src/index.ts', [ + 'import { value } from "shell-lib"', + 'import "asset-lib/theme.css"', + 'export const output = value', + 'export const lazy = () => import("lazy-lib")', + ].join('\n')) + const app = join(root, 'apps/web') + write(root, 'apps/web/package.json', '{"name":"@fixture/web","type":"module","exports":{"./dist/*":"./dist/*"}}') + symlinkSync(resolve(repositoryRoot, 'apps/web/node_modules'), join(app, 'node_modules'), 'junction') + write(root, 'apps/web/index.html', '') + write(root, 'apps/web/main.ts', 'import { output, lazy } from "@fixture/static"; console.log(output); void lazy()') + write(root, 'apps/web/vite.config.ts', `export default { + build: { rollupOptions: { input: { index: ${JSON.stringify(join(app, 'index.html'))}, preview: "missing-preview.ts" } } } + }`) + write(root, 'apps/web/dist/sentinel.txt', 'untouched') + + expect(await browserBundledExternals(root)).toEqual(new Set(['shell-lib', 'lazy-lib', 'asset-lib'])) + expect(readFileSync(join(app, 'dist/sentinel.txt'), 'utf8')).toBe('untouched') + expect(existsSync(join(app, 'dist/index.html'))).toBe(false) + expect(existsSync(join(root, 'packages/client/static/lib'))).toBe(false) + }) + + it('normalizes installed module ids and excludes virtual/workspace modules', () => { + expect(browserPackageOfFile('/repo/node_modules/.pnpm/pkg@1/node_modules/pkg/a.js')).toBe('pkg') + expect(browserPackageOfFile('C:\\repo\\node_modules\\@scope\\pkg\\a.css?url')).toBe('@scope/pkg') + expect(browserPackageOfFile('/repo/packages/client/a.ts')).toBeUndefined() + expect(browserPackageOfFile('\0vite/modulepreload-polyfill')).toBeUndefined() + }) + + it('maps exact names and subpaths from the source facade', () => { + const root = fixture() + const aliases = browserSourceAliases(root) + expect('@fixture/static'.replace(aliases[0]!.find, aliases[0]!.replacement)) + .toBe(join(root, 'packages/client/static/src/index.ts')) + expect(aliases[0]!.find.test('@fixture/static-extra')).toBe(false) + }) +}) diff --git a/scripts/browser-bundled-externals.ts b/scripts/browser-bundled-externals.ts new file mode 100644 index 0000000000..85053be30f --- /dev/null +++ b/scripts/browser-bundled-externals.ts @@ -0,0 +1,178 @@ +/** Resolve direct third-party browser inputs through the shipping build configurations, without emitting files. */ + +import { globSync, readFileSync } from 'node:fs' +import { createRequire } from 'node:module' +import { dirname, resolve } from 'node:path' +import { pathToFileURL } from 'node:url' +import { Rolldown, type UserConfigExport } from 'tsdown' +import ts from 'typescript' + +interface Manifest { + name: string + private?: boolean + dsh?: { client?: unknown } + exports?: Record +} + +interface ResolveContext { + resolve(source: string, importer: string, options: { skipSelf: boolean }): Promise<{ id: string } | null> +} + +/** + * Name of the installed package owning a bundler-resolved file. + * @param file - Resolved module or asset id, including any loader query. + * @returns Package name, or undefined for workspace and virtual modules. + */ +export function browserPackageOfFile(file: string): string | undefined { + const normalized = file.replaceAll('\\', '/') + const marker = normalized.lastIndexOf('/node_modules/') + if (marker < 0) return undefined + const parts = normalized.slice(marker + '/node_modules/'.length).split('/') + return parts[0]?.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0] +} + +function recorder(seen: Set, workspaceNames: ReadonlySet, followWorkspace = false) { + return { + name: 'dsh-browser-direct-dependencies', + enforce: 'pre' as const, + resolveId: { + order: 'pre' as const, + async handler(this: ResolveContext, source: string, importer: string | undefined) { + if (importer === undefined || source.startsWith('.') || source.startsWith('/') + || source.startsWith('\0') || source.startsWith('node:')) return null + const parts = source.split('/') + const name = source.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0] + if (name !== undefined && workspaceNames.has(name)) { + return followWorkspace ? null : { id: source, external: true } + } + const resolved = await this.resolve(source, importer, { skipSelf: true }) + if (resolved === null) throw new Error(`browser notices: cannot resolve ${source} from ${importer}`) + const owner = browserPackageOfFile(resolved.id) + if (owner === undefined) return resolved + if (browserPackageOfFile(importer) === undefined) seen.add(owner) + // Notices disclose direct dependencies; upstream implementation imports stay in the lockfile. + return { id: source, external: true } + }, + }, + } +} + +function readManifest(path: string): Manifest { + return JSON.parse(readFileSync(path, 'utf8')) as Manifest +} + +/** + * Source aliases shared with the repository's source-plane TypeScript programs. + * @param root - Repository root containing tsconfig.base.json. + * @returns Exact and wildcard aliases for the Vite dependency walk. + */ +export function browserSourceAliases(root: string): { find: RegExp; replacement: string }[] { + const path = resolve(root, 'tsconfig.base.json') + const config = ts.readConfigFile(path, file => ts.sys.readFile(file)) + if (config.error !== undefined) throw new Error(ts.flattenDiagnosticMessageText(config.error.messageText, '\n')) + const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, root) + return Object.entries(parsed.options.paths ?? {}).map(([name, targets]) => { + const target = targets[0] + if (target === undefined) throw new Error(`browser notices: ${name} has no source target in ${path}`) + const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace('\\*', '(.*)') + return { find: new RegExp(`^${escaped}$`), replacement: resolve(root, target).replace('*', '$1') } + }) +} + +async function collectClientBundles( + root: string, + manifests: ReadonlyMap, + workspaceNames: ReadonlySet, + seen: Set, +): Promise { + for (const [manifestPath, manifest] of manifests) { + if (manifest.private === true || manifest.dsh?.client === undefined) continue + const dir = dirname(manifestPath) + const loaded = await import(pathToFileURL(resolve(dir, 'tsdown.config.ts')).href) as { default: UserConfigExport } + const factory = await loaded.default + const configured = typeof factory === 'function' ? await factory({ env: {} }, { ci: false }) : factory + const configs = Array.isArray(configured) ? configured : [configured] + const client = configs.find(config => config.name === `${manifest.name}/client`) + if (client === undefined) throw new Error(`browser notices: ${manifest.name} has no browser build config`) + if (typeof client.inputOptions === 'function') throw new Error(`browser notices: ${manifest.name} needs resolved input options`) + const bundle = await Rolldown.rolldown({ + ...client.inputOptions, + cwd: dir, + input: client.entry as Rolldown.InputOption, + platform: 'browser', + transform: client.define === undefined ? {} : { define: client.define }, + plugins: [recorder(seen, workspaceNames), client.plugins ?? []] as NonNullable, + tsconfig: resolve(root, 'tsconfig.base.client.json'), + }) + try { + await bundle.generate({ format: 'cjs', sourcemap: false }) + } finally { + await bundle.close() + } + } +} + +interface ShellConfig { + build: { rollupOptions?: { input?: string | string[] | Record } } +} + +interface ViteApi { + resolveConfig(config: Record, command: 'build'): Promise + build(config: Record): Promise +} + +async function collectShell( + root: string, + workspaceNames: ReadonlySet, + seen: Set, +): Promise { + for (const path of globSync('apps/*/vite.config.ts', { cwd: root }).sort()) { + const dir = dirname(resolve(root, path)) + const manifest = readManifest(resolve(dir, 'package.json')) + if (manifest.private === true || manifest.exports?.['./dist/*'] === undefined) continue + const vitePath = createRequire(resolve(dir, 'package.json')).resolve('vite') + const vite = await import(pathToFileURL(vitePath).href) as ViteApi + const config = await vite.resolveConfig({ root: dir, logLevel: 'error' }, 'build') + const input = config.build.rollupOptions?.input + const entries = typeof input === 'string' ? [input] : Object.values(input ?? {}) + const pages = entries.filter(entry => entry.endsWith('.html')) + if (pages.length === 0) throw new Error(`browser notices: ${manifest.name} has no HTML build entry`) + await vite.build({ + root: dir, + logLevel: 'error', + plugins: [recorder(seen, workspaceNames, true)], + resolve: { alias: browserSourceAliases(root) }, + build: { + write: false, + minify: false, + sourcemap: false, + reportCompressedSize: false, + rollupOptions: { + input: pages.length === 1 ? pages[0] : pages, + // Chunk coloring expects full third-party bodies; the disclosure walk stops at their imports. + output: { manualChunks: () => undefined }, + }, + }, + }) + } +} + +/** + * Direct third-party packages resolved by published browser builds. + * @param root - Repository root with installed build dependencies; lib/ is not required. + * @returns Names of distributed browser inputs, excluding workspace packages and erased types. + */ +export async function browserBundledExternals(root: string): Promise> { + const manifests = new Map() + for (const glob of ['packages/*/*/package.json', 'vendor/*/package.json']) { + for (const path of globSync(glob, { cwd: root }).sort()) { + const absolute = resolve(root, path) + manifests.set(absolute, readManifest(absolute)) + } + } + const names = new Set([...manifests.values()].map(manifest => manifest.name)) + const seen = new Set() + await collectClientBundles(root, manifests, names, seen) + await collectShell(root, names, seen) + return seen +} diff --git a/scripts/gen-third-party-notices.spec.ts b/scripts/gen-third-party-notices.spec.ts index 7bf3f88b18..fabce6e8c9 100644 --- a/scripts/gen-third-party-notices.spec.ts +++ b/scripts/gen-third-party-notices.spec.ts @@ -4,6 +4,7 @@ import { tmpdir } from 'node:os' import { describe, expect, it } from 'vitest' import { CLAUDE_AGENT_SDK_PACKAGE, + assertRuntimeLicenses, claudeDistributionFromManifest, collectPythonDependencies, isOwnerAuthorizedRuntime, @@ -24,8 +25,11 @@ describe('THIRD_PARTY_NOTICES.md', () => { // already runs in the test lane, so the check costs no extra CI process. // Pre-commit regenerates the file whenever a manifest is staged, so reaching // this assertion means the notices were committed without that hook. - it('matches what the generator produces from the current manifests', () => { - const generated = render() + // This case resolves all browser build graphs as well as installed license metadata. + it('matches what the generator produces from the current manifests', { + timeout: 120_000, + }, async () => { + const generated = await render() expect(generated).toContain('It depends on the third-party software listed below.') expect(readFileSync(resolve(root, 'THIRD_PARTY_NOTICES.md'), 'utf8'), 'stale notices — run `pnpm run gen-third-party-notices`').toBe(generated) }) @@ -42,6 +46,37 @@ function workspace(entries: Record): { manifests: Map { + it('keeps license rejection active when a browser library is declared for development', () => { + const { manifests, names } = workspace({ + 'packages/client/ui/package.json': { devDependencies: { 'browser-lib': '^1', 'test-tool': '^1' } }, + }) + const tiers = tierExternalDeps(manifests, names, new Set(['browser-lib'])) + const dependencies = [{ name: 'browser-lib', license: 'GPL-3.0-only' }, { name: 'test-tool', license: 'GPL-3.0-only' }] + .filter(dep => tiers.get(dep.name)) + expect(dependencies.map(dep => dep.name)).toEqual(['browser-lib']) + expect(() => { assertRuntimeLicenses(dependencies) }).toThrow('browser-lib (GPL-3.0-only)') + expect(() => { assertRuntimeLicenses([{ name: 'browser-lib', license: 'MIT' }]) }).not.toThrow() + expect(() => { assertRuntimeLicenses([{ name: CLAUDE_AGENT_SDK_PACKAGE, license: 'SEE LICENSE IN README.md' }]) }) + .not.toThrow() + }) + + it('keeps browser-bundled development dependencies in runtime disclosures', () => { + const { manifests, names } = workspace({ + 'packages/client/ui/package.json': { + name: '@fixture/ui', devDependencies: { react: '^18', 'browser-lib': '^1', 'type-only': '^1' }, + }, + }) + expect(tierExternalDeps(manifests, names, new Set(['react', 'browser-lib']))).toEqual(new Map([ + ['tsx', true], ['react', true], ['browser-lib', true], ['type-only', false], + ])) + }) + + it('rejects a browser library missing from the disclosed declarations', () => { + const { manifests, names } = workspace({}) + expect(() => tierExternalDeps(manifests, names, new Set(['missing-lib']))) + .toThrow('browser package missing-lib has no workspace dependency declaration') + }) + it('tiers by declaring area, not by the declaring section name', () => { const { manifests, names } = workspace({ // Root tooling and test infrastructure never ship, whichever section declares them. diff --git a/scripts/gen-third-party-notices.ts b/scripts/gen-third-party-notices.ts index b9bf11e182..9e83956e5e 100644 --- a/scripts/gen-third-party-notices.ts +++ b/scripts/gen-third-party-notices.ts @@ -13,6 +13,7 @@ import { dirname, resolve } from 'node:path' import * as yaml from 'js-yaml' import { parse as parseToml, type TomlTableWithoutBigInt, type TomlValueWithoutBigInt } from 'smol-toml' import parseSpdx from 'spdx-expression-parse' +import { browserBundledExternals } from './browser-bundled-externals.ts' const root = resolve(import.meta.dirname, '..') const OUT = 'THIRD_PARTY_NOTICES.md' @@ -371,14 +372,11 @@ function normalizeRepo(raw: string | undefined): string | undefined { } /** - * External npm dependencies, tiered by which workspace area declares them at - * runtime: a package is runtime when any manifest outside `DEV_ONLY_AREAS` - * names it in `dependencies`/`optionalDependencies`. A package declared only - * by tooling, test infrastructure, the website, or the demo leaves — whatever - * the declaring section is called — is development-only. + * Direct npm dependencies distributed through installed runtime libraries or + * browser builds. Tooling declarations alone do not imply distribution. */ -function collectNpmDeps(manifests: Map, names: Set): ExternalDep[] { - return [...tierExternalDeps(manifests, names)] +function collectNpmDeps(manifests: Map, names: Set, browser: ReadonlySet): ExternalDep[] { + return [...tierExternalDeps(manifests, names, browser)] .filter(([name]) => !FIRST_PARTY.has(name)) .sort(([a], [b]) => a.localeCompare(b)) .map(([name, runtime]) => ({ name, ...installedMetadata(name, manifests), runtime })) @@ -388,9 +386,12 @@ function collectNpmDeps(manifests: Map, names: Set): E * Tier every external dependency the workspace declares. * @param manifests - workspace manifests keyed by repository-relative path. * @param names - every workspace package name, which never counts as external. + * @param browser - Direct third-party packages resolved by the browser builds. * @returns each external package mapped to whether it is a runtime dependency. */ -export function tierExternalDeps(manifests: Map, names: Set): Map { +export function tierExternalDeps( + manifests: Map, names: Set, browser: ReadonlySet = new Set(), +): Map { const tiers = new Map() // `tsx` is runtime by fiat: the root source-run scripts execute through its ESM hook. tiers.set('tsx', true) @@ -399,11 +400,14 @@ export function tierExternalDeps(manifests: Map, names: Set !isPermissive(dep.license) && !isOwnerAuthorizedRuntime(dep.name)) + if (rejected.length > 0) { + throw new Error(`gen-third-party-notices: runtime ${rejected.map(dep => `${dep.name} (${dep.license})`).join(', ')} is not a permissive license; review the distribution terms and record the decision before regenerating.`) + } +} + /** * Render the sentence that isolates non-permissive development tooling, or * nothing at all when every development dependency is permissive. @@ -671,14 +687,15 @@ ${rows.join('\n')} /** * Render the complete notices document. - * @returns the exact bytes `THIRD_PARTY_NOTICES.md` must hold. + * @returns The exact bytes THIRD_PARTY_NOTICES.md must hold after resolving browser inputs. */ -export function render(): string { +export async function render(): Promise { + const browser = await browserBundledExternals(root) // The linked-manifest cache is keyed by name only, so it must not outlive // the manifests map it was resolved from; render() owns that single load. workspaceLinkedManifestCache.clear() const { manifests, names } = loadWorkspaceManifests() - const npm = collectNpmDeps(manifests, names) + const npm = collectNpmDeps(manifests, names, browser) const runtimeDeps = npm.filter(dep => dep.runtime) const devDeps = npm.filter(dep => !dep.runtime) const vendored = collectVendored() @@ -690,15 +707,7 @@ export function render(): string { ? collectClaudeDistribution(manifests) : undefined const nonPermissiveDev = devDeps.filter(dep => !isPermissive(dep.license)) - // A copyleft license reaching a shipped surface is a distribution decision, - // not a rendering detail; the notices cannot quietly absorb it. - const nonPermissiveRuntime = runtimeDeps.filter(dep => - !isPermissive(dep.license) - && !isOwnerAuthorizedRuntime(dep.name), - ) - if (nonPermissiveRuntime.length > 0) { - throw new Error(`gen-third-party-notices: runtime ${nonPermissiveRuntime.map(dep => `${dep.name} (${dep.license})`).join(', ')} is not a permissive license; review the distribution terms and record the decision before regenerating.`) - } + assertRuntimeLicenses(runtimeDeps) const patchedLines = patched.map(({ spec, patch }) => `- \`${spec}\` — [\`${patch}\`](${patch})`) return ` pkg_system_prompt pkg_tool_fs_search --> pkg_timeout pkg_tool_fs_search --> pkg_tools + pkg_tool_present --> pkg_agent + pkg_tool_present --> pkg_attachment + pkg_tool_present --> pkg_fs + pkg_tool_present --> pkg_llm + pkg_tool_present --> pkg_session + pkg_tool_present --> pkg_session_projection + pkg_tool_present --> pkg_tools pkg_tool_str_replace_editor --> pkg_fs pkg_tool_str_replace_editor --> pkg_sandbox pkg_tool_str_replace_editor --> pkg_sandbox_policy @@ -1370,6 +1378,7 @@ flowchart TD | [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | +| [`tool-present`](../packages/fs/tool-present) | `fs` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) | | [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) | | [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) | | [`tool-web`](../packages/web/tool-web) | `web` | [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index 830f777dde..3c9a732a5c 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -58,6 +58,7 @@ flowchart TD pkg_fs_sandbox["fs-sandbox"] pkg_tool_fs["tool-fs"] pkg_tool_fs_search["tool-fs-search"] + pkg_tool_present["tool-present"] pkg_tool_str_replace_editor["tool-str-replace-editor"] end subgraph group_skill["packages/skill"] @@ -741,6 +742,13 @@ flowchart TD pkg_tool_fs_search --> pkg_system_prompt pkg_tool_fs_search --> pkg_timeout pkg_tool_fs_search --> pkg_tools + pkg_tool_present --> pkg_agent + pkg_tool_present --> pkg_attachment + pkg_tool_present --> pkg_fs + pkg_tool_present --> pkg_llm + pkg_tool_present --> pkg_session + pkg_tool_present --> pkg_session_projection + pkg_tool_present --> pkg_tools pkg_tool_str_replace_editor --> pkg_fs pkg_tool_str_replace_editor --> pkg_sandbox pkg_tool_str_replace_editor --> pkg_sandbox_policy @@ -1372,6 +1380,7 @@ flowchart TD | [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | +| [`tool-present`](../packages/fs/tool-present) | `fs` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) | | [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) | | [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) | | [`tool-web`](../packages/web/tool-web) | `web` | [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) | diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index 147c3a869a..c3b0738000 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-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/persistence-catalog.md -persistence-catalog.md: 06242185a988d6908be0c66e13a45af4e4974e1c -persistence-catalog.zh.md: bc6f4f623e60962df7adfd3771d4bf95b1c1ff3b +persistence-catalog.md: 1749faa99beb39940e2581bc58d0543ea5984fd1 +persistence-catalog.zh.md: b3e13c5f4bb9cf973249eaf7b8cf7d93366be96f diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 06242185a9..1749faa99b 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -396,6 +396,21 @@ Types: [ContentBlock](subsystems/core.md) · [TokenUsage](subsystems/llm-streami Source: [`packages/compaction/compaction/src/types.ts:34`](../packages/compaction/compaction/src/types.ts) +### `deliverables/*` + + + +#### `deliverables/presented` — log-only + +```ts persistence-catalog +/** Saved deliveries from a successful final present result, including nested calls. */ +'deliverables/presented': { turn: number; callId: ToolCallId; files: PresentedFile[] } +``` + +Types: [ToolCallId](subsystems/core.md) + +Source: [`packages/fs/tool-present/src/types.ts:16`](../packages/fs/tool-present/src/types.ts) + ### `feedback/*` diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index bc6f4f623e..b3e13c5f4b 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -398,6 +398,21 @@ export type SessionEvent = { 来源:[`packages/compaction/compaction/src/types.ts:34`](../packages/compaction/compaction/src/types.ts) +### `deliverables/*` + + + +#### `deliverables/presented` — 仅日志 + +```ts persistence-catalog +/** Saved deliveries from a successful final present result, including nested calls. */ +'deliverables/presented': { turn: number; callId: ToolCallId; files: PresentedFile[] } +``` + +类型: [ToolCallId](subsystems/core.zh.md) + +来源: [`packages/fs/tool-present/src/types.ts:16`](../packages/fs/tool-present/src/types.ts) + ### `feedback/*` diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml index 16319731c8..c43efdd85f 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: c85201523e66408e6aa536de1f141df650055265 -tool-catalog.zh.md: ce5f5b57f6ac6f6a3db6a1503967fc7ceb7c48fd +tool-catalog.md: 5a09b07e0a7a24e654fc45a7a477838ce21f30d7 +tool-catalog.zh.md: 528f3a37680ecb5f9482606c5671c4fb056092f1 diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index c85201523e..5a09b07e0a 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -19,6 +19,7 @@ This table connects model-visible tool names to the plugin package and service s | `@deepseek-ai/dsh-tools` | `run_code` | `ctx.tools`, `ctx.codeRuntime (execution time)`, `ctx.systemPrompt` | `tool/call`, `one tool/ptc-dispatch-start + tool/ptc-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-present` | `present` | `ctx.tools`, `ctx.fs`, `ctx.attachments`, `ctx.sessionProjections` | `tool/call`, `deliverables/presented after a successful final result`, `tool/result` | - | Deliveries belong to the calling Session; Web ui-deliverables supplies authenticated downloads and cards. | | `@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. | | `@deepseek-ai/dsh-tool-cordis` | `cordis_define`, `cordis_inspect_list`, `cordis_inspect_query`, `cordis_inspect_self`, `cordis_run`, `cordis_stop`, `cordis_undefine` | `ctx.tools`, `ctx.dynamicCordisRunner` | `tool/call`, `tool/result`, `process-local dynamic package lifecycle` | - | Not in any shipped tree (a deliberate opt-in — dynamic package code reaches the real runtime, see .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). The toolset injects `ctx.dynamicCordisRunner` from `@deepseek-ai/dsh-cordis-host-runner`, which owns the definition registry and the vm sandbox; a composition missing it never activates the tools. A running package may register ADDITIONAL model-visible tools until it is stopped, undefined, or DSH restarts; a full changed request header logs those tool-set changes. | | `@deepseek-ai/dsh-tool-bash-persistent` | `bash` | `ctx.tools`, `ctx.terminals`, `an owning Agent at execution time` | `tool/call`, `PTY shell state`, `tool/result` | - | One owner-isolated persistent bash tool; deployment composition supplies the PTY backend and may override the model-facing environment description. | @@ -218,6 +219,49 @@ Source: [`packages/shell/tool-bash/src/index.ts`](../packages/shell/tool-bash/sr 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-present` + +### `present` + +Deliver final files to the user. Saves a snapshot of each existing workspace file so it remains downloadable after edits or deletion. Create the files before calling this tool. + +```json +{ + "type": "object", + "properties": { + "files": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "description": "Path of an existing file inside the workspace." + }, + "description": { + "type": "string", + "description": "Brief description for the user." + } + }, + "required": [ + "path" + ] + } + } + }, + "required": [ + "files" + ] +} +``` + +Source: [`packages/fs/tool-present/src/index.ts`](../packages/fs/tool-present/src/index.ts) + +Deliveries belong to the calling Session; Web ui-deliverables supplies authenticated downloads and cards. + ## `@deepseek-ai/dsh-tool-pwsh` diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md index ce5f5b57f6..528f3a3768 100644 --- a/docs/tool-catalog.zh.md +++ b/docs/tool-catalog.zh.md @@ -23,6 +23,7 @@ | `@deepseek-ai/dsh-tools` | `run_code` | `ctx.tools`、`ctx.codeRuntime (execution time)`、`ctx.systemPrompt` | `tool/call`、`one tool/ptc-dispatch-start + tool/ptc-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-present` | `present` | `ctx.tools`, `ctx.fs`, `ctx.attachments`, `ctx.sessionProjections` | `tool/call`, `deliverables/presented 在成功的最终结果之后`, `tool/result` | - | 交付归调用方 Session 所有;Web ui-deliverables 提供认证下载与卡片。 | | `@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`。 | | `@deepseek-ai/dsh-tool-cordis` | `cordis_define`、`cordis_inspect_list`、`cordis_inspect_query`、`cordis_inspect_self`、`cordis_run`、`cordis_stop`、`cordis_undefine` | `ctx.tools`、`ctx.dynamicCordisRunner` | `tool/call`、`tool/result`、`process-local dynamic package lifecycle` | - | 不在任何随产品发布的树中,需要显式选择启用;动态 Package 代码可以访问真实运行时,见 .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md。该工具集注入 `@deepseek-ai/dsh-cordis-host-runner` 提供的 `ctx.dynamicCordisRunner`,后者拥有定义注册表和 vm 沙箱;组合缺少它时这些工具不会激活。运行中的 Package 在停止、undefine 或 DSH 重启前可以注册**额外的**模型可见工具;发生这类工具集变化时,系统会记录完整且有变动的请求头。 | | `@deepseek-ai/dsh-tool-bash-persistent` | `bash` | `ctx.tools`、`ctx.terminals`、`an owning Agent at execution time` | `tool/call`、`PTY shell state`、`tool/result` | - | 一个按所有者隔离的持久 bash 工具;部署组合提供 PTY 后端,并可覆盖面向模型的环境描述。 | @@ -222,6 +223,49 @@ ask_user_question 会暂停工具调用,直到当前 UI 提供方返回人类 bash 工具是 bash 执行器 seam 面向模型的消费方。使用 `run_in_background` 的运行会注册到通用 `ctx.jobs` 运行时,并通过 `job_*` 工具(来自 `@deepseek-ai/dsh-tool-jobs`)收集/停止;禁用 `enableRunInBackground` 配置(默认为 true)后,该参数会被完全移除。 + + +## `@deepseek-ai/dsh-tool-present` + +### `present` + +向用户交付最终文件。保存每个已有工作区文件的快照,使其在编辑或删除后仍可下载。调用工具前先创建文件。 + +```json +{ + "type": "object", + "properties": { + "files": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "description": "Path of an existing file inside the workspace." + }, + "description": { + "type": "string", + "description": "Brief description for the user." + } + }, + "required": [ + "path" + ] + } + } + }, + "required": [ + "files" + ] +} +``` + +来源: [`packages/fs/tool-present/src/index.ts`](../packages/fs/tool-present/src/index.ts) + +交付归调用方 Session 所有;Web ui-deliverables 提供认证下载与卡片。 + ## `@deepseek-ai/dsh-tool-pwsh` diff --git a/packages/bundle/base/package.json b/packages/bundle/base/package.json index 3d32022816..4f3407903a 100644 --- a/packages/bundle/base/package.json +++ b/packages/bundle/base/package.json @@ -95,6 +95,7 @@ "@deepseek-ai/dsh-tool-call-timeout-policy": "workspace:^", "@deepseek-ai/dsh-token-meter": "workspace:^", "@deepseek-ai/dsh-tool-bash": "workspace:^", + "@deepseek-ai/dsh-tool-present": "workspace:^", "@deepseek-ai/dsh-tool-fs": "workspace:^", "@deepseek-ai/dsh-tool-fs-search": "workspace:^", "@deepseek-ai/dsh-tool-goal": "workspace:^", diff --git a/packages/client/ui-chat/README.i18n.yaml b/packages/client/ui-chat/README.i18n.yaml index 4985ac3796..8ea1e5d5d6 100644 --- a/packages/client/ui-chat/README.i18n.yaml +++ b/packages/client/ui-chat/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-chat/README.md -README.md: b5a837f927b483a1f72e8282032fce29683bc237 -README.zh.md: f6b8018434251e46875bfd5cc0b0a242cdf2a439 +README.md: 7c9b38a1abc6289c9c0f7b8335d2c4a796e32840 +README.zh.md: 80198a89fea2586d350dfc8bbeaf8a122739122c diff --git a/packages/client/ui-chat/README.md b/packages/client/ui-chat/README.md index b5a837f927..7c9b38a1ab 100644 --- a/packages/client/ui-chat/README.md +++ b/packages/client/ui-chat/README.md @@ -10,6 +10,8 @@ English | [中文](README.zh.md) Use this package to render a browser chat from recorded Session conversations, including historical images, localized actions, and restored scroll position. Compact display folds completed-turn process rows while keeping the final answer and independently useful context visible; packed historical Assistant runs remain collapsed. Local transcript and steering submissions appear immediately, remain in their original surface, and disappear atomically when authoritative Session records arrive, while queued submissions stay outside Chat. The package does not assemble or modify model requests. +File-mention providers receive the viewed Session ID with the closing-turn owner, so links into inherited history can address the fork itself. + ## Table of Contents - [System prompt row](#system-prompt-row) diff --git a/packages/client/ui-chat/README.zh.md b/packages/client/ui-chat/README.zh.md index f6b8018434..80198a89fe 100644 --- a/packages/client/ui-chat/README.zh.md +++ b/packages/client/ui-chat/README.zh.md @@ -10,6 +10,8 @@ kind: "package-reference" 使用本包可在浏览器中渲染已记录的 Session 对话,包括历史图片、本地化操作和滚动位置恢复。紧凑显示会收起已完成轮次的过程行,同时保持最终答案和独立有用的上下文可见;已打包的历史 Assistant 连续消息保持收起。本地 transcript 与 steering 提交会立即显示并保留在原区域,在权威 Session 记录到达时原子地消失,而 queued 提交始终不进入 Chat。本包不组装或修改模型请求。 +文件引用提供方同时接收当前查看的 Session ID 与收尾 turn 的属主信息,因此继承历史中的链接可以指向 fork 自身。 + ## 目录 - [系统提示词行](#system-prompt-row) diff --git a/packages/client/ui-chat/src/client/apply.ts b/packages/client/ui-chat/src/client/apply.ts index 2d8de50561..0fc9e82a22 100644 --- a/packages/client/ui-chat/src/client/apply.ts +++ b/packages/client/ui-chat/src/client/apply.ts @@ -116,7 +116,7 @@ export function apply(ctx: Context): void { chatNode: key => chat.getSnapshot().nodes.source(key), chatNodeProcess: key => chat.getSnapshot().nodes.processSource(key), }, - fileMentions: (owner: TurnTailOwnerProps) => ctx.get('chatFileMentions')?.forClosing(owner), + fileMentions: (owner: TurnTailOwnerProps) => ctx.get('chatFileMentions')?.forClosing(owner, sessionId), // Files open in the right Sidebar, not in a desktop application: the // content stays in the product, beside the conversation that produced // it. A relative path, or an absolute one inside the session's diff --git a/packages/client/ui-chat/src/client/contract/slots.ts b/packages/client/ui-chat/src/client/contract/slots.ts index 3fb8bcfcee..b5c3a45b99 100644 --- a/packages/client/ui-chat/src/client/contract/slots.ts +++ b/packages/client/ui-chat/src/client/contract/slots.ts @@ -1,6 +1,6 @@ /** Chat-owned Slot declarations and composed component props. */ import type { MessageId } from '@deepseek-ai/dsh-llm/brand' -import type { SessionSeq } from '@deepseek-ai/dsh-session/types' +import type { SessionId, SessionSeq } from '@deepseek-ai/dsh-session/types' import type { CommandNode, CompactionSummaryNode, ConversationLocationDataStore, ConversationTurnDataMap, MessageImageLoader, MessageImagesOwnerProps, RenderMessageImages, TurnLocation, @@ -53,9 +53,10 @@ export interface ChatFileMentions { /** * Resolve prose links for one closing Turn. * @param owner - closing-Turn identity and file opener. + * @param sessionId - viewed Session, including when history is inherited from a fork. * @returns link resolver when available. */ - forClosing(owner: TurnTailOwnerProps): MarkdownFileMentions | undefined + forClosing(owner: TurnTailOwnerProps, sessionId: SessionId): MarkdownFileMentions | undefined } declare module '@deepseek-ai/cordis' { diff --git a/packages/client/ui-chat/tests/apply-inject.client.spec.tsx b/packages/client/ui-chat/tests/apply-inject.client.spec.tsx index 947eb739ed..71a7b5afe9 100644 --- a/packages/client/ui-chat/tests/apply-inject.client.spec.tsx +++ b/packages/client/ui-chat/tests/apply-inject.client.spec.tsx @@ -182,7 +182,7 @@ describe('Chat inject API', () => { const forClosing = vi.fn(() => mentions) b.runtime.ctx.provide('chatFileMentions', { forClosing } as never) expect(injected.fileMentions(owner)).toBe(mentions) - expect(forClosing).toHaveBeenCalledWith(owner) + expect(forClosing).toHaveBeenCalledWith(owner, ROOT) expect(injected.chatScroll.read()).toBeNull() const position = { anchorKey: 'node-1', anchorTop: 4, scrollTop: 12 } diff --git a/packages/client/ui-conversation/src/client/conversation/location-index.ts b/packages/client/ui-conversation/src/client/conversation/location-index.ts index f7bac8eb1c..018fc92e7c 100644 --- a/packages/client/ui-conversation/src/client/conversation/location-index.ts +++ b/packages/client/ui-conversation/src/client/conversation/location-index.ts @@ -137,7 +137,9 @@ const SESSION_LOCATION = { kind: 'session' } as const const UNRESOLVED_LOCATION = { kind: 'unresolved' } as const function payloadCoordinates(event: SessionEventLike): Coordinates { - const data = event.data as unknown as { turn?: unknown; step?: unknown } + const payload: unknown = event.data + if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) return {} + const data = payload as { turn?: unknown; step?: unknown } if (data.turn === null) return { session: true } const turn = Number.isSafeInteger(data.turn) && (data.turn as number) >= 0 ? data.turn as number diff --git a/packages/client/ui-deliverables/README.i18n.yaml b/packages/client/ui-deliverables/README.i18n.yaml index 01417ca259..4e53ba774c 100644 --- a/packages/client/ui-deliverables/README.i18n.yaml +++ b/packages/client/ui-deliverables/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-deliverables/README.md -README.md: bfd4c40a952122db25172bebb077d12800b49308 -README.zh.md: 23c983f938d2d77fa5f33e8dcd2526282268b835 +README.md: 721d7146e3d1d0499c45c5d768e5bb8e64a1fff5 +README.zh.md: 5922815b7fd558df73a62333dfb39c4b5d24bf82 diff --git a/packages/client/ui-deliverables/README.md b/packages/client/ui-deliverables/README.md index bfd4c40a95..721d7146e3 100644 --- a/packages/client/ui-deliverables/README.md +++ b/packages/client/ui-deliverables/README.md @@ -27,6 +27,13 @@ This package renders the deliverables row a finished turn ends with — the file Mount this plugin alongside `ui-conversation`; a finished turn then ends with the produced-files row between the closing message's body and its action footer. Each chip opens the file through the owner's `openFile`, which the chat view routes to the right Sidebar as a text-preview tab, with relative paths resolved against the session cwd. The row offers no folder action: the Sidebar has no directory form, so an omitted-file remainder is a label only. + +### Explicit deliveries + +The Web `standard`, `ptc`, and `cordis` presets expose `present` for final files, including files created through Bash. Call it with `files: [{ path, description? }]` after creating the files. The [present tool](../../fs/tool-present/README.md) owns snapshot creation, limits, and Session delivery records. The closing turn shows responsive file cards with names, types, sizes, descriptions, and download actions, and matching inline-code references download the same snapshots after source edits, deletion, or reload. Forks download through the viewed Session. Repeated delivery of a path selects its latest successful snapshot before the closing reply. + +The `present` tool row shows running, delivered, failed, or interrupted status; expanding a settled row reveals its recorded result. File cards include every delivered file. + ### The row The row uses CSS container-width bands to show a responsive prefix of up to six file chips. Flexbox shrinks and ellipsizes basename text, while CSS selects the matching localized `+ N files` label for omitted paths; the full path remains available as the title, and the row performs no JavaScript layout observation or horizontal scrolling. @@ -43,7 +50,7 @@ The closing prose carries the same vocabulary: an inline-code token resolves by
    Implementation internals — click to expand -The Node half registers the static `ui:deliverable-file-references` system-prompt section asking the model to mention primary files from successful creation or modification calls and to write those and any other changed-file references as Markdown inline code. The browser half registers `ProducedFiles` into the chat view's `conversation.chat.turnTail` hole. `deliverablesDefinition` folds each Turn's successful first-party mutation calls into `DeliverablesTurnData` from the validated raw arguments of `write`, `edit`, and mutating `str_replace_editor` commands. Reads, deletes, unsupported tools, malformed calls, and failed results contribute nothing. A new mutation tool needs an explicit Client contribution before it joins the list. The package also provides the `chatFileMentions` service the chat view consults per closing message; composing the plugin out removes both surfaces and leaves the view's empty chain at zero cost. +The Node half registers the static `ui:deliverable-file-references` system-prompt section asking the model to mention primary files from successful creation or modification calls and to write those and any other changed-file references as Markdown inline code. The browser half registers a wrapper around `ProducedFiles` and explicit deliveries into the chat view's `conversation.chat.turnTail` hole. `deliverablesDefinition` folds each Turn's successful first-party mutation calls into `DeliverablesTurnData` from the validated raw arguments of `write`, `edit`, and mutating `str_replace_editor` commands. Reads, deletes, unsupported tools, malformed calls, and failed results contribute nothing. A new mutation tool needs an explicit Client contribution before it joins the list. The package also provides the `chatFileMentions` service the chat view consults per closing message; composing the plugin out removes both surfaces and leaves the view's empty chain at zero cost.
    @@ -72,7 +79,7 @@ One fixed paragraph instructs the model to name primary files from successful cr #### Token effect -One fixed prompt paragraph whenever this package is loaded; no tool schema, tool result, or per-Turn context is added. +One fixed prompt paragraph whenever this package is loaded. The [present tool](../../fs/tool-present/README.md#model-experience) owns the delivery schema and result text. #### KV Cache effect @@ -86,7 +93,8 @@ The section is static at first-party order 9000 for the lifetime of the package These limits define the current deliverables vocabulary. They are current package constraints, not a general file-linking comparison or a task backlog. - **Mention matching is exact path or unique basename only** — a suffix mention stays inert; widening the matcher is deferred until a real closing-message shape needs it. -- **Files created indirectly by terminal commands remain outside the matching vocabulary** — naming such a file in inline code does not make it clickable unless a successful mutation location also records that path. +- **Terminal-created files require explicit delivery** — call `present` to make their snapshots downloadable. +- **Transferred Session exports contain no delivered bytes** — download links require the same snapshots in the serving host’s attachment store; missing or pruned snapshots return 404. - **Directories have no destination** — chips open files in the right Sidebar's text preview, which shows files only; the former native folder handoff is gone rather than replaced. @@ -99,4 +107,4 @@ None.
  • -**Runtime invariant:** No companion is published. The prompt section, slot, dictionary, event definition, and optional service registrations are effect-owned with disposal proven by their plugin specs; this package owns no mutable state. +**Runtime invariant:** No companion is published. The prompt section, slot, dictionary, download route, and optional service registrations are effect-owned with disposal proven by their plugin specs; the attachment service owns saved bytes, and the Session log owns delivery references. diff --git a/packages/client/ui-deliverables/README.zh.md b/packages/client/ui-deliverables/README.zh.md index 23c983f938..5922815b7f 100644 --- a/packages/client/ui-deliverables/README.zh.md +++ b/packages/client/ui-deliverables/README.zh.md @@ -27,6 +27,13 @@ kind: "package-reference" 与 `ui-conversation` 一起挂载本插件;已完成轮次随即以产出文件行收尾,位于收尾消息正文与其动作页脚之间。每个标签项经属主的 `openFile` 打开文件——chat 视图把它路由到右侧 Sidebar 作为一个文本预览 tab——相对路径按会话 cwd 解析。该行不提供文件夹动作:Sidebar 没有目录形态,因此省略文件的余数只是一个标签才会打开会话工作区。 + +### 显式交付 + +Web 的 `standard`、`ptc` 与 `cordis` preset 提供 `present` 用于交付最终文件,包括通过 Bash 创建的文件。创建文件后,以 `files: [{ path, description? }]` 调用。[present 工具](../../fs/tool-present/README.zh.md)拥有快照创建、限制和 Session 交付记录。收尾 turn 显示响应式文件卡片,包含名称、类型、大小、说明和下载操作,匹配的行内代码引用也下载相同快照;修改或删除源文件、重新加载后仍可下载。Fork 通过当前查看的 Session 下载。同一路径重复交付时,选择收尾回复之前最近一次成功的快照。 + +`present` 工具行显示正在交付、已交付、失败或中断状态;展开已结束的调用可查看其记录的结果。文件卡片展示全部交付文件。 + ### 该行 该行通过 CSS 容器宽度档位响应式展示至多六个文件标签项。Flexbox 负责收缩文件名并用 ellipsis 省略,CSS 为未展示路径选择匹配的本地化 `+ N 个文件` 标签;完整路径仍保留在 `title` 中,该行不执行 JavaScript 布局观察,也不提供横向滚动。 @@ -43,7 +50,7 @@ kind: "package-reference"
    实现细节——点击展开 -Node 半部注册静态 `ui:deliverable-file-references` 系统提示词段,要求模型点名成功创建或修改的主要文件,并把这些文件以及正文中提到的其他本轮变更文件写成 Markdown 行内代码。浏览器半部把 `ProducedFiles` 注册进 chat 视图的 `conversation.chat.turnTail` 洞。`deliverablesDefinition` 根据 `write`、`edit` 和有修改作用的 `str_replace_editor` 命令中经过校验的原始参数,把每个轮次成功的第一方修改调用折叠进 `DeliverablesTurnData`。读取、删除、不受支持的工具、格式错误的调用和失败结果不贡献任何条目。新的修改工具必须增加显式 Client contribution 才能加入列表。本包还提供 chat 视图按收尾消息查询的 `chatFileMentions` 服务;把插件组合出去会同时移除两个表面,视图的空链以零成本留下。 +Node 半部注册静态 `ui:deliverable-file-references` 系统提示词段,要求模型点名成功创建或修改的主要文件,并把这些文件以及正文中提到的其他本轮变更文件写成 Markdown 行内代码。浏览器半部把组合 `ProducedFiles` 与显式交付的包装组件注册进 chat 视图的 `conversation.chat.turnTail` 洞。`deliverablesDefinition` 根据 `write`、`edit` 和有修改作用的 `str_replace_editor` 命令中经过校验的原始参数,把每个轮次成功的第一方修改调用折叠进 `DeliverablesTurnData`。读取、删除、不受支持的工具、格式错误的调用和失败结果不贡献任何条目。新的修改工具必须增加显式 Client contribution 才能加入列表。本包还提供 chat 视图按收尾消息查询的 `chatFileMentions` 服务;把插件组合出去会同时移除两个表面,视图的空链以零成本留下。
    @@ -72,7 +79,7 @@ Node 半部注册静态 `ui:deliverable-file-references` 系统提示词段, #### Token 影响 -加载本包时增加一段固定提示词;不增加工具 schema、工具结果或按轮次变化的上下文。 +加载本包时增加一段固定提示词。[present 工具](../../fs/tool-present/README.zh.md#model-experience)拥有交付 schema 和结果文本。 #### KV Cache 影响 @@ -86,7 +93,8 @@ Node 半部注册静态 `ui:deliverable-file-references` 系统提示词段, 这些限制界定了当前产出物词表。它们是当前包约束,不是通用文件链接对比或任务积压。 - **提及匹配只认精确路径或唯一 basename**——后缀式提及保持惰性;等真实的收尾消息形态产生需求后再放宽匹配规则。 -- **终端命令间接创建的文件仍不在匹配词表内**——除非某个成功修改位置也记录了该路径,否则在行内代码中点名这类文件不会使其可点击。 +- **终端创建的文件需要显式交付**——调用 `present` 使其快照可供下载。 +- **转移的 Session 导出不含交付字节** — 下载链接依赖服务主机附件存储中的同一快照;快照缺失或被清理时返回 404。 - **原生文件夹交接以 Host 桌面为目标**——经非 loopback authority 访问的浏览器会省略该动作,报告没有原生打开器的部署也一样;若 SSH 转发让远端 Host 看似 loopback 本地,部署必须为 Session Controller 设置 `nativeOpen: false`。 @@ -99,4 +107,4 @@ Node 半部注册静态 `ui:deliverable-file-references` 系统提示词段,
    -**运行时不变式:** 不发布伴生入口。prompt section、slot、dictionary、event definition 与可选 service 注册都归 effect 所有,释放由插件测试证明;本包不持有可变状态。 +**运行时不变式:** 不发布伴生入口。prompt section、slot、dictionary、下载路由与可选 service 注册都归 effect 所有,释放由插件测试证明;attachment 服务拥有保存的字节,Session 日志拥有交付引用。 diff --git a/packages/client/ui-deliverables/package.json b/packages/client/ui-deliverables/package.json index 187b548e24..e07bd902a4 100644 --- a/packages/client/ui-deliverables/package.json +++ b/packages/client/ui-deliverables/package.json @@ -33,7 +33,8 @@ "@deepseek-ai/dsh-client-locale", "@deepseek-ai/dsh-client-ui-chat", "@deepseek-ai/dsh-client-ui-conversation", - "@deepseek-ai/dsh-client-ui-renderer" + "@deepseek-ai/dsh-client-ui-renderer", + "@deepseek-ai/dsh-client-ui-tool" ], "platform": "web" } @@ -61,7 +62,13 @@ "react": "^18.2.0", "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", - "@deepseek-ai/dsh-system-prompt": "workspace:^" + "@deepseek-ai/dsh-system-prompt": "workspace:^", + "@deepseek-ai/dsh-attachment": "workspace:^", + "@deepseek-ai/dsh-session-query": "workspace:^", + "@deepseek-ai/dsh-llm": "workspace:^", + "@deepseek-ai/dsh-attachment-local": "workspace:^", + "@deepseek-ai/dsh-client-ui-tool": "workspace:^", + "@deepseek-ai/dsh-tool-present": "workspace:^" }, "files": [ "lib/index.js", diff --git a/packages/client/ui-deliverables/src/client/Deliverables.module.css b/packages/client/ui-deliverables/src/client/Deliverables.module.css new file mode 100644 index 0000000000..e83fc1eb34 --- /dev/null +++ b/packages/client/ui-deliverables/src/client/Deliverables.module.css @@ -0,0 +1,13 @@ +/** Immutable file deliveries at the end of a turn. */ +.root { display: flex; flex-direction: column; gap: 8px; min-width: 0; margin-top: 12px; } +.label { font-size: 12px; color: var(--dsw-alias-label-secondary); } +.presented { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); gap: 8px; } +.file { display: flex; align-items: center; gap: 12px; min-width: 0; padding: 14px; border: 0.5px solid var(--dsw-alias-border-l2); border-radius: 12px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); text-decoration: none; } +.file:hover { background: var(--dsw-alias-bg-layer-2); border-color: var(--dsw-alias-border-l3); } +.file:focus-visible { outline: 2px solid var(--dsw-alias-link); outline-offset: 2px; } +.fileIcon { flex: 0 0 auto; width: 24px; height: 24px; } +.details { display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1; } +.fileName { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; font-weight: 500; } +.metadata { color: var(--dsw-alias-label-tertiary); font-size: 11px; } +.description { color: var(--dsw-alias-label-secondary); font-size: 12px; overflow-wrap: anywhere; } +.download { display: flex; flex-direction: column; align-items: center; gap: 4px; flex: 0 0 auto; color: var(--dsw-alias-link); font-size: 11px; } diff --git a/packages/client/ui-deliverables/src/client/Deliverables.tsx b/packages/client/ui-deliverables/src/client/Deliverables.tsx new file mode 100644 index 0000000000..cfab1925bd --- /dev/null +++ b/packages/client/ui-deliverables/src/client/Deliverables.tsx @@ -0,0 +1,51 @@ +/** Existing changed-file chips and explicitly delivered snapshots for a closing turn. */ +import type { TurnTailOwnerProps } from '@deepseek-ai/dsh-client-ui-chat/client' +import { LinkIcon, classifyLinkPath, fileSizeText, IconDownloadOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' +import type { PropsLocale, SessionStandardProps } from '@deepseek-ai/dsh-client-ui-slots' +import { ProducedFiles } from './ProducedFiles.tsx' +import { basename, presentedForClosing, selectProducedFiles, type PresentedPath } from './turn-deliverables.ts' +import type { NS } from './locales.ts' +import { presentedFileUrl } from '../presented.ts' +import css from './Deliverables.module.css' + +interface DeliverablesMatch { produced: readonly string[]; presented: readonly PresentedPath[] } + +/** + * Claim turns containing modified paths or presented snapshots. + * @param owner - closing turn. + * @returns matched files, or null for an empty turn. + */ +export function selectDeliverables(owner: TurnTailOwnerProps): DeliverablesMatch | null { + const produced = selectProducedFiles(owner) ?? [] + const presented = presentedForClosing(owner) + return produced.length + presented.length === 0 ? null : { produced, presented } +} + +/** + * Render workspace file actions and immutable snapshot downloads. + * @param props - matched files, workspace opener, and localized copy. + * @returns the closing turn's file rows. + */ +export function Deliverables({ matched, openFile, t, sessionId }: Pick & { + matched: DeliverablesMatch +} & PropsLocale & Pick) { + return <> + {matched.produced.length > 0 && } + {matched.presented.length > 0 && } + +} diff --git a/packages/client/ui-deliverables/src/client/PresentRow.module.css b/packages/client/ui-deliverables/src/client/PresentRow.module.css new file mode 100644 index 0000000000..b97ff7de02 --- /dev/null +++ b/packages/client/ui-deliverables/src/client/PresentRow.module.css @@ -0,0 +1,6 @@ +/** Present status remains readable beside long workspace paths. */ +.summary { margin-left: 8px; display: flex; align-items: center; gap: 8px; min-width: 0; color: var(--dsw-alias-label-secondary); font-size: 12px; } +.summary > :first-child { flex-shrink: 0; } +.paths { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.output { margin: 8px 0; padding: 12px; border-radius: 8px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-secondary); white-space: pre-wrap; overflow-wrap: anywhere; font-size: 12px; } +.inspect { align-self: flex-start; border: none; padding: 4px 0; background: transparent; color: var(--dsw-alias-link); font: inherit; font-size: 12px; cursor: pointer; } diff --git a/packages/client/ui-deliverables/src/client/PresentRow.tsx b/packages/client/ui-deliverables/src/client/PresentRow.tsx new file mode 100644 index 0000000000..845d2b4dcc --- /dev/null +++ b/packages/client/ui-deliverables/src/client/PresentRow.tsx @@ -0,0 +1,45 @@ +/** Present call status and expandable durable result text. */ +import { useState } from 'react' +import { DisclosureRow, StateDot } from '@deepseek-ai/dsh-client-ui-primitives' +import type { ToolCallViewProps } from '@deepseek-ai/dsh-client-ui-tool/client' +import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots' +import type { NS } from './locales.ts' +import css from './PresentRow.module.css' + +type PresentRowProps = ToolCallViewProps & PropsLocale + +/** Raw arguments can be partial while a call is streaming. */ +function fileNames(raw: string): string { + let args: unknown + try { args = JSON.parse(raw) } + catch { return raw } // Truncated tool JSON remains visible until the call completes. + if (typeof args !== 'object' || args === null || !('files' in args) || !Array.isArray(args.files)) return raw + return args.files.flatMap((file: unknown) => + typeof file === 'object' && file !== null && 'path' in file && typeof file.path === 'string' + ? [file.path] : [], + ).join(', ') +} + +/** + * Render a present call using its recorded arguments and result. + * @param props - tool call and localized status copy. + * @returns a status row with a result disclosure. + */ +export function PresentRow({ block, inspect, t }: PresentRowProps) { + const settled = 'kind' in block + const state = !settled ? 'running' : block.error?.code === 'interrupted' ? 'stopped' : block.isError ? 'error' : 'ok' + const args = (settled ? block.call?.argsRaw : block.argsRaw) ?? '' + const output = settled ? block.content.map(item => item.type === 'text' ? item.text : JSON.stringify(item)).join('\n') : '' + const details = output || (settled && block.error ? `${block.error.name}: ${block.error.code}` : '') + const [expanded, setExpanded] = useState(false) + return
    + } + open={expanded && details !== ''} expandable={details !== ''} expandOnRowClick keepContentWhenOpen + onToggle={() => { setExpanded(value => !value) }} + collapsedContent={{t(`row.${state}`)}{fileNames(args)}}> +
    {details}
    + {inspect && } +
    +
    +} diff --git a/packages/client/ui-deliverables/src/client/index.ts b/packages/client/ui-deliverables/src/client/index.ts index 01877b7b57..db6355664f 100644 --- a/packages/client/ui-deliverables/src/client/index.ts +++ b/packages/client/ui-deliverables/src/client/index.ts @@ -13,10 +13,12 @@ import type { ChatFileMentions } from '@deepseek-ai/dsh-client-ui-chat/client' import type {} from '@deepseek-ai/dsh-client-locale/client' import type {} from '@deepseek-ai/dsh-client-ui-conversation/client' import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' -import { ProducedFiles } from './ProducedFiles.tsx' +import { presentedFileUrl } from '../presented.ts' +import { PresentRow } from './PresentRow.tsx' +import { Deliverables, selectDeliverables } from './Deliverables.tsx' import { en, NS, zh, type DeliverablesKey } from './locales.ts' import { - deliverablesDefinition, producedFileMentions, selectProducedFiles, + deliverablesDefinition, presentedForClosing, producedFileMentions, selectProducedFiles, } from './turn-deliverables.ts' declare module '@deepseek-ai/dsh-client-ui-slots' { @@ -43,20 +45,34 @@ export function apply(ctx: ClientContext): void { 'conversation.chat.turnTail', () => ctx.slots.register({ name: 'conversation.chat.turnTail', - select: selectProducedFiles, + select: selectDeliverables, locale: NS, - }, ProducedFiles), + }, Deliverables), ) + ctx.slots.inject('tool.call.toolview', () => ctx.slots.register( + { name: 'tool.call.toolview', key: 'present', locale: NS }, PresentRow, + )) // The prose side of the same vocabulary: the chat view reaches this face // via ctx.get, so its absence — this plugin composed out — is the off state. const t = ctx.locale.bind(NS) const mentions: ChatFileMentions = { - forClosing(owner) { + forClosing(owner, sessionId) { // Same claim test the turn-tail chain entry runs: no produced files, // no vocabulary — the two surfaces agree by construction. const paths = selectProducedFiles(owner) - if (paths === null) return undefined - return producedFileMentions(paths, owner.openFile, path => t('produced.open', { name: path })) + const presented = presentedForClosing(owner) + if (paths === null && presented.length === 0) return undefined + const deliveries = new Map(presented.map(file => [file.path, file])) + return producedFileMentions([...new Set([...paths ?? [], ...deliveries.keys()])], (path) => { + const file = deliveries.get(path) + if (file === undefined) owner.openFile(path) + else { + const link = document.createElement('a') + link.href = presentedFileUrl(sessionId, file.seq, file.index) + link.download = file.name + link.click() + } + }, path => t(deliveries.has(path) ? 'presented.download' : 'produced.open', { name: path })) }, } ctx.provide('chatFileMentions', mentions) diff --git a/packages/client/ui-deliverables/src/client/locales.ts b/packages/client/ui-deliverables/src/client/locales.ts index 158b659f59..af3cc1cfae 100644 --- a/packages/client/ui-deliverables/src/client/locales.ts +++ b/packages/client/ui-deliverables/src/client/locales.ts @@ -5,6 +5,16 @@ export const NS = 'deliverables' /** Simplified Chinese dictionary (the key-set source of truth). */ export const zh = { + 'presented.label': '交付文件', + 'presented.action': '下载', + 'presented.file': '文件', + 'row.title': '交付文件', + 'row.running': '正在交付', + 'row.ok': '已交付', + 'row.error': '交付失败', + 'row.stopped': '已中断', + 'row.inspect': '查看调用', + 'presented.download': '下载 {name}', 'produced.label': '产物', 'produced.moreOne': '+ 1 个文件', 'produced.more': '+ {count} 个文件', @@ -13,6 +23,16 @@ export const zh = { /** English dictionary (same key set). */ export const en: Record = { + 'presented.label': 'Deliverables', + 'presented.action': 'Download', + 'presented.file': 'File', + 'row.title': 'Present files', + 'row.running': 'Delivering', + 'row.ok': 'Delivered', + 'row.error': 'Delivery failed', + 'row.stopped': 'Interrupted', + 'row.inspect': 'Inspect call', + 'presented.download': 'Download {name}', 'produced.label': 'Produced', 'produced.moreOne': '+ 1 file', 'produced.more': '+ {count} files', diff --git a/packages/client/ui-deliverables/src/client/turn-deliverables.ts b/packages/client/ui-deliverables/src/client/turn-deliverables.ts index 90e3248516..ba70191a01 100644 --- a/packages/client/ui-deliverables/src/client/turn-deliverables.ts +++ b/packages/client/ui-deliverables/src/client/turn-deliverables.ts @@ -7,6 +7,14 @@ import { isAppendSurfaceEvent } from '@deepseek-ai/dsh-session/surface' import type { TurnTailOwnerProps } from '@deepseek-ai/dsh-client-ui-chat/client' import type { ConversationNodeDefinition } from '@deepseek-ai/dsh-client-ui-conversation/client' import type { MarkdownFileMentions } from '@deepseek-ai/dsh-client-ui-primitives' +import type { PresentedFile } from '@deepseek-ai/dsh-tool-present/types' +import { basename, isPresentedData, isPresentedFile } from '../presented.ts' + +/** A saved delivery with its authorized download coordinate. */ +export interface PresentedPath extends PresentedFile { + readonly seq: number + readonly index: number +} interface ProducedPath { readonly seq: number @@ -16,6 +24,7 @@ interface ProducedPath { /** Immutable produced-file facts published against one Turn. */ export interface DeliverablesTurnData { readonly produced: readonly ProducedPath[] + readonly presented?: readonly PresentedPath[] } declare module '@deepseek-ai/dsh-client-ui-conversation/client' { @@ -150,6 +159,7 @@ export const deliverablesDefinition: ConversationNodeDefinition { if (event.type === 'turn/start') return { id: String(event.data.turn), role: 'start' } if (event.type === 'tool/call') return { id: String(event.data.turn), role: 'update' } + if (event.type === 'deliverables/presented') return isPresentedData(event.data) ? { id: String(event.data.turn), role: 'update' } : null if (event.type === 'tool/result' && isAppendSurfaceEvent(event)) { return { id: String(event.data.turn), role: 'update' } } @@ -160,6 +170,12 @@ export const deliverablesDefinition: ConversationNodeDefinition { + if (match.event.type === 'deliverables/presented') { + const { files } = match.event.data + const seq = match.event.seq + const presented = files.flatMap((file, index) => isPresentedFile(file) ? [{ ...file, seq, index }] : []) + return { ...context.state, presented: [...context.state.presented ?? [], ...presented] } + } if (match.event.type === 'tool/call') { const calls = new Map(context.state.calls) calls.set( @@ -182,26 +198,32 @@ export const deliverablesDefinition: ConversationNodeDefinition() + for (const file of owner.turn.data.get('deliverables')?.presented ?? []) { + if (file.seq < owner.seq) files.set(file.path, file) + } + return [...files.values()] } +export { basename } from '../presented.ts' + /** * File-mention vocabulary over one turn's produced paths, for the closing * message's prose: an inline-code token opens the file it names. A token diff --git a/packages/client/ui-deliverables/src/index.ts b/packages/client/ui-deliverables/src/index.ts index 00e1adac9c..963d0c1700 100644 --- a/packages/client/ui-deliverables/src/index.ts +++ b/packages/client/ui-deliverables/src/index.ts @@ -1,15 +1,17 @@ /** * Deliverables plugin, node half. Registers the response-format guidance that - * lets the browser half recognize final-response file references. The browser + * lets the browser half recognize final-response file references and serves + * authenticated snapshot downloads. The browser * half ships via exports["./client"], discovered through the package.json * dsh.client declaration. */ import type { Context } from '@deepseek-ai/cordis' import type {} from '@deepseek-ai/dsh-system-prompt' +import { registerPresentDownload } from './present-download.ts' -/** Services required for the model guidance paired with the browser renderer. */ -export const inject = ['systemPrompt'] +/** Services required for file-reference guidance and authenticated snapshot downloads. */ +export const inject = ['systemPrompt', 'connection', 'sessionQuery', 'attachments'] /** Stable final-response guidance owned by the matching renderer. */ const FILE_REFERENCE_PROMPT = 'When you successfully create or modify files, mention the primary outputs in your final response. ' @@ -20,6 +22,7 @@ const FILE_REFERENCE_PROMPT = 'When you successfully create or modify files, men * @param ctx - host context carrying the system-prompt registry. */ export function apply(ctx: Context): void { + registerPresentDownload(ctx) ctx.systemPrompt.section({ name: 'ui:deliverable-file-references', order: ctx.systemPrompt.getSectionOrder('DELIVERABLE_FILE_REFERENCES'), diff --git a/packages/client/ui-deliverables/src/present-download.ts b/packages/client/ui-deliverables/src/present-download.ts new file mode 100644 index 0000000000..4d5d3ea75b --- /dev/null +++ b/packages/client/ui-deliverables/src/present-download.ts @@ -0,0 +1,72 @@ +/** Authorize downloads against the Session log, then stream only the saved Presented file bytes. */ +import { Readable } from 'node:stream' +import type { Context } from '@deepseek-ai/cordis' +import type {} from '@deepseek-ai/dsh-attachment' +import type {} from '@deepseek-ai/dsh-client-connection' +import type { SessionId, SessionSeq } from '@deepseek-ai/dsh-session' +import { isPresentedData, isPresentedFile, PRESENT_DOWNLOAD_PATH } from './presented.ts' +import type {} from '@deepseek-ai/dsh-session-query' + +/** + * Register a streaming download inside Connection's existing authentication fence. + * @param ctx - Host services owning Session reads, attachments, and HTTP routing. + */ +export function registerPresentDownload(ctx: Context): void { + ctx.connection.fetch.register({ + path: PRESENT_DOWNLOAD_PATH, + methods: ['GET'], + requestBody: 'buffered', + fetch: request => download(ctx, request), + }) +} + +async function download(ctx: Context, request: Request): Promise { + const query = new URL(request.url).searchParams + const id = query.get('sessionId') + const seq = query.get('seq') + const index = query.get('index') + if (!id || seq === null || index === null || !/^\d+$/.test(seq) || !/^\d+$/.test(index) + || !Number.isSafeInteger(Number(seq)) || !Number.isSafeInteger(Number(index))) { + return new Response('Invalid Presented file coordinates.', { status: 400 }) + } + try { + const { target } = await ctx.sessionQuery.readEvent({ + sessionId: id as SessionId, seq: Number(seq) as SessionSeq, before: 0, after: 0, + }, request.signal) + const artifact = target.type === 'deliverables/presented' && isPresentedData(target.data) ? target.data.files[Number(index)] : undefined + if (!isPresentedFile(artifact)) return new Response('Presented file not found in this Session result.', { status: 404 }) + const filename = encodeURIComponent(artifact.name.toWellFormed()) + .replace(/['()*]/g, character => `%${character.charCodeAt(0).toString(16).toUpperCase()}`) + const iterator = ctx.attachments.readFileStream(artifact, request.signal)[Symbol.asyncIterator]() + // Open before sending headers so an absent snapshot is a 404, not an empty successful download. + let first: IteratorResult | undefined = await iterator.next() + const data: AsyncIterableIterator = { + [Symbol.asyncIterator]() { return this }, + next() { + const pending = first + first = undefined + return pending === undefined ? iterator.next() : Promise.resolve(pending) + }, + async return() { + await iterator.return?.() + return { done: true, value: undefined } + }, + } + const body = Readable.toWeb(Readable.from(data, { signal: request.signal })) as ReadableStream + return new Response(body, { + headers: { + 'content-type': 'application/octet-stream', + 'content-disposition': `attachment; filename*=UTF-8''${filename}`, + 'cache-control': 'no-store', + 'x-content-type-options': 'nosniff', + }, + }) + } catch (error: unknown) { + request.signal.throwIfAborted() + const missing = error instanceof Error && 'code' in error + && (error.code === 'SESSION_QUERY_SESSION_NOT_FOUND' || error.code === 'SESSION_QUERY_EVENT_NOT_FOUND') + const absentSnapshot = ctx.attachments.isAttachmentError(error) && error.code === 'ATTACHMENT_NOT_FOUND' + const status = missing || absentSnapshot ? 404 : 500 + return new Response('Presented file snapshot unavailable.', { status }) + } +} diff --git a/packages/client/ui-deliverables/src/presented.ts b/packages/client/ui-deliverables/src/presented.ts new file mode 100644 index 0000000000..0e58ff0c49 --- /dev/null +++ b/packages/client/ui-deliverables/src/presented.ts @@ -0,0 +1,55 @@ +/** Validate durable delivery references and address their Web downloads. */ +import type { PresentedFile } from '@deepseek-ai/dsh-tool-present/types' +import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type { ToolCallId } from '@deepseek-ai/dsh-llm/brand' + +/** Authenticated route for saved file bytes. */ +export const PRESENT_DOWNLOAD_PATH = '/api/present.download' + +/** + * Validate a saved delivery read from a Session log. + * @param value - decoded durable data. + * @returns whether the reference contains the fields used for display and downloads. + */ +export function isPresentedFile(value: unknown): value is PresentedFile { + if (typeof value !== 'object' || value === null || Array.isArray(value)) return false + const { path, name, bytes, attachmentId, description } = value as Record + return typeof path === 'string' && path.trim().length > 0 + && typeof name === 'string' && name.trim().length > 0 + && typeof attachmentId === 'string' && attachmentId.length > 0 + && typeof bytes === 'number' && Number.isSafeInteger(bytes) && bytes >= 0 + && (description === undefined || typeof description === 'string') +} + +/** + * Build an authenticated download coordinate for a saved delivery. + * @param sessionId - owning Session. + * @param seq - deliverables/presented event sequence. + * @param index - original index in the event's files array. + * @returns same-origin download URL. + */ +export function presentedFileUrl(sessionId: SessionId, seq: number, index: number): string { + return `${PRESENT_DOWNLOAD_PATH}?${new URLSearchParams({ sessionId, seq: String(seq), index: String(index) })}` +} + +/** + * Validate a delivery event before reading its turn or saved references. + * @param value - decoded durable event data. + * @returns whether the event identifies a turn, call, and file-reference list. + */ +export function isPresentedData(value: unknown): value is { turn: number; callId: ToolCallId; files: unknown[] } { + if (typeof value !== 'object' || value === null || Array.isArray(value)) return false + const { turn, callId, files } = value as Record + return typeof turn === 'number' && Number.isSafeInteger(turn) && turn >= 1 + && typeof callId === 'string' && callId.length > 0 && Array.isArray(files) +} + +/** + * Trailing path segment, the part that identifies the file at a glance. + * @param path - Slash- or backslash-separated path. + * @returns The final segment, or the whole string when separator-free. + */ +export function basename(path: string): string { + const at = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')) + return at === -1 ? path : path.slice(at + 1) +} diff --git a/packages/client/ui-deliverables/tests/present-download.host.spec.ts b/packages/client/ui-deliverables/tests/present-download.host.spec.ts new file mode 100644 index 0000000000..45f039cf07 --- /dev/null +++ b/packages/client/ui-deliverables/tests/present-download.host.spec.ts @@ -0,0 +1,198 @@ +/** Saved Presented file downloads over the real Connection route and local attachment store. */ +import { mkdtemp, rm } from 'node:fs/promises' +import { once } from 'node:events' +import { createServer } from 'node:http' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { Context } from '@deepseek-ai/cordis' +import { AttachmentError, AttachmentId } from '@deepseek-ai/dsh-attachment' +import LocalAttachmentStore from '@deepseek-ai/dsh-attachment-local' +import { HostConnectionService } from '@deepseek-ai/dsh-client-connection' +import { bridge } from '@deepseek-ai/dsh-client-connection/src/http-bridge.ts' +import type { BrowserAuth } from '@deepseek-ai/dsh-client-connection/src/browser-auth.ts' +import { SessionId } from '@deepseek-ai/dsh-session' +import type { SessionEvent } from '@deepseek-ai/dsh-session' +import type { PresentedFile } from '@deepseek-ai/dsh-tool-present/types' +import { SessionQueryError } from '@deepseek-ai/dsh-session-query' +import type { SessionEventReadRequest } from '@deepseek-ai/dsh-session-query' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { registerPresentDownload } from '../src/present-download.ts' +import { presentedFileUrl, PRESENT_DOWNLOAD_PATH } from '../src/presented.ts' + +const cleanups: Array<() => Promise> = [] +afterEach(async () => { + vi.restoreAllMocks() + for (const cleanup of cleanups.reverse()) await cleanup() + cleanups.length = 0 +}) + +async function fixture() { + const root = await mkdtemp(join(tmpdir(), 'dsh-present-download-')) + cleanups.push(() => rm(root, { recursive: true, force: true })) + const ctx = new Context() + cleanups.push(() => ctx.fiber.dispose()) + await ctx.plugin(LocalAttachmentStore, { dshHome: root }) + const ref = await ctx.attachments.saveFile({ data: Uint8Array.of(80, 75, 0, 255), name: "日记模板('1').docx" }) + const artifact: PresentedFile = { ...ref, path: 'deleted/日记模板.docx' } + const readEvent = vi.fn(async (request: SessionEventReadRequest) => { + if (request.sessionId !== 'owner') throw new SessionQueryError('missing', 'SESSION_QUERY_SESSION_NOT_FOUND') + if (request.seq !== 7) throw new SessionQueryError('missing', 'SESSION_QUERY_EVENT_NOT_FOUND') + return { target: { type: 'deliverables/presented', data: { turn: 1, callId: 'present-call', files: [artifact] } } as SessionEvent } + }) + ctx.provide('sessionQuery', { readEvent } as never) + const connection = new HostConnectionService(ctx, [], {} as BrowserAuth) + const fiber = ctx.plugin({ inject: ['connection', 'sessionQuery', 'attachments'], apply: registerPresentDownload }) + await fiber + const fetch = (query = '?sessionId=owner&seq=7&index=0', signal?: AbortSignal) => connection + .createSharedFetchHandler('/api').fetch(new Request(`http://localhost${PRESENT_DOWNLOAD_PATH}${query}`, { signal: signal ?? null })) + return { ctx, fiber, artifact, readEvent, fetch, handler: connection.createSharedFetchHandler('/api') } +} + +describe('Presented file download route', () => { + it('downloads the saved bytes with a Unicode filename without opening the workspace path', async () => { + const { fetch, fiber } = await fixture() + const response = await fetch() + expect(response.status).toBe(200) + expect(response.headers.get('content-disposition')).toBe("attachment; filename*=UTF-8''%E6%97%A5%E8%AE%B0%E6%A8%A1%E6%9D%BF%28%271%27%29.docx") + expect(response.headers.get('content-length')).toBeNull() + expect(response.headers.get('x-content-type-options')).toBe('nosniff') + expect(response.headers.get('cache-control')).toBe('no-store') + expect(new Uint8Array(await response.arrayBuffer())).toEqual(Uint8Array.of(80, 75, 0, 255)) + expect(presentedFileUrl(SessionId('owner'), 7, 0)).toBe(`${PRESENT_DOWNLOAD_PATH}?sessionId=owner&seq=7&index=0`) + await fiber.dispose() + expect((await fetch()).status).toBe(404) + }) + + it.each(['correct', 'smaller', 'zero', 'larger'] as const)('finishes HTTP downloads only after integrity verification: %s length', async (length) => { + const { ctx, artifact, handler } = await fixture() + const data = new Uint8Array(131072).fill(65) + Object.assign(artifact, await ctx.attachments.saveFile({ data, name: 'data.bin' })) + if (length !== 'correct') artifact.bytes = length === 'smaller' ? 1 : length === 'zero' ? 0 : data.byteLength + 1 + const errors: unknown[] = [] + const requests = new Set>() + const server = createServer((req, res) => { + const pending = bridge(req, res, handler).catch((error: unknown) => { + errors.push(error) + res.destroy() + }).finally(() => { requests.delete(pending) }) + requests.add(pending) + }) + cleanups.push(async () => { + await new Promise((resolve, reject) => { + server.close((error) => { + if (error) reject(error) + else resolve() + }) + server.closeAllConnections() + }) + await Promise.all(requests) + }) + server.listen(0, '127.0.0.1') + await once(server, 'listening') + const address = server.address() + if (address === null || typeof address === 'string') throw new Error('Expected a TCP listener') + const download = async () => { + const response = await fetch(`http://127.0.0.1:${address.port}${PRESENT_DOWNLOAD_PATH}?sessionId=owner&seq=7&index=0`) + return new Uint8Array(await response.arrayBuffer()) + } + if (length === 'correct') { + expect(await download()).toEqual(data) + expect(errors).toEqual([]) + } else { + await expect(download()).rejects.toThrow() + expect(errors).toEqual([expect.objectContaining({ code: 'ATTACHMENT_CORRUPT' })]) + } + }) + + it.each(['', '?seq=7&index=0', '?sessionId=owner&index=0', '?sessionId=owner&seq=7', + '?sessionId=owner&seq=-1&index=0', '?sessionId=owner&seq=7&index=0.1', + '?sessionId=owner&seq=9007199254740992&index=0', '?sessionId=owner&seq=7&index=9007199254740992', + ])('rejects invalid coordinates before reading: %s', async (query) => { + const { fetch, readEvent } = await fixture() + expect((await fetch(query)).status).toBe(400) + expect(readEvent).not.toHaveBeenCalled() + }) + + it('refuses unrelated Sessions, absent events, and undeclared Presented file indices', async () => { + const { fetch, readEvent } = await fixture() + expect((await fetch('?sessionId=other&seq=7&index=0')).status).toBe(404) + expect((await fetch('?sessionId=owner&seq=8&index=0')).status).toBe(404) + expect((await fetch('?sessionId=owner&seq=7&index=1')).status).toBe(404) + readEvent.mockResolvedValueOnce({ target: { type: 'turn/start' } as SessionEvent }) + expect((await fetch()).status).toBe(404) + readEvent.mockResolvedValueOnce({ target: { type: 'tool/result', data: {} } as SessionEvent }) + expect((await fetch()).status).toBe(404) + }) + + it('reports an absent snapshot and query failures without leaking Host paths', async () => { + const { fetch, artifact, readEvent } = await fixture() + artifact.attachmentId = AttachmentId(`sha256:${'0'.repeat(64)}`) + expect((await fetch()).status).toBe(404) + readEvent.mockRejectedValueOnce(new Error('/private/host/path')) + const response = await fetch() + expect(response.status).toBe(500) + expect(await response.text()).not.toContain('/private/host/path') + readEvent.mockRejectedValueOnce(new SessionQueryError('corrupt', 'SESSION_QUERY_CORRUPT_SESSION')) + expect((await fetch()).status).toBe(500) + }) + + it.each([null, { name: 4 }, 'invalid'])('returns 404 for a malformed recorded Presented file: %j', async (artifact) => { + const { ctx, fetch, readEvent } = await fixture() + const data: unknown = JSON.parse(JSON.stringify({ type: 'deliverables/presented', data: { turn: 1, callId: 'present-call', files: [artifact] } })) + readEvent.mockResolvedValueOnce({ target: data as SessionEvent }) + const read = vi.spyOn(ctx.attachments, 'readFileStream') + expect((await fetch()).status).toBe(404) + expect(read).not.toHaveBeenCalled() + }) + + it.each([null, [], 'invalid', {}, { turn: 1, callId: 'call', files: null }])('returns 404 for malformed delivery data: %j', async (data) => { + const { ctx, fetch, readEvent } = await fixture() + readEvent.mockResolvedValueOnce({ target: { type: 'deliverables/presented', data } as unknown as SessionEvent }) + const read = vi.spyOn(ctx.attachments, 'readFileStream') + expect((await fetch()).status).toBe(404) + expect(read).not.toHaveBeenCalled() + }) + + it('closes the provider iterator on browser cancellation, even before the first browser read', async () => { + const { ctx, fetch } = await fixture() + const returned = Promise.withResolvers() + const stop = vi.fn(async () => { returned.resolve(undefined); return { done: true as const, value: undefined } }) + vi.spyOn(ctx.attachments, 'readFileStream').mockReturnValue({ + [Symbol.asyncIterator]: () => ({ next: async () => ({ done: false, value: new Uint8Array(65536) }), return: stop }), + }) + const response = await fetch() + await response.body!.cancel() + await returned.promise + expect(stop).toHaveBeenCalledOnce() + }) + + it('delivers an empty saved file and reports a corrupt snapshot as a server failure', async () => { + const { ctx, fetch, artifact } = await fixture() + Object.assign(artifact, await ctx.attachments.saveFile({ data: new Uint8Array(), name: 'empty.txt' })) + const empty = await fetch() + expect(empty.status).toBe(200) + expect(await empty.text()).toBe('') + vi.spyOn(ctx.attachments, 'readFileStream').mockImplementation(async function* () { + throw new AttachmentError('corrupt', 'ATTACHMENT_CORRUPT') + }) + expect((await fetch()).status).toBe(500) + }) + + it('propagates cancellation while authorizing the download', async () => { + const { readEvent, fetch } = await fixture() + const controller = new AbortController() + controller.abort(new Error('cancelled')) + readEvent.mockRejectedValueOnce(controller.signal.reason) + await expect(fetch(undefined, controller.signal)).rejects.toThrow('cancelled') + }) + + it('fails the response stream if stored-byte integrity verification fails', async () => { + const { ctx, fetch } = await fixture() + vi.spyOn(ctx.attachments, 'readFileStream').mockImplementation(async function* () { + yield Uint8Array.of(1) + throw new AttachmentError('corrupt', 'ATTACHMENT_CORRUPT') + }) + const response = await fetch() + await expect(response.arrayBuffer()).rejects.toThrow('corrupt') + }) +}) diff --git a/packages/client/ui-deliverables/tests/present-row.client.spec.tsx b/packages/client/ui-deliverables/tests/present-row.client.spec.tsx new file mode 100644 index 0000000000..05d6b99540 --- /dev/null +++ b/packages/client/ui-deliverables/tests/present-row.client.spec.tsx @@ -0,0 +1,58 @@ +// @vitest-environment jsdom +/** Present UI derives statuses and details from durable tool records. */ +import { cleanup, fireEvent, render } from '@testing-library/react' +import { afterEach, expect, it, vi } from 'vitest' +import type { RunningToolCall, ToolResultNode } from '@deepseek-ai/dsh-client-ui-chat/client' +import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' +import { PresentRow } from '../src/client/PresentRow.tsx' +import { en } from '../src/client/locales.ts' + +afterEach(cleanup) +type Props = Parameters[0] +const running: RunningToolCall = { callId: 'p', name: 'present', argsRaw: '{"files":[{"path":"report.txt"}]}', turn: 1, step: 1, time: 1, subCalls: [] } +const settled: ToolResultNode = { kind: 'tool-result', seq: 2, time: 2, callId: 'p', call: { name: 'present', argsRaw: running.argsRaw }, callTime: 1, content: [{ type: 'text', text: 'Presented report.txt (4 bytes)' }], isError: false, subCalls: [] } +function props(block: Props['block'], inspect?: () => void): Props { + return { block, callId: 'p', toolName: 'present', openFile: vi.fn(), inspect, t: makeTranslate(en) } as Props +} + +it('discloses the saved result and offers call inspection', () => { + const inspect = vi.fn() + const view = render() + expect(view.getByText('Delivered')).toBeTruthy() + expect(view.getByText('report.txt')).toBeTruthy() + expect(view.queryByText('Presented report.txt (4 bytes)')).toBeNull() + const row = view.getByRole('button') + fireEvent.keyDown(row, { key: 'Enter' }) + expect(view.getByText('Presented report.txt (4 bytes)')).toBeTruthy() + fireEvent.click(view.getByRole('button', { name: 'Inspect call' })) + expect(inspect).toHaveBeenCalledOnce() + fireEvent.click(row) + expect(row.getAttribute('aria-expanded')).toBe('false') +}) + +it.each([ + [running, 'running', 'Delivering'], + [{ ...settled, isError: true }, 'error', 'Delivery failed'], + [{ ...settled, error: { name: 'Interrupted', code: 'interrupted' } }, 'stopped', 'Interrupted'], +] as const)('renders call lifecycle without claiming failed delivery', (block, state, label) => { + const view = render() + expect(view.container.querySelector('[data-tool="present"]')?.getAttribute('data-state')).toBe(state) + expect(view.getByText(label)).toBeTruthy() + expect(view.queryByText('Delivered')).toBeNull() +}) + +it.each(['', '{', 'null', '[]', '{"files":null}', '{"files":[null,{},1,{"path":false},{"path":"good.txt"}]}'])('tolerates partial arguments %s', (argsRaw) => { + const view = render() + expect(view.getByText('Delivering')).toBeTruthy() + expect(view.queryByRole('button')).toBeNull() +}) + +it('shows orphaned error details and non-text results', () => { + const view = render() + fireEvent.click(view.getByRole('button')) + expect(view.getByText('Missing: missing')).toBeTruthy() + view.rerender() + expect(view.container.textContent).toContain('Recorded detail') + view.rerender() + expect(view.queryByRole('button')).toBeNull() +}) diff --git a/packages/client/ui-deliverables/tests/produced-files.client.spec.tsx b/packages/client/ui-deliverables/tests/produced-files.client.spec.tsx index 9c006c93e7..058db67a8b 100644 --- a/packages/client/ui-deliverables/tests/produced-files.client.spec.tsx +++ b/packages/client/ui-deliverables/tests/produced-files.client.spec.tsx @@ -21,13 +21,15 @@ import { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client' import { apply as applyLocale, inject as localeInject } from '@deepseek-ai/dsh-client-locale/client' import type { ChatFileMentions, TurnTailOwnerProps } from '@deepseek-ai/dsh-client-ui-chat/client' import { makeTranslate, stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime' +import { Deliverables, selectDeliverables } from '../src/client/Deliverables.tsx' import { ProducedFiles } from '../src/client/ProducedFiles.tsx' import { - basename, deliverablesDefinition, producedFileMentions, producedForClosing, selectProducedFiles, + basename, deliverablesDefinition, presentedForClosing, producedFileMentions, producedForClosing, selectProducedFiles, type DeliverablesTurnData, } from '../src/client/turn-deliverables.ts' import { apply, inject } from '../src/client/index.ts' import { en, zh } from '../src/client/locales.ts' +import { SessionId } from '@deepseek-ai/dsh-session/types' import type { SessionEvent } from '@deepseek-ai/dsh-session/types' afterEach(() => { @@ -494,7 +496,7 @@ describe('plugin registration', () => { // The owning view's child declaration, stood up by a bench root entry. ctx.slots.register({ name: 'root', - children: { 'conversation.chat.turnTail': { kind: 'chain', scope: 'session' } }, + children: { 'conversation.chat.turnTail': { kind: 'chain', scope: 'session' }, 'tool.call.toolview': { kind: 'keyed', scope: 'session' } }, } as never, () => null) // ui-theme's Appearance row binds a durable scope through these two. const session = { @@ -513,6 +515,7 @@ describe('plugin registration', () => { await fiber.await() const [entry] = ctx.slots.entries('conversation.chat.turnTail') expect(entry).toBeDefined() + expect(ctx.slots.entries('tool.call.toolview')).toHaveLength(1) // The row needs no injected Host capability: it hands a path to its owner // and nothing in it reaches the local machine. expect(entry?.inject).toBeUndefined() @@ -526,15 +529,85 @@ describe('plugin registration', () => { (path) => { opened.push(path) }, ) const service = (ctx as unknown as { get(name: string): ChatFileMentions | undefined }).get('chatFileMentions') - const mentions = service?.forClosing(owner) + const mentions = service?.forClosing(owner, SessionId('viewed-session')) mentions?.resolve('report.html')?.open() expect(opened).toEqual(['site/report.html']) + const click = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(function (this: HTMLAnchorElement) { + expect(this.getAttribute('href')).toBe('/api/present.download?sessionId=child-session&seq=2&index=0') + expect(this.download).toBe('report.docx') + }) + const delivered = tailOwner({ produced: [], presented: [{ path: 'report.docx', name: 'report.docx', bytes: 4, attachmentId: 'saved' as never, seq: 2, index: 0 }] }, 3) + service?.forClosing(delivered, SessionId('child-session'))?.resolve('report.docx')?.open() + expect(click).toHaveBeenCalledOnce() // A turn that produced nothing yields no vocabulary at all. - expect(service?.forClosing(tailOwner(undefined, 2))).toBeUndefined() + expect(service?.forClosing(tailOwner(undefined, 2), SessionId('viewed-session'))).toBeUndefined() await fiber.dispose() expect(ctx.slots.entries('conversation.chat.turnTail')).toHaveLength(0) + expect(ctx.slots.entries('tool.call.toolview')).toHaveLength(0) // Fiber teardown retracts the service: the consumer's ctx.get sees the off state. expect((ctx as unknown as { get(name: string): unknown }).get('chatFileMentions')).toBeUndefined() }) }) + + +describe('presented files', () => { + const file = (path = 'report.docx') => ({ path, name: path, bytes: 4, attachmentId: 'saved-ref' }) + + it('replays deliveries without mutation calls, preserves indices, and isolates turns', () => { + const value = assembler([ + at(1, 'turn/start', { turn: 1 }), + at(2, 'deliverables/presented', { turn: 1, callId: 'nested', files: [null, { ...file(), description: 'Final report' }] }), + at(3, 'deliverables/presented', { turn: 1, callId: 'again', files: [{ ...file(), attachmentId: 'new-ref' }] }), + at(4, 'turn/end', { turn: 1 }), + at(5, 'turn/start', { turn: 2 }), + ]) + const first = presentedForClosing(tailOwner(deliverablesOf(value), 3)) + expect(first).toMatchObject([{ path: 'report.docx', seq: 2, index: 1, attachmentId: 'saved-ref', description: 'Final report' }]) + expect(presentedForClosing(tailOwner(deliverablesOf(value), 4))) + .toMatchObject([{ path: 'report.docx', seq: 3, attachmentId: 'new-ref' }]) + expect(selectDeliverables(tailOwner(deliverablesOf(value, 2), 9))).toBeNull() + }) + + it('uses the viewed fork Session in every download and retains all delivered files', () => { + const value = assembler([ + at(1, 'turn/start', { turn: 1 }), + at(2, 'deliverables/presented', { turn: 1, callId: 'nested', files: Array.from({ length: 8 }, (_, i) => file(`report-${i}.docx`)) }), + ]) + const owner = tailOwner(deliverablesOf(value), 3) + const matched = selectDeliverables(owner)! + const view = render() + expect(view.getAllByRole('link')).toHaveLength(8) + expect(view.getAllByRole('link')[0]?.getAttribute('href')).toBe('/api/present.download?sessionId=child-session&seq=2&index=0') + expect(view.queryByText('Produced')).toBeNull() + }) +}) + + +it.each([null, [], 'invalid', {}, { turn: '1', callId: 'bad', files: [] }, + { turn: 1.5, callId: 'bad', files: [] }, { turn: 0, callId: 'bad', files: [] }, + { turn: 1, files: [] }, { turn: 1, callId: '', files: [] }, { turn: 1, callId: 'bad', files: null }, +])('ignores malformed delivery data and keeps the existing produced row: %j', (data) => { + const value = assembler([ + at(1, 'turn/start', { turn: 1 }), + call(2, 'write-a', 'write', { file_path: 'a.txt', content: 'a' }), + result(3, 'write-a'), + at(4, 'deliverables/presented', data), + ]) + const owner = tailOwner(deliverablesOf(value), 5) + const matched = selectDeliverables(owner)! + const view = render() + expect(view.getByText('Produced')).toBeTruthy() + expect(view.queryByText('Deliverables')).toBeNull() +}) + +it('shows file metadata and descriptions without hiding extensionless deliveries', () => { + const view = render( {}} sessionId={SessionId('session')} t={makeTranslate(en)} />) + expect(view.getByText('Quarterly summary')).toBeTruthy() + expect(view.getByText('TXT · 4.0KB')).toBeTruthy() + expect(view.getByText('File · 0B')).toBeTruthy() + expect(view.getByRole('link', { name: 'Download out/report.txt' }).getAttribute('title')).toBe('out/report.txt') +}) diff --git a/packages/client/ui-deliverables/tests/prompt.client.spec.ts b/packages/client/ui-deliverables/tests/prompt.host.spec.ts similarity index 88% rename from packages/client/ui-deliverables/tests/prompt.client.spec.ts rename to packages/client/ui-deliverables/tests/prompt.host.spec.ts index 9c2ee24471..042699d6d3 100644 --- a/packages/client/ui-deliverables/tests/prompt.client.spec.ts +++ b/packages/client/ui-deliverables/tests/prompt.host.spec.ts @@ -16,6 +16,9 @@ describe('ui-deliverables node plugin', () => { it('registers final-response file-reference guidance only while mounted', async () => { ctx = new Context() await ctx.plugin(SystemPrompt, { personaPrefix: '' }) + ctx.provide('connection', { fetch: { register: () => () => {} } } as never) + ctx.provide('sessionQuery', {} as never) + ctx.provide('attachments', {} as never) const mounted = ctx.plugin({ apply, inject }) await mounted.await() diff --git a/packages/client/ui-deliverables/tsconfig.client.json b/packages/client/ui-deliverables/tsconfig.client.json new file mode 100644 index 0000000000..989b773565 --- /dev/null +++ b/packages/client/ui-deliverables/tsconfig.client.json @@ -0,0 +1,60 @@ +{ + "extends": "../../../tsconfig.base.client.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types", + "tsBuildInfoFile": "lib/tsconfig.client.tsbuildinfo" + }, + "include": [ + "src/client", + "src/presented.ts", + "src/css-modules.d.ts" + ], + "references": [ + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../api/remotes/tsconfig.client.json" + }, + { + "path": "../connection/tsconfig.client.json" + }, + { + "path": "../locale" + }, + { + "path": "../store" + }, + { + "path": "../ui-conversation" + }, + { + "path": "../ui-chat" + }, + { + "path": "../ui-renderer" + }, + { + "path": "../ui-primitives" + }, + { + "path": "../ui-slots" + }, + { + "path": "../../core/session" + }, + { + "path": "../../attachment/attachment" + }, + { + "path": "../../llm/llm" + }, + { + "path": "../ui-tool" + }, + { + "path": "../../fs/tool-present" + } + ] +} diff --git a/packages/client/ui-deliverables/tsconfig.host.json b/packages/client/ui-deliverables/tsconfig.host.json new file mode 100644 index 0000000000..39f2f8a2cc --- /dev/null +++ b/packages/client/ui-deliverables/tsconfig.host.json @@ -0,0 +1,39 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types", + "tsBuildInfoFile": "lib/tsconfig.host.tsbuildinfo" + }, + "files": [ + "src/index.ts", + "src/present-download.ts", + "src/presented.ts" + ], + "references": [ + { + "path": "../../../vendor/cordis" + }, + { + "path": "../connection/tsconfig.host.json" + }, + { + "path": "../../attachment/attachment" + }, + { + "path": "../../core/session" + }, + { + "path": "../../core/system-prompt" + }, + { + "path": "../../session-query/session-query" + }, + { + "path": "../../llm/llm" + }, + { + "path": "../../fs/tool-present" + } + ] +} diff --git a/packages/client/ui-deliverables/tsconfig.json b/packages/client/ui-deliverables/tsconfig.json index a99881d93b..2eca820546 100644 --- a/packages/client/ui-deliverables/tsconfig.json +++ b/packages/client/ui-deliverables/tsconfig.json @@ -1,48 +1,11 @@ { - "extends": "../../../tsconfig.base.client.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "lib/types" - }, - "include": [ - "src" - ], + "files": [], "references": [ { - "path": "../../api/remotes/tsconfig.client.json" + "path": "./tsconfig.host.json" }, { - "path": "../../../vendor/cordis" - }, - { - "path": "../connection/tsconfig.client.json" - }, - { - "path": "../locale" - }, - { - "path": "../store" - }, - { - "path": "../ui-conversation" - }, - { - "path": "../ui-chat" - }, - { - "path": "../ui-renderer" - }, - { - "path": "../ui-primitives" - }, - { - "path": "../ui-slots" - }, - { - "path": "../../core/system-prompt" - }, - { - "path": "../../core/session" + "path": "./tsconfig.client.json" } ] } diff --git a/packages/core/session/src/known-event-types.ts b/packages/core/session/src/known-event-types.ts index decb838cb5..dd6411240b 100644 --- a/packages/core/session/src/known-event-types.ts +++ b/packages/core/session/src/known-event-types.ts @@ -33,6 +33,7 @@ export const KNOWN_SESSION_EVENT_TYPES: ReadonlySet = new Set([ 'compaction/prune', 'compaction/start', 'compaction/summary', + 'deliverables/presented', 'feedback/message-delete', 'feedback/message-put', 'feedback/record', diff --git a/packages/core/tools/tests/gen-tool-catalog.spec.ts b/packages/core/tools/tests/gen-tool-catalog.spec.ts index 0630cfaa8e..c26ef3ff28 100644 --- a/packages/core/tools/tests/gen-tool-catalog.spec.ts +++ b/packages/core/tools/tests/gen-tool-catalog.spec.ts @@ -30,7 +30,7 @@ describe('gen-tool-catalog collectToolCatalog', () => { 'cordis_inspect_query', 'cordis_inspect_self', 'cordis_run', 'cordis_stop', 'cordis_undefine', 'create_goal', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep', 'interrupt_agent', 'interrupt_agent', 'job_kill', 'job_list', 'job_output', - 'list_agents', 'list_agents', 'list_subagent_models', 'lsp', 'pwsh', 'pwsh', 'ralph', + 'list_agents', 'list_agents', 'list_subagent_models', 'lsp', 'present', 'pwsh', 'pwsh', 'ralph', 'read', 'read_image', 'run_code', 'schedule_create', 'schedule_delete', 'schedule_list', 'send_message', 'send_message', 'session_event_read', 'session_event_search', 'session_event_trace', 'session_search', 'session_trace', 'skill', 'spawn_teammate', diff --git a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts index d6232bd312..11bda279be 100644 --- a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts @@ -174,7 +174,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.chat.assistant-actions\', () => ctx.slots.register(\n { name: \'conversation.chat.assistant-actions\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-chat/src/client/contract/slots.ts:212', + source: 'packages/client/ui-chat/src/client/contract/slots.ts:213', }, { key: 'conversation.chat.commandview', @@ -221,7 +221,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ occupants: [], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.chat.commandview\', () => ctx.slots.register(\n { name: \'conversation.chat.commandview\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-chat/src/client/contract/slots.ts:200', + source: 'packages/client/ui-chat/src/client/contract/slots.ts:201', }, { key: 'conversation.chat.node', @@ -289,7 +289,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'shadows-shipped-ui', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.chat.node\', () => ctx.slots.register(\n { name: \'conversation.chat.node\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-chat/src/client/contract/slots.ts:181', + source: 'packages/client/ui-chat/src/client/contract/slots.ts:182', }, { key: 'conversation.chat.turnTail', @@ -332,11 +332,11 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ slotInject: '', declaredBy: 'an entry in \'conversation.chat.node\' (client-ui-chat), so it exists while that entry is mounted', occupants: [ - 'client-ui-deliverables ProducedFiles', + 'client-ui-deliverables Deliverables', ], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.chat.turnTail\', () => ctx.slots.register(\n { name: \'conversation.chat.turnTail\', select: owner => null },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-chat/src/client/contract/slots.ts:206', + source: 'packages/client/ui-chat/src/client/contract/slots.ts:207', }, { key: 'conversation.composer', @@ -983,7 +983,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'shadows-shipped-ui', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.message.images\', () => ctx.slots.register(\n { name: \'conversation.message.images\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-chat/src/client/contract/slots.ts:194', + source: 'packages/client/ui-chat/src/client/contract/slots.ts:195', }, { key: 'conversation.session', @@ -2576,11 +2576,12 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ 'useProjection: UseProjection', 'useTrajectory: UseTrajectory', ], - keyDomain: 'open: any string the owner dispatches (no compile-time key set), already taken: ask_user_question, bash, cordis_define, cordis_run, cordis_stop, cordis_undefine, edit, glob, grep, read, read_image, skill, todo_write, web_fetch, web_search, write', + keyDomain: 'open: any string the owner dispatches (no compile-time key set), already taken: ask_user_question, bash, cordis_define, cordis_run, cordis_stop, cordis_undefine, edit, glob, grep, present, read, read_image, skill, todo_write, web_fetch, web_search, write', hookContext: '', slotInject: '', declaredBy: 'an entry in \'conversation.chat.node\' (client-ui-tool), so it exists while that entry is mounted', occupants: [ + 'client-ui-deliverables PresentRow key \'present\'', 'client-ui-skill SkillRow key \'skill\'', 'client-ui-tool AskQuestionRow key \'ask_user_question\'', 'client-ui-tool BashRow key \'bash\'', diff --git a/packages/fs/README.i18n.yaml b/packages/fs/README.i18n.yaml index 5311da4c5b..6f879b8f5f 100644 --- a/packages/fs/README.i18n.yaml +++ b/packages/fs/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/fs/README.md -README.md: c8cfc5f8d5dd5d225149abbf192d23df1b51cf71 -README.zh.md: 9596f72e76241de43072ee590c141b043af6a861 +README.md: dd3e6f1fab3a8439dd8e2ff15f854e2872549eef +README.zh.md: 6e0b34b2b9ca7a8076f47c4c3302c67b91e200da diff --git a/packages/fs/README.md b/packages/fs/README.md index c8cfc5f8d5..dd3e6f1fab 100644 --- a/packages/fs/README.md +++ b/packages/fs/README.md @@ -22,7 +22,7 @@ The `fs/` group gives agents durable, policy-governed access to files: the `ctx. ## Packages -Seven packages plus the remote sibling `fs-e2b` play the filesystem roles; the subsystem reference owns the exhaustive contracts and the error taxonomy. +Eight packages plus the remote sibling `fs-e2b` play the filesystem roles; the subsystem reference owns the exhaustive contracts and the error taxonomy. | Package | Role | ctx key | |---|---|---| @@ -34,6 +34,7 @@ Seven packages plus the remote sibling `fs-e2b` play the filesystem roles; the s | [`tool-fs/`](tool-fs/README.md) | Model-facing `read`, `read_image`, `write`, and `edit` tools plus their executor | registers on `ctx.tools` | | [`tool-fs-search/`](tool-fs-search/README.md) | Model-facing `glob` and `grep` discovery tools backed by the packaged ripgrep binary | registers on `ctx.tools` | | [`tool-str-replace-editor/`](tool-str-replace-editor/README.md) | Standalone `str_replace_editor` tool: `view`, `create`, `str_replace`, and `insert` over `ctx.fs` | registers on `ctx.tools` | +| [`tool-present/`](tool-present/README.md) | Explicit immutable snapshots of delivered files | registers on `ctx.tools` | The policy is a plugin, not a service the tools inject: removing it leaves the bare provider's unconditional mutation behavior instead of breaking the tools. The mode fence in `fs-sandbox` and the read-before-edit gate compose. `tool-fs-search` deliberately does not extend the provider contract — search is a process-backed ripgrep workflow, so filesystem backends stay free of a universal search API. diff --git a/packages/fs/README.zh.md b/packages/fs/README.zh.md index 9596f72e76..6e0b34b2b9 100644 --- a/packages/fs/README.zh.md +++ b/packages/fs/README.zh.md @@ -22,7 +22,7 @@ kind: "package-group" ## 包 -七个包加上远程同级 `fs-e2b` 承担文件系统角色;子系统参考文档拥有穷尽式约定与错误分类体系。 +八个包加上远程同级 `fs-e2b` 承担文件系统角色;子系统参考文档拥有穷尽式约定与错误分类体系。 | 包 | 职责 | ctx 键 | |---|---|---| @@ -34,6 +34,7 @@ kind: "package-group" | [`tool-fs/`](tool-fs/README.zh.md) | 面向模型的 `read`、`read_image`、`write` 与 `edit` 工具及其执行器 | 注册到 `ctx.tools` | | [`tool-fs-search/`](tool-fs-search/README.zh.md) | 由打包 ripgrep 二进制支持的面向模型 `glob` 与 `grep` 发现工具 | 注册到 `ctx.tools` | | [`tool-str-replace-editor/`](tool-str-replace-editor/README.zh.md) | 独立的 `str_replace_editor` 工具:基于 `ctx.fs` 的 `view`、`create`、`str_replace` 与 `insert` | 注册到 `ctx.tools` | +| [`tool-present/`](tool-present/README.zh.md) | 显式保存交付文件的不可变快照 | 注册到 `ctx.tools` | 策略是插件,不是工具注入的服务:移除它只会让工具回到裸提供方的无条件变更行为,而不会破坏工具。`fs-sandbox` 的模式围栏与编辑前读取门禁可以组合。`tool-fs-search` 有意不扩展提供方约定——搜索是由进程支持的 ripgrep 工作流,因此文件系统后端无需承担通用搜索 API。 diff --git a/packages/fs/tool-present/README.i18n.yaml b/packages/fs/tool-present/README.i18n.yaml new file mode 100644 index 0000000000..24d2c2f70f --- /dev/null +++ b/packages/fs/tool-present/README.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write packages/fs/tool-present/README.md +README.md: a4f18712e77eda7f4e6a9da358db3916ef5cd815 +README.zh.md: 2b3671ab39eeaf6679e0f0b76560d2c2a2b6b563 diff --git a/packages/fs/tool-present/README.md b/packages/fs/tool-present/README.md new file mode 100644 index 0000000000..a4f18712e7 --- /dev/null +++ b/packages/fs/tool-present/README.md @@ -0,0 +1,105 @@ +--- +description: "Deliver immutable snapshots of workspace files with the present tool; configuration, Session ownership, and download prerequisites." +kind: "package-reference" +--- + +# @deepseek-ai/dsh-tool-present + +English | [中文](README.zh.md) + +## Summary + +Use `present` to deliver final workspace files, including files created through shell commands. Each successful call saves immutable bytes, so users can download the delivered version after source edits or deletion. The calling Session owns the delivery; the Web deliverables plugin supplies download links and cards. + +## Table of Contents + +- [Use this package](#use-this-package) +- [Understand the implementation](#understand-the-implementation) +- [Further Exploration](#further-exploration) +- [Model Experience](#model-experience) +- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work) +- [Dev Note](#dev-note) + +----- + + +## Use this package + +The `standard`, `ptc`, and `cordis` agent presets mount this plugin. Call `present` with `files: [{ path, description? }]` after creating the files. Files must exist inside the Session workspace and be regular files. Missing, oversized, outside-workspace, or concurrently modified files fail the call. + +Mount it in an agent's Cordis composition with `tools`, `fs`, `attachments`, and the `turnBoundary` Session projection available: + +```yaml +- name: '@deepseek-ai/dsh-tool-present' + config: + maxFileBytes: 104857600 + maxFiles: 8 +``` + +| Field | Default | Meaning | +|---|---|---| +| `maxFileBytes` | `104857600` | Positive per-file byte cap, at most 100 MiB | +| `maxFiles` | `8` | Positive maximum file count per call | + +Limits are validated at mount. The tool requires an agent Session with a workspace and an open turn. Delivery belongs to the calling Session; a parent must call `present` itself to offer its own download links for files created by a subagent. + +----- + + +## Understand the implementation + +
    +Implementation internals — click to expand + +The tool resolves paths through the configured filesystem provider, checks workspace containment and versions around bounded reads, and saves bytes through the attachment service. Successful final `tools/result` notifications append `deliverables/presented`, including nested calls. A later enclosing program failure does not revoke an already completed delivery. Blocked results publish no delivery. Each plugin instance records only snapshots from calls it executed; scoped tools with the same name cannot publish through another instance. + +The pure `./types` entry declares `PresentedFile` and the Session event without importing Host runtime code. The Web consumer validates persisted references before displaying them or authorizing downloads. The event stores no Session ID, so forked history authorizes downloads through the viewed Session. + +**Runtime invariant:** No companion is published. Tool and event registrations are effect-owned; the attachment service owns immutable bytes, and the Session log owns delivery references. + +
    + +----- + + +## Further Exploration + +- [Filesystem subsystem](../../../docs/subsystems/filesystem.md) — provider paths and errors. +- [Attachment service](../../attachment/attachment/README.md) — saved bytes and retention. +- [Web deliverables](../../client/ui-deliverables/README.md) — authenticated downloads and cards. +- [Delivery decision](../../../.agents/notes/implemented/feature/2026-09-08-web-explicit-file-delivery.md) — Session ownership and required-on-read events. + + +## Model Experience + +### present + +#### What the model sees + +The [present schema](../../../docs/tool-catalog.md#present) asks for existing workspace files: “Deliver final files to the user. Saves a snapshot of each existing workspace file so it remains downloadable after edits or deletion. Create the files before calling this tool.” Results report `Presented ( bytes)` for each file; attachment IDs remain in the program result and durable event. + +#### Token effect + +One tool schema per mounted agent and one result line per delivered file. File bytes do not enter model messages. + +#### KV Cache effect + +The tool schema is static for the mount lifetime. Delivery result text extends the conversation without rewriting its prompt prefix. + +## Known Limitations and Deferred Work + + + +- Containment and before/after version checks reject ordinary changes, but the path API cannot atomically defend against malicious swap-and-restore. +- Files saved before a failed call can remain unreferenced in the attachment store. +- Session ZIP exports retain delivery references in JSONL and omit delivered bytes. Downloads after transfer require the same snapshots in the serving host's attachment store. + + +### Dev Note + +
    +Working context for maintainers — click to expand + +None. + +
    diff --git a/packages/fs/tool-present/README.zh.md b/packages/fs/tool-present/README.zh.md new file mode 100644 index 0000000000..2b3671ab39 --- /dev/null +++ b/packages/fs/tool-present/README.zh.md @@ -0,0 +1,105 @@ +--- +description: "通过 present 工具交付工作区文件的不可变快照;配置、Session 归属与下载前提。" +kind: "package-reference" +--- + +# @deepseek-ai/dsh-tool-present + +[English](README.md) | 中文 + +## 概述 + +使用 `present` 交付最终工作区文件,包括通过 shell 命令创建的文件。每次成功调用都会保存不可变字节,因此源文件被编辑或删除后,用户仍可下载交付时的版本。交付归调用方 Session 所有;Web 交付插件提供下载链接和卡片。 + +## 目录 + +- [使用本包](#use-this-package) +- [理解实现](#understand-the-implementation) +- [进一步探索](#further-exploration) +- [模型体验](#model-experience) +- [已知限制与延期工作](#known-limitations-and-deferred-work) +- [开发备注](#dev-note) + +----- + + +## 使用本包 + +`standard`、`ptc` 与 `cordis` Agent preset 挂载本插件。创建文件后,以 `files: [{ path, description? }]` 调用 `present`。文件必须存在于 Session 工作区内,且为普通文件。文件缺失、超限、位于工作区外或读取期间发生变化时,调用失败。 + +在 Agent 的 Cordis 组合中挂载,并提供 `tools`、`fs`、`attachments` 和 `turnBoundary` Session 投影: + +```yaml +- name: '@deepseek-ai/dsh-tool-present' + config: + maxFileBytes: 104857600 + maxFiles: 8 +``` + +| 字段 | 默认值 | 含义 | +|---|---|---| +| `maxFileBytes` | `104857600` | 每个文件的字节上限,为正整数且不超过 100 MiB | +| `maxFiles` | `8` | 每次调用的最大文件数,为正整数 | + +挂载时校验限制。工具要求 Agent Session 具有工作区和已开始的 turn。交付归调用方 Session 所有;父 Session 如需为子 Agent 创建的文件提供自己的下载链接,必须自行调用 `present`。 + +----- + + +## 理解实现 + +
    +实现细节——点击展开 + +工具通过配置的文件系统提供方解析路径,检查工作区包含关系和有界读取前后的版本,并通过 attachment 服务保存字节。成功的最终 `tools/result` 通知追加 `deliverables/presented`,嵌套调用也适用。外层程序随后失败不会撤销已完成的交付。被阻止的结果不发布交付。每个插件实例只记录其实际执行调用保存的快照;同名作用域工具不能通过其他实例发布交付。 + +纯 `./types` 入口声明 `PresentedFile` 与 Session 事件,不导入 Host 运行时代码。Web 消费方在展示或授权下载前校验持久引用。事件不保存 Session ID,因此 fork 历史通过当前查看的 Session 授权下载。 + +**运行时不变式:** 不发布伴生入口。工具与事件注册归 effect 所有;attachment 服务拥有不可变字节,Session 日志拥有交付引用。 + +
    + +----- + + +## 进一步探索 + +- [文件系统子系统](../../../docs/subsystems/filesystem.zh.md)——提供方路径与错误。 +- [Attachment 服务](../../attachment/attachment/README.zh.md)——保存的字节与保留策略。 +- [Web 交付](../../client/ui-deliverables/README.zh.md)——认证下载与卡片。 +- [交付决策](../../../.agents/notes/implemented/feature/2026-09-08-web-explicit-file-delivery.zh.md)——Session 归属与读取端必须识别的事件。 + + +## 模型体验 + +### present + +#### 模型看到的内容 + +[present schema](../../../docs/tool-catalog.zh.md#present)要求已有的工作区文件:“Deliver final files to the user. Saves a snapshot of each existing workspace file so it remains downloadable after edits or deletion. Create the files before calling this tool.” 每个文件的结果为 `Presented ( bytes)`;attachment ID 保留在程序结果和持久事件中。 + +#### Token 影响 + +每个挂载的 Agent 增加一个工具 schema,每个交付文件增加一行结果。文件字节不进入模型消息。 + +#### KV Cache 影响 + +工具 schema 在挂载期间保持静态。交付结果文本扩展对话,不重写提示词前缀。 + +## 已知限制与延期工作 + + + +- 路径包含关系和读取前后版本校验会拒绝普通变化,但路径 API 无法原子防御恶意替换后复原。 +- 失败调用此前保存的文件可能作为无引用对象留在 attachment 存储中。 +- Session ZIP 导出在 JSONL 中保留交付引用,不包含交付字节。转移后下载依赖服务主机 attachment 存储中的同一快照。 + + +### 开发备注 + +
    +维护者的工作上下文——点击展开 + +无。 + +
    diff --git a/packages/fs/tool-present/package.json b/packages/fs/tool-present/package.json new file mode 100644 index 0000000000..6cb32cbc8a --- /dev/null +++ b/packages/fs/tool-present/package.json @@ -0,0 +1,63 @@ +{ + "name": "@deepseek-ai/dsh-tool-present", + "description": "Explicit immutable file delivery snapshots for the DeepSeek Harness", + "version": "0.1.3-alpha.2", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", + "directory": "packages/fs/tool-present" + }, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./types": { + "types": "./lib/types/types.d.ts", + "default": "./lib/types/types.js" + }, + "./src/*": "./src/*", + "./package.json": "./package.json" + }, + "license": "MIT", + "files": [ + "lib/index.js", + "lib/types/**/*.js", + "lib/types/**/*.d.ts" + ], + "dependencies": { + "@deepseek-ai/schemastery": "workspace:^" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "workspace:^", + "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-attachment": "workspace:^", + "@deepseek-ai/dsh-fs": "workspace:^", + "@deepseek-ai/dsh-llm": "workspace:^", + "@deepseek-ai/dsh-session": "workspace:^", + "@deepseek-ai/dsh-session-projection": "workspace:^", + "@deepseek-ai/dsh-tools": "workspace:^" + }, + "devDependencies": { + "@deepseek-ai/cordis": "workspace:^", + "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-attachment": "workspace:^", + "@deepseek-ai/dsh-fs": "workspace:^", + "@deepseek-ai/dsh-llm": "workspace:^", + "@deepseek-ai/dsh-session": "workspace:^", + "@deepseek-ai/dsh-session-projection": "workspace:^", + "@deepseek-ai/dsh-tools": "workspace:^", + "@deepseek-ai/dsh-agent-loop": "workspace:^", + "@deepseek-ai/dsh-agent-loop-testkit": "workspace:^", + "@deepseek-ai/dsh-attachment-local": "workspace:^", + "@deepseek-ai/dsh-fs-local": "workspace:^", + "@deepseek-ai/dsh-system-prompt": "workspace:^", + "@deepseek-ai/dsh-scope": "workspace:^" + } +} diff --git a/packages/fs/tool-present/src/index.ts b/packages/fs/tool-present/src/index.ts new file mode 100644 index 0000000000..429ec8f92e --- /dev/null +++ b/packages/fs/tool-present/src/index.ts @@ -0,0 +1,121 @@ +/** Scoped tool that saves immutable file deliveries and records their owning Session. */ +import type { Context } from '@deepseek-ai/cordis' +import z from '@deepseek-ai/schemastery' +import { FsError } from '@deepseek-ai/dsh-fs' +import { defineTool, type ToolExecution } from '@deepseek-ai/dsh-tools' +import type {} from '@deepseek-ai/dsh-attachment' +import type {} from '@deepseek-ai/dsh-agent' +import type {} from '@deepseek-ai/dsh-session-projection' +import type { Session } from '@deepseek-ai/dsh-session' +import type { PresentedFile } from './types.ts' + +/** Stable Loader identity. */ +export const name = 'tool-present' + +/** Per-call snapshot limits. */ +export interface Config { + /** Inclusive per-file byte cap; at most 100 MiB. */ + maxFileBytes: number + /** Maximum number of files in one call. */ + maxFiles: number +} + +/** Validated snapshot limits. */ +export const Config: z = z.object({ + maxFileBytes: z.number().default(100 * 1024 * 1024), + maxFiles: z.number().default(8), +}) + +/** Services used by the scoped snapshot tool. */ +export const inject = ['tools', 'fs', 'attachments', 'sessionProjections'] + +/** + * Register present with durable file references in its tool result. + * @param ctx - agent-scoped services. + * @param config - per-file and per-call limits. + */ +export function apply(ctx: Context, config: Config): void { + if (!Number.isSafeInteger(config.maxFileBytes) || config.maxFileBytes < 1 || config.maxFileBytes > 100 * 1024 * 1024 + || !Number.isSafeInteger(config.maxFiles) || config.maxFiles < 1) { + throw new Error('present requires positive integer limits; maxFileBytes must not exceed 100 MiB') + } + const pending = new WeakMap() + ctx.tools.register(defineTool({ + name: 'present', + description: 'Deliver final files to the user. Saves a snapshot of each existing workspace file so it remains downloadable after edits or deletion. Create the files before calling this tool.', + parameters: { + files: { + type: 'array', required: true, + items: { + type: 'object', additionalProperties: false, + properties: { + path: { type: 'string', required: true, description: 'Path of an existing file inside the workspace.' }, + description: { type: 'string', description: 'Brief description for the user.' }, + }, + }, + }, + }, + output: { + schema: { + type: 'object', additionalProperties: false, + properties: { + turn: { type: 'integer', required: true }, + files: { + type: 'array', required: true, + items: { + type: 'object', additionalProperties: false, + properties: { + path: { type: 'string', required: true }, name: { type: 'string', required: true }, + attachmentId: { type: 'string', required: true }, bytes: { type: 'integer', required: true }, + description: { type: 'string' }, + }, + }, + }, + }, + }, + render: (_args, value) => [{ type: 'text', text: value.files.map(file => `Presented ${file.path} (${file.bytes} bytes)`).join('\n') }], + }, + async execute(args, exec) { + if (exec.agent === undefined) throw new Error('present requires an agent Session') + const boundary = ctx.sessionProjections.stateOf(exec.agent.session, 'turnBoundary') + if (boundary === undefined || boundary.openTurnStartSeq === null) throw new Error('present requires an open turn') + if (args.files.length === 0 || args.files.length > config.maxFiles) throw new Error(`present accepts 1 to ${config.maxFiles} files`) + const cwd = exec.agent.session.header.cwd + if (cwd === undefined) throw new Error('present requires a workspace') + const options = { cwd, signal: exec.signal } + const root = await ctx.fs.resolve('.', options) + const admitted = [] + for (const file of args.files) { + if (file.path.trim().length === 0) throw new Error('present requires a non-empty file path') + const target = await ctx.fs.resolve(file.path, options) + if (!ctx.fs.contains(root, target)) throw new Error(`Cannot present ${file.path}: outside the workspace`) + const info = await ctx.fs.stat(target, exec.signal) + if (info === undefined) throw new FsError(`Cannot present ${file.path}: file not found. Check the path, create the file if needed, and retry.`, 'FS_NOT_FOUND') + if (info.type !== 'file') throw new Error(`Cannot present ${file.path}: not a regular file`) + if (info.size !== undefined && info.size > config.maxFileBytes) throw new FsError(`Cannot present ${file.path}: file exceeds ${config.maxFileBytes} bytes`, 'FS_TOO_LARGE') + admitted.push({ file, target, version: info.version }) + } + const files = [] + for (const { file, target, version } of admitted) { + const data = await ctx.fs.readBytes(target, exec.signal, config.maxFileBytes) + const after = await ctx.fs.stat(target, exec.signal) + if (after?.version !== version) throw new FsError(`Cannot present ${file.path}: file changed while reading; retry.`, 'FS_STALE_VERSION') + exec.signal.throwIfAborted() + const name = file.path.slice(Math.max(file.path.lastIndexOf('/'), file.path.lastIndexOf('\\')) + 1) + const ref = await ctx.attachments.saveFile({ data, name }) + files.push({ ...file, ...ref }) + } + pending.set(exec, { session: exec.agent.session, turn: boundary.lastTurn, files }) + return { turn: boundary.lastTurn, files } + }, + })) + ctx.on('tools/result', (exec, result) => { + const delivery = pending.get(exec) + pending.delete(exec) + if (delivery === undefined || result.isError) return + const { session, turn, files } = delivery + session.append('deliverables/presented', { + turn, callId: exec.callId, files, + }) + }) +} diff --git a/packages/fs/tool-present/src/types.ts b/packages/fs/tool-present/src/types.ts new file mode 100644 index 0000000000..0c3d1816af --- /dev/null +++ b/packages/fs/tool-present/src/types.ts @@ -0,0 +1,18 @@ +/** Durable file deliveries produced by the present tool. */ +import type { FileAttachmentRef } from '@deepseek-ai/dsh-attachment/types' +import type { ToolCallId } from '@deepseek-ai/dsh-llm/brand' + +/** A saved file and the model-selected path it came from. */ +export interface PresentedFile extends FileAttachmentRef { + /** Original workspace path. */ + path: string + /** Optional description supplied by the model. */ + description?: string +} + +declare module '@deepseek-ai/dsh-session/types' { + interface SessionEventMap { + /** Saved deliveries from a successful final present result, including nested calls. */ + 'deliverables/presented': { turn: number; callId: ToolCallId; files: PresentedFile[] } + } +} diff --git a/packages/fs/tool-present/tests/built-errors.e2e.ts b/packages/fs/tool-present/tests/built-errors.e2e.ts new file mode 100644 index 0000000000..369e21b4d7 --- /dev/null +++ b/packages/fs/tool-present/tests/built-errors.e2e.ts @@ -0,0 +1,65 @@ +/** Built tool and runtime bundles must preserve their shared structured error classes. */ +import { execFile } from 'node:child_process' +import { existsSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { promisify } from 'node:util' +import { expect, it } from 'vitest' + +const repoRoot = fileURLToPath(new URL('../../../../', import.meta.url)) +const bundle = fileURLToPath(new URL('../lib/index.js', import.meta.url)) +const execFileAsync = promisify(execFile) +const probe = String.raw` +import assert from 'node:assert/strict' +import { Context } from './vendor/cordis/lib/index.js' +import AgentRegistry from './packages/core/agent/lib/index.js' +import LocalFileSystem from './packages/fs/fs-local/lib/index.js' +import SystemPrompt from './packages/core/system-prompt/lib/index.js' +import ToolRuntime from './packages/core/tools/lib/index.js' +import { Session, SESSION_FORMAT_VERSION } from './packages/core/session/lib/index.js' +import * as Present from './packages/fs/tool-present/lib/index.js' +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +const root = await mkdtemp(join(tmpdir(), 'present-built-')) +const ctx = new Context() +try { + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRuntime) + await ctx.plugin(AgentRegistry) + await ctx.plugin(LocalFileSystem, { cwd: root }) + ctx.provide('attachments', { saveFile() { throw new Error('must reject before saving') } }) + ctx.provide('sessionProjections', { stateOf() { return { openTurnStartSeq: 1, lastTurn: 1 } } }) + await ctx.plugin(Present, { maxFiles: 2, maxFileBytes: 3 }) + const scope = ctx.plugin(() => {}) + const session = Session.create('built-present', [], { version: SESSION_FORMAT_VERSION, id: 'built-present', createdAt: 0, cwd: root, isSeeded: false }) + const owner = { id: 'built-present', session, ctx: scope.ctx, options: {}, status: 'idle' } + ctx.agents.register(owner) + await writeFile(join(root, 'large'), 'four') + await writeFile(join(root, 'changed'), 'a') + const read = ctx.fs.readBytes.bind(ctx.fs) + ctx.fs.readBytes = async (...args) => { const data = await read(...args); await writeFile(join(root, 'changed'), 'ab'); return data } + const events = [] + ctx.on('tools/result', (_exec, result) => events.push(result)) + let n = 0 + for (const [files, code] of [[[{ path: 'missing' }], 'FS_NOT_FOUND'], [[{ path: 'large' }], 'FS_TOO_LARGE'], [[{ path: 'changed' }], 'FS_STALE_VERSION'], [[{ path: 42 }], 'INVALID_ARGS']]) { + const result = await ctx.tools.execute({ signal: new AbortController().signal, name: 'present', callId: 'call-' + (++n), arguments: { files }, agent: owner }) + assert.equal(result.isError, true) + assert.equal(result.error.info.code, code) + assert.equal(events.at(-1).error.info.code, code) + console.log('built ToolRuntime result preserved ' + code) + } +} finally { + try { await ctx.fiber.dispose() } + finally { await rm(root, { recursive: true, force: true }) } +} +` + +it.skipIf(!existsSync(bundle))('preserves present error codes through built ToolRuntime results', { retry: 0 }, async ({ signal }) => { + const { stdout } = await execFileAsync(process.execPath, ['--input-type=module', '-e', probe], { cwd: repoRoot, signal }) + expect(stdout.trim().split('\n')).toEqual([ + 'built ToolRuntime result preserved FS_NOT_FOUND', + 'built ToolRuntime result preserved FS_TOO_LARGE', + 'built ToolRuntime result preserved FS_STALE_VERSION', + 'built ToolRuntime result preserved INVALID_ARGS', + ]) +}) diff --git a/packages/fs/tool-present/tests/present.spec.ts b/packages/fs/tool-present/tests/present.spec.ts new file mode 100644 index 0000000000..0d5e4740b0 --- /dev/null +++ b/packages/fs/tool-present/tests/present.spec.ts @@ -0,0 +1,183 @@ +/** Explicit deliveries commit only after a successful final tool result. */ +import { mkdtemp, rm, writeFile, unlink, symlink } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { Context } from '@deepseek-ai/cordis' +import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent' +import { unsupportedInbox } from '@deepseek-ai/dsh-agent-loop-testkit' +import LocalAttachmentStore from '@deepseek-ai/dsh-attachment-local' +import LocalFileSystem from '@deepseek-ai/dsh-fs-local' +import { createScope, type Scope } from '@deepseek-ai/dsh-scope' +import { ToolCallId } from '@deepseek-ai/dsh-llm' +import { SESSION_FORMAT_VERSION, Session, SessionId } from '@deepseek-ai/dsh-session' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' +import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent-loop' +import SystemPrompt from '@deepseek-ai/dsh-system-prompt' +import ToolRuntime, { defineTool } from '@deepseek-ai/dsh-tools' +import type { PresentedFile } from '../src/types.ts' +import * as Present from '../src/index.ts' + +const cleanups: Array<() => Promise> = [] +let callNumber = 0 +afterEach(async () => { + for (const cleanup of cleanups.reverse()) await cleanup() + cleanups.length = 0 + vi.restoreAllMocks() +}) +async function agent(ctx: Context, cwd: string | undefined): Promise { + const id = SessionId(`present-owner-${++callNumber}`) + let scope: Scope + const session = Session.create(id, [], { + version: SESSION_FORMAT_VERSION, id, createdAt: 0, ...cwd === undefined ? {} : { cwd }, isSeeded: false, + }) + const value: Agent = { + id, + options: {}, + session, + inbox: unsupportedInbox(), + status: 'idle', + get ctx() { return scope.ctx }, + send: () => {}, + followup: () => {}, + steer: () => ({ outcome: Promise.resolve({ status: 'rejected' as const }) }), + inject: () => {}, + cancel() {}, + runMaintenance: task => task(new AbortController().signal), + whenIdle: () => Promise.resolve(), + } + await ctx.plugin(Object.assign((inner: Context) => { scope = createScope(inner, value) }, { inject: ['tools'] })) + ctx.agents.register(value) + return value +} + + +async function setup(maxFileBytes = 1024) { + const root = await mkdtemp(join(tmpdir(), 'dsh-present-minimal-')) + cleanups.push(() => rm(root, { recursive: true, force: true })) + const ctx = new Context() + cleanups.push(() => ctx.fiber.dispose()) + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRuntime) + await ctx.plugin(AgentRegistry) + await ctx.plugin(LocalFileSystem, { cwd: root }) + await ctx.plugin(LocalAttachmentStore, { dshHome: join(root, 'home') }) + await ctx.plugin(SessionProjectionRegistry) + ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + const fiber = ctx.plugin(Present, { maxFileBytes, maxFiles: 2 }) + await fiber + const owner = await agent(ctx, root) + owner.session.append('turn/start', { turn: 1 }) + const execute = (files: unknown) => ctx.tools.execute({ + signal: new AbortController().signal, callId: ToolCallId(`call-${++callNumber}`), + name: 'present', arguments: { files }, agent: owner, + }) + return { ctx, owner, root, fiber, execute } +} + +describe('present', () => { + it('saves binary bytes, records one delivery, and survives source deletion', async () => { + const { ctx, owner, root, execute, fiber } = await setup() + const data = Uint8Array.of(80, 75, 0, 255) + await writeFile(join(root, '报告.docx'), data) + const result = await execute([{ path: '报告.docx', description: 'Report' }]) + expect(result.isError).toBe(false) + if (result.isError) throw new Error('present failed') + const files = (result.value as unknown as { files: PresentedFile[] }).files + expect(files).toHaveLength(1) + expect(owner.session.snapshotEvents().find(event => event.type === 'deliverables/presented')?.data.files).toEqual(files) + const file = files[0]! + await unlink(join(root, '报告.docx')) + const chunks = [] + for await (const chunk of ctx.attachments.readFileStream(file)) chunks.push(chunk) + expect(Buffer.concat(chunks)).toEqual(Buffer.from(data)) + await fiber.dispose() + expect(ctx.tools.get('present', owner)).toBeUndefined() + }) + + it('ignores a different present definition in the calling agent scope', async () => { + const { owner, execute } = await setup() + owner.ctx.tools.register(defineTool({ + name: 'present', description: 'Scoped replacement.', parameters: {}, + output: { + schema: { + type: 'object', additionalProperties: false, + properties: { + turn: { type: 'integer', required: true }, + files: { type: 'array', required: true, items: { type: 'string' } }, + }, + }, + render: () => [], + }, + execute: async () => ({ turn: 1, files: [] }), + })) + expect((await execute([])).isError).toBe(false) + expect(owner.session.snapshotEvents().filter(event => event.type === 'deliverables/presented')).toEqual([]) + }) + + it('records once when ancestor and agent scopes both mount present', async () => { + const { owner, root, execute } = await setup() + await owner.ctx.plugin(Present, { maxFileBytes: 1024, maxFiles: 2 }) + await writeFile(join(root, 'a'), 'a') + expect((await execute([{ path: 'a' }])).isError).toBe(false) + const deliveries = owner.session.snapshotEvents().filter(event => event.type === 'deliverables/presented') + expect(deliveries).toHaveLength(1) + expect(deliveries[0]?.data.files[0]?.path).toBe('a') + }) + + it('does not publish deliveries after post-execute blocks a successful snapshot', async () => { + const { ctx, root, owner, execute } = await setup() + await writeFile(join(root, 'a'), 'a') + ctx.on('tools/post-execute', async (_exec, _result, next) => { + await next() + return { kind: 'block', feedback: [{ type: 'text', text: 'blocked' }] } + }) + expect((await execute([{ path: 'a' }])).isError).toBe(true) + expect(owner.session.snapshotEvents().some(event => event.type === 'deliverables/presented')).toBe(false) + }) + + it('rejects missing, non-file, outside-workspace, empty, and oversized inputs', async () => { + const { root, owner, execute } = await setup(3) + await writeFile(join(root, 'large'), 'four') + await symlink(tmpdir(), join(root, 'outside')) + for (const files of [[], [{ path: '' }], [{ path: 'missing' }], [{ path: '.' }], [{ path: 'outside' }], [{ path: 'large' }]]) { + const result = await execute(files) + expect(result.isError, JSON.stringify(files)).toBe(true) + } + expect(owner.session.snapshotEvents().some(event => event.type === 'deliverables/presented')).toBe(false) + }) + + it('refuses a file changed during the bounded read before saving it', async () => { + const { ctx, root, owner, execute } = await setup() + await writeFile(join(root, 'a'), 'old') + const read = ctx.fs.readBytes.bind(ctx.fs) + vi.spyOn(ctx.fs, 'readBytes').mockImplementation(async (...args) => { + const data = await read(...args) + await writeFile(join(root, 'a'), 'changed') + return data + }) + const save = vi.spyOn(ctx.attachments, 'saveFile') + expect((await execute([{ path: 'a' }])).isError).toBe(true) + expect(save).not.toHaveBeenCalled() + expect(owner.session.snapshotEvents().some(event => event.type === 'deliverables/presented')).toBe(false) + }) +}) + + +it('validates deployment limits before registering the tool', () => { + for (const config of [{ maxFileBytes: 0, maxFiles: 2 }, { maxFileBytes: 104857601, maxFiles: 2 }, { maxFileBytes: 3, maxFiles: 0 }]) { + expect(() => { Present.apply(new Context(), config) }).toThrow('positive integer limits') + } +}) + +it('requires an agent, an open turn, and a workspace', async () => { + const { ctx, owner, execute } = await setup() + const detached = await ctx.tools.execute({ signal: new AbortController().signal, callId: ToolCallId('detached'), name: 'present', arguments: { files: [{ path: 'a' }] } }) + expect(detached.isError).toBe(true) + owner.session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) + expect((await execute([{ path: 'a' }])).isError).toBe(true) + const noWorkspace = await agent(ctx, undefined) + noWorkspace.session.append('turn/start', { turn: 1 }) + const absent = await ctx.tools.execute({ signal: new AbortController().signal, callId: ToolCallId('no-workspace'), name: 'present', arguments: { files: [{ path: 'a' }] }, agent: noWorkspace }) + expect(absent.isError).toBe(true) +}) diff --git a/packages/fs/tool-present/tsconfig.json b/packages/fs/tool-present/tsconfig.json new file mode 100644 index 0000000000..0a256f850d --- /dev/null +++ b/packages/fs/tool-present/tsconfig.json @@ -0,0 +1,39 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../../vendor/schemastery" + }, + { + "path": "../../core/agent" + }, + { + "path": "../../attachment/attachment" + }, + { + "path": "../fs" + }, + { + "path": "../../llm/llm" + }, + { + "path": "../../core/session" + }, + { + "path": "../../session/session-projection" + }, + { + "path": "../../core/tools" + } + ] +} diff --git a/packages/preset/agent-presets/README.i18n.yaml b/packages/preset/agent-presets/README.i18n.yaml index da31f205d2..2ddf994845 100644 --- a/packages/preset/agent-presets/README.i18n.yaml +++ b/packages/preset/agent-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/preset/agent-presets/README.md -README.md: bb9f59d11adc40ee7b428beb3d929829d88ff282 -README.zh.md: 13d30f9b7544f5d42523c1a8560cabf64165cabf +README.md: 6ef3374a5f1ae6b83005073f80d63d156ee92f9c +README.zh.md: a1be6d634e592126711befad4ffcb06ca0bdc26c diff --git a/packages/preset/agent-presets/README.md b/packages/preset/agent-presets/README.md index bb9f59d11a..6ef3374a5f 100644 --- a/packages/preset/agent-presets/README.md +++ b/packages/preset/agent-presets/README.md @@ -27,6 +27,8 @@ Use `dsh-agent-presets` to give each session the tools, prompt sections, and ski Mount this package in a composition that should give each agent session its own tools, prompt sections, and skills from a preset file. Every session names a preset — explicitly or through the configured default — and is composed from it; without the package, sessions fall back to whatever the host composition mounts. +The shipped Web `standard`, `ptc`, and `cordis` presets include [explicit file delivery](../../client/ui-deliverables/README.md#explicit-deliveries). The `minimal` preset keeps its fixed two-tool training configuration. + ### What a preset gives a session A session composed from a preset runs the plugins that preset's `agent.cordis.yml` names: its tools, prompt sections, and skills. Sessions joined to the same preset share one installed composition, and each session's state stays separate. A child agent (subagent) joins its parent's composition, so it sees the same tools and prompt sections as the agent that spawned it. diff --git a/packages/preset/agent-presets/README.zh.md b/packages/preset/agent-presets/README.zh.md index 13d30f9b75..a1be6d634e 100644 --- a/packages/preset/agent-presets/README.zh.md +++ b/packages/preset/agent-presets/README.zh.md @@ -27,6 +27,8 @@ kind: "package-reference" 在需要让每个 agent 会话从 preset 文件获得自己的工具、提示词段落与 skill 的组装中挂载本包。每个会话都会命名一个 preset——显式指定或通过配置的默认值——并据此组装;没有本包时,会话只能回退到宿主组装挂载的内容。 +随附 Web 的 `standard`、`ptc` 与 `cordis` preset 包含[显式文件交付](../../client/ui-deliverables/README.zh.md#explicit-deliveries)。`minimal` preset 保留固定的双工具训练配置。 + ### preset 给会话带来什么 从 preset 组装的会话会运行该 preset `agent.cordis.yml` 所列插件:它的工具、提示词段落与 skill。加入同一 preset 的会话共享一份已安装的组装,且各会话的状态彼此隔离。子 agent(subagent)会加入其父方的组装,因此它看到的工具与提示词段落和创建它的 agent 相同。 diff --git a/packages/preset/agent-presets/presets/cordis/agent.cordis.yml b/packages/preset/agent-presets/presets/cordis/agent.cordis.yml index 7b6664a440..940a7c9e48 100644 --- a/packages/preset/agent-presets/presets/cordis/agent.cordis.yml +++ b/packages/preset/agent-presets/presets/cordis/agent.cordis.yml @@ -261,3 +261,6 @@ - id: tool-skill name: '@deepseek-ai/dsh-tool-skill' + +- id: present + name: '@deepseek-ai/dsh-tool-present' diff --git a/packages/preset/agent-presets/presets/ptc/agent.cordis.yml b/packages/preset/agent-presets/presets/ptc/agent.cordis.yml index 0bdc7db732..7773f64109 100644 --- a/packages/preset/agent-presets/presets/ptc/agent.cordis.yml +++ b/packages/preset/agent-presets/presets/ptc/agent.cordis.yml @@ -270,3 +270,6 @@ name: '@deepseek-ai/dsh-agent-tool-presentation' config: mode: ptc + +- id: present + name: '@deepseek-ai/dsh-tool-present' diff --git a/packages/preset/agent-presets/presets/standard/agent.cordis.yml b/packages/preset/agent-presets/presets/standard/agent.cordis.yml index c2f4c51a0b..b2c26e4ef0 100644 --- a/packages/preset/agent-presets/presets/standard/agent.cordis.yml +++ b/packages/preset/agent-presets/presets/standard/agent.cordis.yml @@ -250,3 +250,6 @@ config: fetch: true searchTimeoutMs: 60000 + +- id: present + name: '@deepseek-ai/dsh-tool-present' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 543267a69a..9cd52b76f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -292,6 +292,9 @@ importers: '@deepseek-ai/dsh-tool-jobs': specifier: workspace:^ version: link:../../packages/jobs/tool-jobs + '@deepseek-ai/dsh-tool-present': + specifier: workspace:^ + version: link:../../packages/fs/tool-present '@deepseek-ai/dsh-tool-pwsh': specifier: workspace:^ version: link:../../packages/shell/tool-pwsh @@ -1524,6 +1527,9 @@ importers: '@deepseek-ai/dsh-tool-jobs': specifier: workspace:^ version: link:../../jobs/tool-jobs + '@deepseek-ai/dsh-tool-present': + specifier: workspace:^ + version: link:../../fs/tool-present '@deepseek-ai/dsh-tool-pwsh': specifier: workspace:^ version: link:../../shell/tool-pwsh @@ -2687,6 +2693,12 @@ importers: '@deepseek-ai/dsh-api-remotes': specifier: workspace:^ version: link:../../api/remotes + '@deepseek-ai/dsh-attachment': + specifier: workspace:^ + version: link:../../attachment/attachment + '@deepseek-ai/dsh-attachment-local': + specifier: workspace:^ + version: link:../../attachment/attachment-local '@deepseek-ai/dsh-client-connection': specifier: workspace:^ version: link:../connection @@ -2714,12 +2726,24 @@ importers: '@deepseek-ai/dsh-client-ui-slots': specifier: workspace:^ version: link:../ui-slots + '@deepseek-ai/dsh-client-ui-tool': + specifier: workspace:^ + version: link:../ui-tool + '@deepseek-ai/dsh-llm': + specifier: workspace:^ + version: link:../../llm/llm '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../core/session + '@deepseek-ai/dsh-session-query': + specifier: workspace:^ + version: link:../../session-query/session-query '@deepseek-ai/dsh-system-prompt': specifier: workspace:^ version: link:../../core/system-prompt + '@deepseek-ai/dsh-tool-present': + specifier: workspace:^ + version: link:../../fs/tool-present '@types/react': specifier: ~18.3.1 version: 18.3.31 @@ -6064,6 +6088,55 @@ importers: specifier: workspace:^ version: link:../../core/tools + packages/fs/tool-present: + dependencies: + '@deepseek-ai/schemastery': + specifier: link:../../../vendor/schemastery + version: link:../../../vendor/schemastery + devDependencies: + '@deepseek-ai/cordis': + specifier: workspace:^ + version: link:../../../vendor/cordis + '@deepseek-ai/dsh-agent': + specifier: workspace:^ + version: link:../../core/agent + '@deepseek-ai/dsh-agent-loop': + specifier: workspace:^ + version: link:../../core/agent-loop + '@deepseek-ai/dsh-agent-loop-testkit': + specifier: workspace:^ + version: link:../../test-support/agent-loop-testkit + '@deepseek-ai/dsh-attachment': + specifier: workspace:^ + version: link:../../attachment/attachment + '@deepseek-ai/dsh-attachment-local': + specifier: workspace:^ + version: link:../../attachment/attachment-local + '@deepseek-ai/dsh-fs': + specifier: workspace:^ + version: link:../fs + '@deepseek-ai/dsh-fs-local': + specifier: workspace:^ + version: link:../fs-local + '@deepseek-ai/dsh-llm': + specifier: workspace:^ + version: link:../../llm/llm + '@deepseek-ai/dsh-scope': + specifier: workspace:^ + version: link:../../core/scope + '@deepseek-ai/dsh-session': + specifier: workspace:^ + version: link:../../core/session + '@deepseek-ai/dsh-session-projection': + specifier: workspace:^ + version: link:../../session/session-projection + '@deepseek-ai/dsh-system-prompt': + specifier: workspace:^ + version: link:../../core/system-prompt + '@deepseek-ai/dsh-tools': + specifier: workspace:^ + version: link:../../core/tools + packages/fs/tool-str-replace-editor: dependencies: '@deepseek-ai/schemastery': @@ -11007,6 +11080,9 @@ importers: '@deepseek-ai/dsh-tool-jobs': specifier: workspace:^ version: link:../../packages/jobs/tool-jobs + '@deepseek-ai/dsh-tool-present': + specifier: workspace:^ + version: link:../../packages/fs/tool-present '@deepseek-ai/dsh-tool-pwsh': specifier: workspace:^ version: link:../../packages/shell/tool-pwsh diff --git a/python/sdk-runtime/package.json b/python/sdk-runtime/package.json index d34b14bec4..3a3f128cf5 100644 --- a/python/sdk-runtime/package.json +++ b/python/sdk-runtime/package.json @@ -102,6 +102,7 @@ "@deepseek-ai/dsh-tool-bash-persistent": "workspace:^", "@deepseek-ai/dsh-tool-call-timeout-policy": "workspace:^", "@deepseek-ai/dsh-tool-cordis": "workspace:^", + "@deepseek-ai/dsh-tool-present": "workspace:^", "@deepseek-ai/dsh-tool-fs": "workspace:^", "@deepseek-ai/dsh-tool-fs-search": "workspace:^", "@deepseek-ai/dsh-tool-goal": "workspace:^", diff --git a/scripts/gen-tool-catalog.ts b/scripts/gen-tool-catalog.ts index 0fcf1c41fe..000f551081 100644 --- a/scripts/gen-tool-catalog.ts +++ b/scripts/gen-tool-catalog.ts @@ -46,6 +46,7 @@ import * as ToolBashPersistent from '@deepseek-ai/dsh-tool-bash-persistent' import * as ToolPwshPersistent from '@deepseek-ai/dsh-tool-pwsh-persistent' import CordisHostRunner from '@deepseek-ai/dsh-cordis-host-runner' import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis' +import * as ToolPresent from '@deepseek-ai/dsh-tool-present' import * as ToolFs from '@deepseek-ai/dsh-tool-fs' import * as ToolFsSearch from '@deepseek-ai/dsh-tool-fs-search' import * as ToolStrReplaceEditor from '@deepseek-ai/dsh-tool-str-replace-editor' @@ -241,6 +242,19 @@ const TOOL_PACKAGES: ToolPackage[] = [ note: '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.', }, + { + pkg: '@deepseek-ai/dsh-tool-present', + dir: 'tool-present', + source: 'packages/fs/tool-present/src/index.ts', + requires: ['ctx.tools', 'ctx.fs', 'ctx.attachments', 'ctx.sessionProjections'], + writes: ['tool/call', 'deliverables/presented after a successful final result', 'tool/result'], + async mount(ctx) { + await ctx.plugin(LocalFileSystem) + await ctx.plugin(CatalogAttachmentStore) + await ctx.plugin(ToolPresent) + }, + note: 'Deliveries belong to the calling Session; Web ui-deliverables supplies authenticated downloads and cards.', + }, { pkg: '@deepseek-ai/dsh-tool-pwsh', dir: 'tool-pwsh', diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index 9276b51c77..1b08040d06 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -787,6 +787,7 @@ function builtBinSmokeGate(needs: string[] = ['build']): Gate { 'apps/cli/tests/built-bin.e2e.ts', 'packages/host/directory-picker-native/tests/built-worker.e2e.ts', 'packages/sdk/server/tests/built-scope-carrier.e2e.ts', + 'packages/fs/tool-present/tests/built-errors.e2e.ts', 'packages/subprocess/subprocess-local/tests/spawn-runner-built.e2e.ts', 'packages/subagent/subagent-codex/tests/loader-composition.e2e.ts', 'packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts', diff --git a/snapshots/web/cordis-tool-round/tool-schemas.expected.json b/snapshots/web/cordis-tool-round/tool-schemas.expected.json index 82e5ac7630..2242317f16 100644 --- a/snapshots/web/cordis-tool-round/tool-schemas.expected.json +++ b/snapshots/web/cordis-tool-round/tool-schemas.expected.json @@ -520,6 +520,38 @@ } } }, + { + "name": "present", + "description": "Deliver final files to the user. Saves a snapshot of each existing workspace file so it remains downloadable after edits or deletion. Create the files before calling this tool.", + "parameters": { + "type": "object", + "properties": { + "files": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "description": "Path of an existing file inside the workspace." + }, + "description": { + "type": "string", + "description": "Brief description for the user." + } + }, + "required": [ + "path" + ] + } + } + }, + "required": [ + "files" + ] + } + }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", diff --git a/snapshots/web/fresh-round-trip/tool-schemas.expected.json b/snapshots/web/fresh-round-trip/tool-schemas.expected.json index da820f02a2..e087897363 100644 --- a/snapshots/web/fresh-round-trip/tool-schemas.expected.json +++ b/snapshots/web/fresh-round-trip/tool-schemas.expected.json @@ -323,6 +323,38 @@ } } }, + { + "name": "present", + "description": "Deliver final files to the user. Saves a snapshot of each existing workspace file so it remains downloadable after edits or deletion. Create the files before calling this tool.", + "parameters": { + "type": "object", + "properties": { + "files": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "description": "Path of an existing file inside the workspace." + }, + "description": { + "type": "string", + "description": "Brief description for the user." + } + }, + "required": [ + "path" + ] + } + } + }, + "required": [ + "files" + ] + } + }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", diff --git a/snapshots/web/present/session.v2.jsonl b/snapshots/web/present/session.v2.jsonl new file mode 100644 index 0000000000..95046d9d90 --- /dev/null +++ b/snapshots/web/present/session.v2.jsonl @@ -0,0 +1,28 @@ +{"type":"session","version":2,"id":"{{session:1}}","createdAt":1788841384234,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0,"agentPreset":"ptc"} +{"type":"permission/preset","data":{"preset":"workspace-write"}} +{"type":"sandbox/mode","data":{"mode":"workspace-write"}} +{"type":"approval/policy","data":{"policy":"ask"}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Use one run_code program to do the following in order. Call present for missing.txt and catch its error without creating that file. Use bash to run exactly `printf \"DELIVERED_REPORT\\n\" > report.txt; printf \"DELIVERED_NOTE\\n\" > 说明.txt`. Call present for report.txt and 说明.txt. After present succeeds, deliberately throw the string \"AFTER_PRESENT\" (not an Error object) from that same run_code program. Do not retry the program or create any other files. Finish by mentioning `report.txt` and `说明.txt` in inline code, and put PRESENT_DONE in a separate paragraph."}],"source":{"kind":"user","rpcId":"{{rpc:1}}","clientTimeZone":"{{clientTimeZone}}"},"role":"user","id":"{{message:1}}"}]}} +{"type":"turn/start","data":{"turn":1}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} +{"type":"step/start","data":{"turn":1,"step":1}} +{"type":"user/message","data":{"content":[{"type":"text","text":"Use one run_code program to do the following in order. Call present for missing.txt and catch its error without creating that file. Use bash to run exactly `printf \"DELIVERED_REPORT\\n\" > report.txt; printf \"DELIVERED_NOTE\\n\" > 说明.txt`. Call present for report.txt and 说明.txt. After present succeeds, deliberately throw the string \"AFTER_PRESENT\" (not an Error object) from that same run_code program. Do not retry the program or create any other files. Finish by mentioning `report.txt` and `说明.txt` in inline code, and put PRESENT_DONE in a separate paragraph."}],"source":{"kind":"user","rpcId":"{{rpc:1}}","clientTimeZone":"{{clientTimeZone}}"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{message:2}}"},"surfaceOp":"append"} +{"type":"session/title","data":{"title":"Use one run_code program to","messageSeqs":[7],"source":{"kind":"fallback"}}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":128000}} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants one run_code program that:\n1. Calls present for missing.txt and catches its error without creating that file.\n2. Uses bash to run exactly `printf \"DELIVERED_REPORT\\n\" > report.txt; printf \"DELIVERED_NOTE\\n\" > 说明.txt`.\n3. Calls present for report.txt and 说明.txt.\n4. After present succeeds, deliberately throws the string \"AFTER_PRESENT\" (not an Error object) from that same run_code program.\n5. Does not retry the program or create other files.\n6. Finishes by mentioning report.txt and 说明.txt in inline code, and put PRESENT_DONE in a separate paragraph.\n\nLet me write the program.\n\npresent tool args: { files: [{ path, description? }] }.\n\nSo first: try present for missing.txt — catch error. It should error because the file doesn't exist (present requires existing workspace file).\n\nThen bash run exact command. Note the command uses `;` inside a single string. Run as a single command string: `printf \"DELIVERED_REPORT\\n\" > report.txt; printf \"DELIVERED_NOTE\\n\" > 说明.txt`.\n\nWait — printf interprets the arguments... In bash, `printf \"DELIVERED_REPORT\\n\"` with the format string containing \\n — printf will interpret \\n as newline. Good.\n\nThen present for report.txt and 说明.txt.\n\nThen throw \"AFTER_PRESENT\" string. In TS async function body, `throw \"AFTER_PRESENT\"` works.\n\nLet me write it."},{"type":"tool-call","id":"call_00_GgvYPol45qxGf6GezK3L3590","name":"run_code","arguments":"{\"code\": \"// Step 1: present for missing.txt — must fail, catch the error, don't create the file\\ntry {\\n await tools.present({ files: [{ path: \\\"missing.txt\\\", description: \\\"must not exist\\\" }] });\\n console.log(\\\"UNEXPECTED: present succeeded for missing.txt\\\");\\n} catch (err) {\\n console.log(\\\"Caught present error for missing.txt:\\\", typeof err, String(err).slice(0, 200));\\n}\\n\\n// Step 2: run the exact bash command to create the two report files\\nconst res = await tools.bash({\\n command: 'printf \\\"DELIVERED_REPORT\\\\n\\\" > report.txt; printf \\\"DELIVERED_NOTE\\\\n\\\" > 说明.txt',\\n description: \\\"Write DELIVERED_REPORT and DELIVERED_NOTE to files\\\",\\n});\\nconsole.log(\\\"bash exit:\\\", res.exitCode);\\n\\n// Step 3: present for report.txt and 说明.txt (must succeed)\\nconst p2 = await tools.present({\\n files: [\\n { path: \\\"report.txt\\\", description: \\\"delivered report\\\" },\\n { path: \\\"说明.txt\\\", description: \\\"delivered note\\\" },\\n ],\\n});\\nconsole.log(\\\"present ok, files:\\\", p2.files.map((f) => f.path).join(\\\", \\\"));\\n\\n// Step 4: deliberately throw a plain string AFTER_PRESENT (not an Error)\\nthrow \\\"AFTER_PRESENT\\\";\", \"description\": \"Present files then deliberately throw AFTER_PRESENT\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":981,"outputTokens":695,"totalTokens":9356,"cacheReadTokens":7680,"reasoningTokens":329},"stream":[{"type":"chunk","time":1788856033793,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788856033794,"index":0,"dt":[116,24,2,24,9,0,0,10,0,1,0,19,1,0,24,0,20,0,0,0,23,0,0,0,0,0,21,1,0,0,0,23,0,1,21,0,0,1,0,0,21,0,0,0,0,1,21,0,0,0,0,1,21,0,0,0,0,0,45,0,0,0,0,0,0,23,0,1,0,0,45,0,0,0,0,0,0,0,0,1,0,0,19,0,0,0,1,0,22,0,0,0,0,0,23,0,0,0,21,0,1,0,0,0,22,0,0,0,0,0,22,0,22,0,0,0,0,22,1,0,0,24,0,0,0,0,0,21,1,0,0,0,29,0,0,0,0,0,15,0,0,0,0,0,22,1,0,0,24,1,26,1,17,1,0,21,0,23,1,23,1,0,0,0,2,18,0,21,0,0,22,0,0,0,22,1,0,21,22,23,0,0,0,0,1,19,1,31,1,12,1,0,0,0,21,1,22,0,0,22,1,22,22,1,21,23,0,0,22,0,1,22,0,22,0,1,22,0,0,0,21,1,0,0,0,1,22,0,0,0,0,0,21,0,0,0,0,0,22,1,0,0,0,22,0,0,0,23,21,1,22,22,23,1,0,22,0,0,0,0,0,21,1,0,0,0,22,1,22,1,0,22,0,0,22,23,0,0,0,23,0,1,0,0,1,20,0,22,1,0,0,0,0,21,1,0,0,22,1,0,22,0,0,0,0,1,25,19,0,23,1,0,0,0,22,0,0,0,0,1,21,1,21,0,1,0,0,21],"texts":["The"," user"," wants"," one"," run","_code"," program"," that",":\n","1","."," Calls"," present"," for"," missing",".txt"," and"," catches"," its"," error"," without"," creating"," that"," file",".\n","2","."," Uses"," bash"," to"," run"," exactly"," `","printf"," \"","DEL","IVER","ED","_RE","PORT","\\n","\""," >"," report",".txt",";"," printf"," \"","DEL","IVER","ED","_N","OTE","\\n","\""," >"," ","说明",".txt","`.\n","3","."," Calls"," present"," for"," report",".txt"," and"," ","说明",".txt",".\n","4","."," After"," present"," succeeds",","," deliberately"," throws"," the"," string"," \"","AF","TER","_P","RES","ENT","\""," (","not"," an"," Error"," object",")"," from"," that"," same"," run","_code"," program",".\n","5","."," Does"," not"," ret","ry"," the"," program"," or"," create"," other"," files",".\n","6","."," Fin","ishes"," by"," mentioning"," report",".txt"," and"," ","说明",".txt"," in"," inline"," code",","," and"," put"," PRES","ENT","_D","ONE"," in"," a"," separate"," paragraph",".\n\n","Let"," me"," write"," the"," program",".\n\n","present"," tool"," args",":"," {"," files",":"," [{"," path",","," description","?"," }","]"," }",".\n\n","So"," first",":"," try"," present"," for"," missing",".txt"," —"," catch"," error","."," It"," should"," error"," because"," the"," file"," doesn","'t"," exist"," (","present"," requires"," existing"," workspace"," file",").\n\n","Then"," bash"," run"," exact"," command","."," Note"," the"," command"," uses"," `",";","`"," inside"," a"," single"," string","."," Run"," as"," a"," single"," command"," string",":"," `","printf"," \"","DEL","IVER","ED","_RE","PORT","\\n","\""," >"," report",".txt",";"," printf"," \"","DEL","IVER","ED","_N","OTE","\\n","\""," >"," ","说明",".txt","`.\n\n","Wait"," —"," printf"," interprets"," the"," arguments","..."," In"," bash",","," `","printf"," \"","DEL","IVER","ED","_RE","PORT","\\n","\"","`"," with"," the"," format"," string"," containing"," \\","n"," —"," printf"," will"," interpret"," \\","n"," as"," new","line","."," Good",".\n\n","Then"," present"," for"," report",".txt"," and"," ","说明",".txt",".\n\n","Then"," throw"," \"","AF","TER","_P","RES","ENT","\""," string","."," In"," TS"," async"," function"," body",","," `","throw"," \"","AF","TER","_P","RES","ENT","\"","`"," works",".\n\n","Let"," me"," write"," it","."]},{"type":"chunk","time":1788856035925,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788856035925,"index":1,"dt":[23,1,0,20,1,0,0,23,0,1,0,41,1,0,1,1,23,0,0,0,23,21,0,24,1,0,0,0,0,20,0,0,0,0,1,21,1,21,1,0,0,0,0,21,1,0,0,0,0,23,0,1,0,20,1,0,0,23,0,0,0,24,0,0,0,0,0,21,0,0,0,0,1,21,1,22,0,0,0,0,0,22,1,0,0,21,0,0,0,1,0,21,1,22,0,24,0,0,0,0,0,21,1,0,0,0,0,21,1,0,0,21,1,0,21,1,0,0,21,1,0,22,0,0,0,0,0,23,0,0,0,0,22,1,0,1,0,21,0,0,0,0,1,21,1,0,0,0,0,21,0,0,0,0,1,22,0,0,0,0,0,22,1,0,21,22,1,0,0,0,0,21,0,0,0,0,1,22,22,22,0,1,0,22,0,0,0,0,1,21,0,0,0,0,1,21,0,0,23,0,0,1,0,1,21,0,0,23,0,23,1,0,0,0,0,21,0,0,0,0,1,22,0,0,0,0,1,40,1,0,0,10,1,0,0,0,1,13,0,0,1,0,0,22,0,0,0,0,1,22,0,0,0,0,1,22,0,0,0,21,0,0,0,0,1,21,0,0,0,0,1,21,1,0,0,0,1,36,1,0,0,0,0,6,0,1,21,0,0,22,22,1,0,22,0,0,0,0,0,21,1,0,0,0,1,20,22,1,0,0,0,23,0,21,22,22,22,1,22,0,1,0,21],"id":"call_00_GgvYPol45qxGf6GezK3L3590","name":"run_code","args":["","{","\"","code","\"",": ","\"","//"," Step"," ","1",":"," present"," for"," missing",".txt"," —"," must"," fail",","," catch"," the"," error",","," don","'t"," create"," the"," file","\\n","try"," {\\n"," "," await"," tools",".p","resent","({"," files",":"," [{"," path",":"," \\\"","missing",".txt","\\\","," description",":"," \\\"","must"," not"," exist","\\\""," }","]"," });\\n"," "," console",".log","(\\\"","UN","EXP","ECT","ED",":"," present"," succeeded"," for"," missing",".txt","\\\");\\n","}"," catch"," (","err",")"," {\\n"," "," console",".log","(\\\"","Ca","ught"," present"," error"," for"," missing",".txt",":\\\","," typeof"," err",","," String","(err",").","slice","(","0",","," ","200","));\\n","}\\n\\n","//"," Step"," ","2",":"," run"," the"," exact"," bash"," command"," to"," create"," the"," two"," report"," files","\\n","const"," res"," ="," await"," tools",".b","ash","({\\n"," "," command",":"," '","printf"," \\\"","DEL","IVER","ED","_RE","PORT","\\\\n","\\\""," >"," report",".txt",";"," printf"," \\\"","DEL","IVER","ED","_N","OTE","\\\\n","\\\""," >"," ","说明",".txt","',\\n"," "," description",":"," \\\"","Write"," DEL","IVER","ED","_RE","PORT"," and"," DEL","IVER","ED","_N","OTE"," to"," files","\\\",\\n","});\\n","console",".log","(\\\"","bash"," exit",":\\\","," res",".exit","Code",");\\n\\n","//"," Step"," ","3",":"," present"," for"," report",".txt"," and"," ","说明",".txt"," (","must"," succeed",")\\n","const"," p","2"," ="," await"," tools",".p","resent","({\\n"," "," files",":"," [\\n"," "," {"," path",":"," \\\"","report",".txt","\\\","," description",":"," \\\"","del","ivered"," report","\\\""," },\\n"," "," {"," path",":"," \\\"","说明",".txt","\\\","," description",":"," \\\"","del","ivered"," note","\\\""," },\\n"," "," ],\\n","});\\n","console",".log","(\\\"","present"," ok",","," files",":\\\","," p","2",".f","iles",".map","((","f",")"," =>"," f",".path",").","join","(\\\","," \\\"","));\\n\\n","//"," Step"," ","4",":"," deliberately"," throw"," a"," plain"," string"," AFTER","_P","RES","ENT"," (","not"," an"," Error",")\\n","throw"," \\\"","AF","TER","_P","RES","ENT","\\\";","\"",", ","\"","description","\"",": ","\"","Present"," files"," then"," deliberately"," throw"," AFTER","_P","RES","ENT","\"","}"]},{"type":"chunk","time":1788856037718,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants one run_code program that:\n1. Calls present for missing.txt and catches its error without creating that file.\n2. Uses bash to run exactly `printf \"DELIVERED_REPORT\\n\" > report.txt; printf \"DELIVERED_NOTE\\n\" > 说明.txt`.\n3. Calls present for report.txt and 说明.txt.\n4. After present succeeds, deliberately throws the string \"AFTER_PRESENT\" (not an Error object) from that same run_code program.\n5. Does not retry the program or create other files.\n6. Finishes by mentioning report.txt and 说明.txt in inline code, and put PRESENT_DONE in a separate paragraph.\n\nLet me write the program.\n\npresent tool args: { files: [{ path, description? }] }.\n\nSo first: try present for missing.txt — catch error. It should error because the file doesn't exist (present requires existing workspace file).\n\nThen bash run exact command. Note the command uses `;` inside a single string. Run as a single command string: `printf \"DELIVERED_REPORT\\n\" > report.txt; printf \"DELIVERED_NOTE\\n\" > 说明.txt`.\n\nWait — printf interprets the arguments... In bash, `printf \"DELIVERED_REPORT\\n\"` with the format string containing \\n — printf will interpret \\n as newline. Good.\n\nThen present for report.txt and 说明.txt.\n\nThen throw \"AFTER_PRESENT\" string. In TS async function body, `throw \"AFTER_PRESENT\"` works.\n\nLet me write it."}}},{"type":"chunk","time":1788856037719,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_GgvYPol45qxGf6GezK3L3590","name":"run_code","arguments":"{\"code\": \"// Step 1: present for missing.txt — must fail, catch the error, don't create the file\\ntry {\\n await tools.present({ files: [{ path: \\\"missing.txt\\\", description: \\\"must not exist\\\" }] });\\n console.log(\\\"UNEXPECTED: present succeeded for missing.txt\\\");\\n} catch (err) {\\n console.log(\\\"Caught present error for missing.txt:\\\", typeof err, String(err).slice(0, 200));\\n}\\n\\n// Step 2: run the exact bash command to create the two report files\\nconst res = await tools.bash({\\n command: 'printf \\\"DELIVERED_REPORT\\\\n\\\" > report.txt; printf \\\"DELIVERED_NOTE\\\\n\\\" > 说明.txt',\\n description: \\\"Write DELIVERED_REPORT and DELIVERED_NOTE to files\\\",\\n});\\nconsole.log(\\\"bash exit:\\\", res.exitCode);\\n\\n// Step 3: present for report.txt and 说明.txt (must succeed)\\nconst p2 = await tools.present({\\n files: [\\n { path: \\\"report.txt\\\", description: \\\"delivered report\\\" },\\n { path: \\\"说明.txt\\\", description: \\\"delivered note\\\" },\\n ],\\n});\\nconsole.log(\\\"present ok, files:\\\", p2.files.map((f) => f.path).join(\\\", \\\"));\\n\\n// Step 4: deliberately throw a plain string AFTER_PRESENT (not an Error)\\nthrow \\\"AFTER_PRESENT\\\";\", \"description\": \"Present files then deliberately throw AFTER_PRESENT\"}"}}},{"type":"chunk","time":1788856037719,"chunk":{"type":"usage","usage":{"inputTokens":981,"outputTokens":695,"totalTokens":9356,"cacheReadTokens":7680,"reasoningTokens":329}}},{"type":"chunk","time":1788856037719,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_00_GgvYPol45qxGf6GezK3L3590","name":"run_code","arguments":"{\"code\": \"// Step 1: present for missing.txt — must fail, catch the error, don't create the file\\ntry {\\n await tools.present({ files: [{ path: \\\"missing.txt\\\", description: \\\"must not exist\\\" }] });\\n console.log(\\\"UNEXPECTED: present succeeded for missing.txt\\\");\\n} catch (err) {\\n console.log(\\\"Caught present error for missing.txt:\\\", typeof err, String(err).slice(0, 200));\\n}\\n\\n// Step 2: run the exact bash command to create the two report files\\nconst res = await tools.bash({\\n command: 'printf \\\"DELIVERED_REPORT\\\\n\\\" > report.txt; printf \\\"DELIVERED_NOTE\\\\n\\\" > 说明.txt',\\n description: \\\"Write DELIVERED_REPORT and DELIVERED_NOTE to files\\\",\\n});\\nconsole.log(\\\"bash exit:\\\", res.exitCode);\\n\\n// Step 3: present for report.txt and 说明.txt (must succeed)\\nconst p2 = await tools.present({\\n files: [\\n { path: \\\"report.txt\\\", description: \\\"delivered report\\\" },\\n { path: \\\"说明.txt\\\", description: \\\"delivered note\\\" },\\n ],\\n});\\nconsole.log(\\\"present ok, files:\\\", p2.files.map((f) => f.path).join(\\\", \\\"));\\n\\n// Step 4: deliberately throw a plain string AFTER_PRESENT (not an Error)\\nthrow \\\"AFTER_PRESENT\\\";\", \"description\": \"Present files then deliberately throw AFTER_PRESENT\"}"}} +{"type":"tool/code-dispatch-start","data":{"rootCallId":"call_00_GgvYPol45qxGf6GezK3L3590","parentCallId":"call_00_GgvYPol45qxGf6GezK3L3590","subCallId":"call_00_GgvYPol45qxGf6GezK3L3590:code:1","name":"present","arguments":{"files":[{"path":"missing.txt","description":"must not exist"}]}}} +{"type":"tool/code-dispatch","data":{"rootCallId":"call_00_GgvYPol45qxGf6GezK3L3590","parentCallId":"call_00_GgvYPol45qxGf6GezK3L3590","subCallId":"call_00_GgvYPol45qxGf6GezK3L3590:code:1","name":"present","arguments":{"files":[{"path":"missing.txt","description":"must not exist"}]},"isError":true,"content":[{"type":"text","text":"Error: Cannot present missing.txt: file not found. Check the path, create the file if needed, and retry."}]}} +{"type":"tool/code-dispatch-start","data":{"rootCallId":"call_00_GgvYPol45qxGf6GezK3L3590","parentCallId":"call_00_GgvYPol45qxGf6GezK3L3590","subCallId":"call_00_GgvYPol45qxGf6GezK3L3590:code:2","name":"bash","arguments":{"command":"printf \"DELIVERED_REPORT\n\" > report.txt; printf \"DELIVERED_NOTE\n\" > 说明.txt","description":"Write DELIVERED_REPORT and DELIVERED_NOTE to files"}}} +{"type":"tool/code-dispatch","data":{"rootCallId":"call_00_GgvYPol45qxGf6GezK3L3590","parentCallId":"call_00_GgvYPol45qxGf6GezK3L3590","subCallId":"call_00_GgvYPol45qxGf6GezK3L3590:code:2","name":"bash","arguments":{"command":"printf \"DELIVERED_REPORT\n\" > report.txt; printf \"DELIVERED_NOTE\n\" > 说明.txt","description":"Write DELIVERED_REPORT and DELIVERED_NOTE to files"},"isError":false,"content":[{"type":"text","text":"(no output)"}]}} +{"type":"tool/code-dispatch-start","data":{"rootCallId":"call_00_GgvYPol45qxGf6GezK3L3590","parentCallId":"call_00_GgvYPol45qxGf6GezK3L3590","subCallId":"call_00_GgvYPol45qxGf6GezK3L3590:code:3","name":"present","arguments":{"files":[{"path":"report.txt","description":"delivered report"},{"path":"说明.txt","description":"delivered note"}]}}} +{"type":"deliverables/presented","data":{"turn":1,"callId":"call_00_GgvYPol45qxGf6GezK3L3590:code:3","files":[{"path":"report.txt","description":"delivered report","attachmentId":"sha256:7e7bab1fdcf9c16099c5a1ec237feba0d936c8e57cf521cab61b99a51a49c169","name":"report.txt","bytes":17},{"path":"说明.txt","description":"delivered note","attachmentId":"sha256:5e019bfe8e78d6f01f71b6a8ac6477ee8ed3895861259c6565ecd9e736f1d69f","name":"说明.txt","bytes":15}]}} +{"type":"tool/code-dispatch","data":{"rootCallId":"call_00_GgvYPol45qxGf6GezK3L3590","parentCallId":"call_00_GgvYPol45qxGf6GezK3L3590","subCallId":"call_00_GgvYPol45qxGf6GezK3L3590:code:3","name":"present","arguments":{"files":[{"path":"report.txt","description":"delivered report"},{"path":"说明.txt","description":"delivered note"}]},"isError":false,"content":[{"type":"text","text":"Presented report.txt (17 bytes)\nPresented 说明.txt (15 bytes)"}]}} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_GgvYPol45qxGf6GezK3L3590"},"content":[{"type":"tool-result","toolCallId":"call_00_GgvYPol45qxGf6GezK3L3590","content":[{"type":"text","text":"Error: code run failed (exception): AFTER_PRESENT\nCaptured output:\nCaught present error for missing.txt: object ToolCallError: Cannot present missing.txt: file not found. Check the path, create the file if needed, and retry.\nbash exit: 0\npresent ok, files: report.txt, 说明.txt"}],"isError":true}],"role":"user","id":"{{message:4}}"},"error":{"name":"CodeRunFailedError","code":"CODE_RUN_FAILED"}},"sourceEventSeqs":[13],"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":1}} +{"type":"step/start","data":{"turn":1,"step":2}} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The program ran as intended:\n1. present for missing.txt failed with a ToolCallError, caught and logged — missing.txt was not created.\n2. bash ran exactly the given command, exit 0.\n3. present succeeded for report.txt and 说明.txt.\n4. Then threw the plain string \"AFTER_PRESENT\" (program failed with that exception, as expected — not an Error object).\n\nI did not retry the program or create other files. Now finish mentioning `report.txt` and `说明.txt` in inline code, with PRESENT_DONE in a separate paragraph."},{"type":"text","text":"The single program ran exactly as ordered: `present` for `missing.txt` failed and was caught (no file created), bash ran the exact command successfully, then `present` succeeded for `report.txt` and `说明.txt`, after which the program deliberately threw the string `AFTER_PRESENT` — no retries, no extra files.\n\nPRESENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":94,"outputTokens":198,"totalTokens":9636,"cacheReadTokens":9344,"reasoningTokens":121},"stream":[{"type":"chunk","time":1788841400011,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788841400011,"index":0,"dt":[117,22,0,0,0,0,1,22,0,0,40,0,0,0,13,1,0,0,0,22,0,21,1,22,0,0,23,0,0,0,0,0,23,1,19,23,0,0,0,0,22,0,0,0,0,0,22,0,0,0,0,0,23,0,0,0,22,0,22,0,0,0,0,1,22,0,1,0,0,21,0,22,1,0,23,0,21,1,22,22,0,0,0,0,0,22,1,0,22,1,0,0,22,0,0,1,21,22,1,0,0,0,0,23,0,1,0,0,0,22,1,19,0,0,0,0,1,22,1,0],"texts":["The"," program"," ran"," as"," intended",":\n","1","."," present"," for"," missing",".txt"," failed"," with"," a"," Tool","Call","Error",","," caught"," and"," logged"," —"," missing",".txt"," was"," not"," created",".\n","2","."," bash"," ran"," exactly"," the"," given"," command",","," exit"," ","0",".\n","3","."," present"," succeeded"," for"," report",".txt"," and"," ","说明",".txt",".\n","4","."," Then"," threw"," the"," plain"," string"," \"","AF","TER","_P","RES","ENT","\""," (","program"," failed"," with"," that"," exception",","," as"," expected"," —"," not"," an"," Error"," object",").\n\n","I"," did"," not"," ret","ry"," the"," program"," or"," create"," other"," files","."," Now"," finish"," mentioning"," `","report",".txt","`"," and"," `","说明",".txt","`"," in"," inline"," code",","," with"," PRES","ENT","_D","ONE"," in"," a"," separate"," paragraph","."]},{"type":"chunk","time":1788841400853,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788841400853,"index":1,"dt":[0,20,23,28,1,0,16,0,25,0,0,0,0,0,19,0,23,0,0,0,22,1,0,23,0,25,1,0,0,0,16,1,22,0,0,0,0,0,22,0,0,0,0,1,21,0,0,23,0,1,0,21,0,1,0,0,21,1,1,0,0,0,20,1,0,22,1,0,22,0,0,0,0,1,22],"texts":["The"," single"," program"," ran"," exactly"," as"," ordered",":"," `","present","`"," for"," `","missing",".txt","`"," failed"," and"," was"," caught"," (","no"," file"," created","),"," bash"," ran"," the"," exact"," command"," successfully",","," then"," `","present","`"," succeeded"," for"," `","report",".txt","`"," and"," `","说明",".txt","`,"," after"," which"," the"," program"," deliberately"," threw"," the"," string"," `","AF","TER","_P","RES","ENT","`"," —"," no"," ret","ries",","," no"," extra"," files",".\n\n","PR","ES","ENT","_D","ONE"]},{"type":"chunk","time":1788841401575,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The program ran as intended:\n1. present for missing.txt failed with a ToolCallError, caught and logged — missing.txt was not created.\n2. bash ran exactly the given command, exit 0.\n3. present succeeded for report.txt and 说明.txt.\n4. Then threw the plain string \"AFTER_PRESENT\" (program failed with that exception, as expected — not an Error object).\n\nI did not retry the program or create other files. Now finish mentioning `report.txt` and `说明.txt` in inline code, with PRESENT_DONE in a separate paragraph."}}},{"type":"chunk","time":1788841401575,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"The single program ran exactly as ordered: `present` for `missing.txt` failed and was caught (no file created), bash ran the exact command successfully, then `present` succeeded for `report.txt` and `说明.txt`, after which the program deliberately threw the string `AFTER_PRESENT` — no retries, no extra files.\n\nPRESENT_DONE"}}},{"type":"chunk","time":1788841401575,"chunk":{"type":"usage","usage":{"inputTokens":94,"outputTokens":198,"totalTokens":9636,"cacheReadTokens":9344,"reasoningTokens":121}}},{"type":"chunk","time":1788841401575,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":2}} +{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/web/present/snapshot.yml b/snapshots/web/present/snapshot.yml new file mode 100644 index 0000000000..67b8b12df0 --- /dev/null +++ b/snapshots/web/present/snapshot.yml @@ -0,0 +1,9 @@ +version: 1 +scenario: present +profile: web +composition: web-ptc +recording: live +header: + class: web-ptc +workspace: + final: true diff --git a/snapshots/web/present/ui.expected.md b/snapshots/web/present/ui.expected.md new file mode 100644 index 0000000000..3004a21a11 --- /dev/null +++ b/snapshots/web/present/ui.expected.md @@ -0,0 +1,109 @@ +- banner: + - navigation "Session hierarchy": + - button "Use one run_code program to" [disabled] + - img + - text: PTC mode + - button "Session log": + - text: Session log + - img + - button "Open the sidebar": + - img + - tablist: + - tab "Chat" [selected] + - tab "Trajectory" +- button "System prompt": + - img + - img + - text: System prompt +- text: "Use one run_code program to do the following in order. Call present for missing.txt and catch its error without creating that file. Use bash to run exactly `printf \"DELIVERED_REPORT\\n\" > report.txt; printf \"DELIVERED_NOTE\\n\" > 说明.txt`. Call present for report.txt and 说明.txt. After present succeeds, deliberately throw the string \"AFTER_PRESENT\" (not an Error object) from that same run_code program. Do not retry the program or create any other files. Finish by mentioning `report.txt` and `说明.txt` in inline code, and put PRESENT_DONE in a separate paragraph. {{clock}}" +- button "Copy": + - img +- button "1 tool call" [expanded]: + - text: 1 tool call + - img +- button "Context injection @deepseek-ai/dsh-system-prompt": + - img + - img + - text: Context injection @deepseek-ai/dsh-system-prompt +- button "Think The user wants one run_code program that:": + - img + - img + - text: "Think The user wants one run_code program that:" +- text: Failed +- 'button "Code Error: code run failed (exception): AFTER_PRESENT"': + - img + - text: "Code Error: code run failed (exception): AFTER_PRESENT" +- button "Present files Delivery failed missing.txt": + - img + - text: Present files Delivery failed missing.txt +- button "Bash Write DELIVERED_REPORT and DELIVERED_NOTE to files": + - img + - img + - text: Bash Write DELIVERED_REPORT and DELIVERED_NOTE to files +- button "Present files Delivered report.txt, 说明.txt": + - img + - text: Present files Delivered report.txt, 说明.txt +- button "Think The program ran as intended:": + - img + - img + - text: "Think The program ran as intended:" +- paragraph: + - text: "The single program ran exactly as ordered:" + - code: present + - text: for + - code: missing.txt + - text: failed and was caught (no file created), bash ran the exact command successfully, then + - code: present + - text: succeeded for + - code: + - button "Download report.txt": report.txt + - text: and + - code: + - button "Download 说明.txt": 说明.txt + - text: ", after which the program deliberately threw the string" + - code: AFTER_PRESENT + - text: — no retries, no extra files. +- paragraph: PRESENT_DONE +- text: Deliverables +- link "Download report.txt": + - /url: /api/present.download?sessionId=session-{{uuid}}&seq=19&index=0 + - text: report.txt TXT · 17B delivered report + - img + - text: Download +- link "Download 说明.txt": + - /url: /api/present.download?sessionId=session-{{uuid}}&seq=19&index=1 + - text: 说明.txt TXT · 15B delivered note + - img + - text: Download +- button "Copy": + - img +- button "Good response": + - img +- button "Bad response": + - img +- button "Branch into a new conversation": + - img +- button "Usage 19K tok": + - img + - text: Usage 19K tok +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} +- textbox "Message or run a task, / commands, @ files or sessions" +- button "Commands": + - img +- button "Add attachment": + - img +- 'button "Access mode, current: Workspace Write"': Workspace Write +- button "Select model, current DeepSeek-V4-Flash": + - text: DeepSeek-V4-Flash + - img +- button "8% of context used" +- button "Send message" [disabled] +- button "1 turns 2 steps · {{throughput}} tok/s": + - img + - text: 1 turns 2 steps{{throughput}} tok/s +- button "19K tok · Cache hit 94%": + - img + - text: 19K tokCache hit 94% diff --git a/snapshots/web/present/workspace.expected/report.txt b/snapshots/web/present/workspace.expected/report.txt new file mode 100644 index 0000000000..d94ec652e9 --- /dev/null +++ b/snapshots/web/present/workspace.expected/report.txt @@ -0,0 +1 @@ +DELIVERED_REPORT diff --git a/snapshots/web/present/workspace.expected/说明.txt b/snapshots/web/present/workspace.expected/说明.txt new file mode 100644 index 0000000000..e0f8e32623 --- /dev/null +++ b/snapshots/web/present/workspace.expected/说明.txt @@ -0,0 +1 @@ +DELIVERED_NOTE diff --git a/snapshots/web/ptc-round/system-prompt.expected.md b/snapshots/web/ptc-round/system-prompt.expected.md index 13e7e6ef83..4da368fcf2 100644 --- a/snapshots/web/ptc-round/system-prompt.expected.md +++ b/snapshots/web/ptc-round/system-prompt.expected.md @@ -162,6 +162,15 @@ interface ToolArgsMap { /** children (default) lists direct children only; descendants walks the complete tree below you. */ scope?: "children" | "descendants"; } & Record; + /** Deliver final files to the user. Saves a snapshot of each existing workspace file so it remains downloadable after edits or deletion. Create the files before calling this tool. */ + present: { + files: { + /** Path of an existing file inside the workspace. */ + path: string; + /** Brief description for the user. */ + description?: string; + }[]; + } & Record; /** Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools. */ ralph: { /** The immutable completion objective for every fresh Ralph round. */ @@ -399,6 +408,16 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; + present: { + turn: number; + files: { + path: string; + name: string; + attachmentId: string; + bytes: number; + description?: string; + }[]; + }; ralph: { runId: string; agentsStarted: number; diff --git a/tsconfig.base.json b/tsconfig.base.json index 637fc6e454..e6f4d7c9b9 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -195,6 +195,7 @@ "@deepseek-ai/dsh-client-ui-conversation": ["./packages/client/ui-conversation/src"], "@deepseek-ai/dsh-client-ui-conversation/client": ["./packages/client/ui-conversation/src/client/index.ts"], "@deepseek-ai/dsh-client-ui-tool": ["./packages/client/ui-tool/src"], + "@deepseek-ai/dsh-tool-present/types": ["./packages/fs/tool-present/src/types.ts"], "@deepseek-ai/dsh-client-ui-deliverables": ["./packages/client/ui-deliverables/src"], "@deepseek-ai/dsh-client-ui-workflow-run": ["./packages/client/ui-workflow-run/src"], "@deepseek-ai/dsh-client-ui-input-trigger": ["./packages/client/ui-input-trigger/src"], @@ -417,6 +418,7 @@ "@deepseek-ai/dsh-tool-goal": ["./packages/goal/tool-goal/src"], "@deepseek-ai/dsh-tool-jobs": ["./packages/jobs/tool-jobs/src"], "@deepseek-ai/dsh-tool-lsp": ["./packages/lsp/tool-lsp/src"], + "@deepseek-ai/dsh-tool-present": ["./packages/fs/tool-present/src"], "@deepseek-ai/dsh-tool-pwsh-persistent": ["./packages/shell/tool-pwsh-persistent/src"], "@deepseek-ai/dsh-tool-ralph": ["./packages/workflow/tool-ralph/src"], "@deepseek-ai/dsh-tool-session-query": ["./packages/session-query/tool-session-query/src"], diff --git a/tsconfig.client.json b/tsconfig.client.json index b9a2a55382..9fa63a2b40 100644 --- a/tsconfig.client.json +++ b/tsconfig.client.json @@ -75,7 +75,7 @@ { "path": "./packages/client/ui-chat" }, { "path": "./packages/client/ui-conversation" }, { "path": "./packages/client/ui-tool" }, - { "path": "./packages/client/ui-deliverables" }, + { "path": "./packages/client/ui-deliverables/tsconfig.client.json" }, { "path": "./packages/client/ui-workflow-run" }, { "path": "./packages/client/ui-workspace" }, { "path": "./packages/client/ui-input-trigger" }, diff --git a/tsconfig.host.json b/tsconfig.host.json index e13180bbdf..ece0365eb9 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -49,6 +49,7 @@ "apps/web/tests/rail-search-expand.e2e.ts", "apps/web/tests/conversation-column-overflow.e2e.ts", "apps/web/tests/ptc-round.e2e.ts", + "apps/web/tests/present.e2e.ts", "apps/web/tests/composer-draft-scroll.e2e.ts", "apps/web/tests/cordis-tool-round.e2e.ts", "apps/web/tests/web-search-round.e2e.ts", @@ -129,6 +130,7 @@ "scripts/client-bundle-purity.spec.ts" ], "references": [ + { "path": "./packages/client/ui-deliverables/tsconfig.host.json" }, { "path": "./vendor/cosmokit" }, { "path": "./vendor/schemastery" }, { "path": "./vendor/cordis" }, @@ -267,6 +269,7 @@ { "path": "./packages/fs/fs-observation-policy" }, { "path": "./packages/fs/fs-sandbox" }, { "path": "./packages/fs/tool-fs" }, + { "path": "./packages/fs/tool-present" }, { "path": "./packages/fs/tool-fs-search" }, { "path": "./packages/fs/tool-str-replace-editor" }, { "path": "./packages/compaction/compaction" }, From 6d8a400d79ac6905a9b55352314342a4061d14f7 Mon Sep 17 00:00:00 2001 From: yudshj Date: Tue, 8 Sep 2026 18:26:50 +0800 Subject: [PATCH 49/58] fix(web): open delivered snapshots in the default application --- ...09-08-web-explicit-file-delivery.i18n.yaml | 4 +- .../2026-09-08-web-explicit-file-delivery.md | 10 +- ...026-09-08-web-explicit-file-delivery.zh.md | 10 +- apps/web/tests/present.e2e.ts | 66 +++++++++---- docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 2 +- docs/config-catalog.zh.md | 2 +- .../client/ui-deliverables/README.i18n.yaml | 4 +- packages/client/ui-deliverables/README.md | 12 ++- packages/client/ui-deliverables/README.zh.md | 8 +- packages/client/ui-deliverables/package.json | 3 +- .../src/client/Deliverables.module.css | 5 +- .../src/client/Deliverables.tsx | 45 +++++---- .../ui-deliverables/src/client/index.ts | 19 ++-- .../ui-deliverables/src/client/locales.ts | 14 ++- .../src/client/present-open.ts | 54 +++++++++++ packages/client/ui-deliverables/src/index.ts | 6 +- .../ui-deliverables/src/present-download.ts | 22 ++++- .../ui-deliverables/src/present-open.ts | 53 +++++++++++ .../client/ui-deliverables/src/presented.ts | 12 ++- .../tests/present-download.host.spec.ts | 93 ++++++++++++++++++- .../tests/present-open.client.spec.ts | 63 +++++++++++++ .../tests/produced-files.client.spec.tsx | 56 +++++++---- .../ui-deliverables/tests/prompt.host.spec.ts | 1 + .../client/ui-deliverables/tsconfig.host.json | 6 +- pnpm-lock.yaml | 3 + snapshots/web/present/ui.expected.md | 16 ++-- 27 files changed, 479 insertions(+), 114 deletions(-) create mode 100644 packages/client/ui-deliverables/src/client/present-open.ts create mode 100644 packages/client/ui-deliverables/src/present-open.ts create mode 100644 packages/client/ui-deliverables/tests/present-open.client.spec.ts diff --git a/.agents/notes/implemented/feature/2026-09-08-web-explicit-file-delivery.i18n.yaml b/.agents/notes/implemented/feature/2026-09-08-web-explicit-file-delivery.i18n.yaml index c6374bc3ae..0925919414 100644 --- a/.agents/notes/implemented/feature/2026-09-08-web-explicit-file-delivery.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-08-web-explicit-file-delivery.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-08-web-explicit-file-delivery.md -2026-09-08-web-explicit-file-delivery.md: 6ea89c6de392df4b739bb3692a313b53dd22bbe6 -2026-09-08-web-explicit-file-delivery.zh.md: e503bf520a8e08bdd65e1fc3f0f93a527f87ef95 +2026-09-08-web-explicit-file-delivery.md: 5ab211d8e8ea3c7f008cf39307f1ddfdd77acfee +2026-09-08-web-explicit-file-delivery.zh.md: 3077b23a78f0617a1a377bfc37de201169bbdc82 diff --git a/.agents/notes/implemented/feature/2026-09-08-web-explicit-file-delivery.md b/.agents/notes/implemented/feature/2026-09-08-web-explicit-file-delivery.md index 6ea89c6de3..5ab211d8e8 100644 --- a/.agents/notes/implemented/feature/2026-09-08-web-explicit-file-delivery.md +++ b/.agents/notes/implemented/feature/2026-09-08-web-explicit-file-delivery.md @@ -10,15 +10,17 @@ Workspace links read live paths, so edits or deletion can invalidate a final del ## Decision -The [present tool](../../../../packages/fs/tool-present/README.md) owns execution, immutable snapshots, delivery types, and the durable event. The [deliverables plugin](../../../../packages/client/ui-deliverables/README.md) owns authenticated downloads and browser rendering, with type-only imports from the tool’s `./types` entry. The `standard`, `ptc`, and `cordis` presets mount the tool package; `minimal` retains its two-tool training configuration. The existing attachment service saves immutable bytes; successful final `tools/result` notifications append `deliverables/presented` to the calling Session. Native and nested calls use the same recorder. A later enclosing program failure does not undo a completed nested delivery. Blocked tool results publish none. +The [present tool](../../../../packages/fs/tool-present/README.md) owns execution, immutable snapshots, delivery types, and the durable event. The [deliverables plugin](../../../../packages/client/ui-deliverables/README.md) owns authenticated snapshot actions and browser rendering, with type-only imports from the tool’s `./types` entry. The `standard`, `ptc`, and `cordis` presets mount the tool package; `minimal` retains its two-tool training configuration. The existing attachment service saves immutable bytes; successful final `tools/result` notifications append `deliverables/presented` to the calling Session. Native and nested calls use the same recorder. A later enclosing program failure does not undo a completed nested delivery. Blocked tool results publish none. -Downloads authorize a reference by the viewed Session, event sequence, and file index. The event stores no Session ID, so forked history uses the child's own log. The existing produced-file row keeps its names and behavior. Session ZIP retains delivery events but does not collect their attachment bytes. +Download and native-open requests authorize a reference by the viewed Session, event sequence, and file index. The event stores no Session ID, so forked history uses the child's own log. The existing produced-file row keeps its names and behavior. Session ZIP retains delivery events but does not collect their attachment bytes. + +Card and closing-mention gestures open a verified private copy with the existing native-command utility. A POST expresses the desktop side effect; GET remains a byte read. Each gesture receives a new copy so application edits cannot corrupt the immutable attachment or alter later opens. Successful copies survive until plugin disposal for applications that read lazily; failed copies are removed immediately, and disposal awaits cancelled work before cleanup. ## Alternatives considered **A Host tool subpath in the UI package** couples preset installation to browser packaging and requires extra published entries. An ordinary tool package preserves shared filesystem and tool error classes through the repository’s peer dependency rules. -**Live workspace links** cannot preserve a delivered version after edits or deletion. +**Live workspace links** cannot preserve a delivered version after edits or deletion. Opening the attachment store’s own path instead would expose immutable saved bytes to application writes. **Generic artifact fields throughout tools, dispatch, and Session** would broaden unrelated APIs for one Web feature. A plugin-owned event uses existing extension points and avoids parent-result forwarding. @@ -32,4 +34,4 @@ The implementation adds no artifact service or attachment format. Unreferenced s The delivery event is required-on-read because it is the authorization index for saved bytes, not only display metadata. Skipping it would allow an older reader to reconstruct or fork a Session without its completed deliveries. Unsupported readers refuse that loss instead of silently dropping the references. -Focused tests cover snapshot bytes, invalid inputs, blocked results, HTTP integrity, turn isolation, and fork-addressed links. The recorded Web scenario covers nested completion followed by an enclosing failure, source deletion, reload, and ZIP exclusion. +Focused tests cover snapshot bytes, invalid inputs, blocked results, HTTP integrity, native-open copy isolation, retry and disposal, turn isolation, and fork-addressed actions. The recorded Web scenario covers nested completion followed by an enclosing failure, source deletion, reload, native-open gestures without browser downloads, and ZIP exclusion. diff --git a/.agents/notes/implemented/feature/2026-09-08-web-explicit-file-delivery.zh.md b/.agents/notes/implemented/feature/2026-09-08-web-explicit-file-delivery.zh.md index e503bf520a..3077b23a78 100644 --- a/.agents/notes/implemented/feature/2026-09-08-web-explicit-file-delivery.zh.md +++ b/.agents/notes/implemented/feature/2026-09-08-web-explicit-file-delivery.zh.md @@ -10,15 +10,17 @@ Status: implemented ## 决策 -[present 工具](../../../../packages/fs/tool-present/README.zh.md)拥有执行、不可变快照、交付类型和持久事件。[交付插件](../../../../packages/client/ui-deliverables/README.zh.md)拥有认证下载和浏览器渲染,仅从工具的 `./types` 入口导入类型。`standard`、`ptc` 与 `cordis` preset 挂载工具包;`minimal` 保留双工具训练配置。现有 attachment 服务保存不可变字节;成功的最终 `tools/result` 通知将 `deliverables/presented` 追加到调用方 Session。原生与嵌套调用使用同一个记录器。外层程序随后失败不会撤销已完成的嵌套交付。被阻止的工具结果不发布交付。 +[present 工具](../../../../packages/fs/tool-present/README.zh.md)拥有执行、不可变快照、交付类型和持久事件。[交付插件](../../../../packages/client/ui-deliverables/README.zh.md)拥有认证快照操作和浏览器渲染,仅从工具的 `./types` 入口导入类型。`standard`、`ptc` 与 `cordis` preset 挂载工具包;`minimal` 保留双工具训练配置。现有 attachment 服务保存不可变字节;成功的最终 `tools/result` 通知将 `deliverables/presented` 追加到调用方 Session。原生与嵌套调用使用同一个记录器。外层程序随后失败不会撤销已完成的嵌套交付。被阻止的工具结果不发布交付。 -下载通过当前查看的 Session、事件序号与文件索引授权引用。事件不保存 Session ID,因此 fork 历史使用子 Session 自己的日志。现有产出文件行保留其名称和行为。Session ZIP 保留交付事件,但不收集其中引用的 attachment 字节。 +下载与原生打开请求通过当前查看的 Session、事件序号与文件索引授权引用。事件不保存 Session ID,因此 fork 历史使用子 Session 自己的日志。现有产出文件行保留其名称和行为。Session ZIP 保留交付事件,但不收集其中引用的 attachment 字节。 + +卡片和收尾引用操作通过现有 native-command 工具,在默认应用中打开经过校验的私有副本。POST 表达桌面副作用;GET 仍仅读取字节。每次操作创建新副本,避免应用内编辑损坏不可变 attachment 或改变后续打开的内容。成功副本保留到插件释放,以支持延迟读取的应用;失败副本立即删除,释放时先等待取消的操作结束再清理。 ## 已考虑的替代方案 **在 UI 包中提供 Host 工具子路径**会将 preset 安装与浏览器打包耦合,并要求额外发布入口。普通工具包通过仓库 peer dependency 规则保留共享的文件系统和工具错误类。 -**实时工作区链接**无法在编辑或删除后保留已交付版本。 +**实时工作区链接**无法在编辑或删除后保留已交付版本。直接打开 attachment 存储路径则会使不可变保存字节暴露于应用写入。 **在工具、dispatch 和 Session 中增加通用 artifact 字段**会为单个 Web 功能扩大无关 API。插件拥有的事件使用现有扩展点,并省去父调用结果转发。 @@ -32,4 +34,4 @@ Status: implemented 交付事件要求读取端识别,因为它是保存字节的授权索引,不只是显示元数据。跳过事件会让旧读取端在重建或分叉 Session 时丢失已完成的交付。不支持该事件的读取端拒绝读取,避免静默丢弃引用。 -定向测试覆盖快照字节、无效输入、被阻止的结果、HTTP 完整性、turn 隔离及使用 fork 地址的链接。录制 Web 场景覆盖嵌套调用完成后外层失败、源文件删除、重新加载和 ZIP 排除。 +定向测试覆盖快照字节、无效输入、被阻止的结果、HTTP 完整性、原生打开的副本隔离、重试与释放、turn 隔离及使用 fork 地址的操作。录制 Web 场景覆盖嵌套调用完成后外层失败、源文件删除、重新加载、不触发浏览器下载的原生打开操作和 ZIP 排除。 diff --git a/apps/web/tests/present.e2e.ts b/apps/web/tests/present.e2e.ts index c76377ec23..0f48cac2f2 100644 --- a/apps/web/tests/present.e2e.ts +++ b/apps/web/tests/present.e2e.ts @@ -1,10 +1,11 @@ /** Recorded delivery, source deletion, reload, and Session ZIP behavior. */ -import { readFile, unlink, mkdir } from 'node:fs/promises' -import { join } from 'node:path' +import { readFile, unlink, mkdir, mkdtemp, writeFile, rm } from 'node:fs/promises' +import { join, delimiter } from 'node:path' import { fileURLToPath } from 'node:url' import { chromium, type Browser, type Page } from 'playwright' import { unzipSync, strFromU8 } from 'fflate' -import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' +import { tmpdir, release } from 'node:os' import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session' import type {} from '@deepseek-ai/dsh-tool-present/types' import { @@ -22,7 +23,8 @@ const PROMPT = 'Use one run_code program to do the following in order. Call pres + 'Call present for report.txt and 说明.txt. After present succeeds, deliberately throw the string "AFTER_PRESENT" (not an Error object) from that same run_code program. ' + 'Do not retry the program or create any other files. Finish by mentioning `report.txt` and `说明.txt` in inline code, and put PRESENT_DONE in a separate paragraph.' -describe('web e2e: explicit file delivery', () => { +// The recorded Bash scenario and executable opener fixture require a POSIX host outside WSL. +describe.skipIf(process.platform === 'win32' || release().toLowerCase().includes('microsoft'))('web e2e: explicit file delivery', () => { let scaffold: WebScaffold let browser: Browser let page: Page @@ -31,8 +33,22 @@ describe('web e2e: explicit file delivery', () => { let cwd: string let disposeApproval: (() => void) | undefined const events: SessionEvent[] = [] + let nativeRoot: string | undefined + let openLog: string + const opened = async (): Promise => (await readFile(openLog, 'utf8')).split('\n').filter(Boolean).map(line => JSON.parse(line) as string) + const downloads: string[] = [] beforeAll(async () => { + nativeRoot = await mkdtemp(join(tmpdir(), 'dsh-present-native-')) + openLog = join(nativeRoot, 'opened.jsonl') + await writeFile(openLog, '') + // Exercise the built Host through its actual OS command, replacing only the desktop application. + const command = process.platform === 'darwin' ? 'open' : 'xdg-open' + await writeFile(join(nativeRoot, command), `#!${process.execPath} +const fs = require('node:fs'); +fs.appendFileSync(${JSON.stringify(openLog)}, JSON.stringify(fs.readFileSync(process.argv[2], 'utf8')) + '\\n'); +`, { mode: 0o700 }) + vi.stubEnv('PATH', `${nativeRoot}${delimiter}${process.env.PATH ?? ''}`) await mkdir(DIR, { recursive: true }) scaffold = await launchWebScaffold({ agentPresets: { roots: [], default: 'ptc' }, compareReplaySession: true, @@ -43,15 +59,24 @@ describe('web e2e: explicit file delivery', () => { browser = await chromium.launch() page = await newEnglishPage(browser) tripwire = watchConsole(page) + page.on('download', (download) => { downloads.push(download.suggestedFilename()) }) await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) await connectFreshWorkspace(page, scaffold.workspaceCwd) }, 120_000) afterAll(async () => { - await browser?.close() - disposeApproval?.() - await scaffold?.close() + try { + await browser?.close() + } finally { + disposeApproval?.() + try { + await scaffold?.close() + } finally { + vi.unstubAllEnvs() + if (nativeRoot !== undefined) await rm(nativeRoot, { recursive: true, force: true }) + } + } }) it('delivers nested snapshots even when the enclosing program subsequently fails', async () => { @@ -73,7 +98,7 @@ describe('web e2e: explicit file delivery', () => { expect(events.some(event => event.type === 'tool/result' && event.data.message.content[0].isError)).toBe(true) }, 200_000) - it('downloads after source deletion and reload, while Session ZIP contains only references', async () => { + it('opens saved copies after source deletion and reload, while Session ZIP contains only references', async () => { await unlink(join(cwd, 'report.txt')) await unlink(join(cwd, '说明.txt')) for (const reload of [false, true]) { @@ -85,16 +110,25 @@ describe('web e2e: explicit file delivery', () => { } const row = page.locator('[data-presented-files-row]') await row.waitFor() - expect(await row.getByRole('link').count()).toBe(2) + expect(await row.getByRole('button').count()).toBe(2) for (const [name, bytes] of [['report.txt', 'DELIVERED_REPORT\n'], ['说明.txt', 'DELIVERED_NOTE\n']]) { - const pending = page.waitForEvent('download') - await row.getByRole('link', { name: `Download ${name}`, exact: true }).click() - const download = await pending - expect(download.suggestedFilename()).toBe(name) - expect(await download.failure()).toBeNull() - expect(await readFile(await download.path(), 'utf8')).toBe(bytes) + const count = (await opened()).length + const response = page.waitForResponse(response => response.url().includes('/api/present.open?') && response.request().method() === 'POST') + await row.getByRole('button', { name: `Open ${name} in default app`, exact: true }).click() + expect((await response).status()).toBe(204) + await page.waitForFunction(() => document.querySelector('[data-presented-files-row] button:disabled') === null) + expect(await opened()).toHaveLength(count + 1) + expect((await opened()).at(-1)).toBe(bytes) } } + const count = (await opened()).length + const openedResponse = page.waitForResponse(response => response.url().includes('/api/present.open?') && response.request().method() === 'POST') + await page.locator('code').getByRole('button', { name: 'Open report.txt in default app', exact: true }).click() + await page.waitForFunction(() => document.querySelector('[data-presented-files-row] button:disabled') === null) + expect((await openedResponse).status()).toBe(204) + expect(await opened()).toHaveLength(count + 1) + expect((await opened()).at(-1)).toBe('DELIVERED_REPORT\n') + expect(downloads).toEqual([]) const response = await page.request.get(new URL(`/api/session.export?sessionId=${sessionId}`, scaffold.authenticatedUrl).href) expect(response.status()).toBe(200) const entries = unzipSync(await response.body()) @@ -114,7 +148,7 @@ describe('web e2e: explicit file delivery', () => { await page.setViewportSize({ width: 480, height: 900 }) const row = page.locator('[data-presented-files-row]') await row.scrollIntoViewIfNeeded() - for (const card of await row.getByRole('link').all()) { + for (const card of await row.getByRole('button').all()) { const bounds = await card.boundingBox() expect(bounds).not.toBeNull() expect(bounds!.x).toBeGreaterThanOrEqual(0) diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 05a54a98f0..9006428dc3 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-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/config-catalog.md -config-catalog.md: 58825972327e6a473f04b42a4219aaae049cd4ea -config-catalog.zh.md: 3b41c549315da44ee604e93fbb3a8718039f1ab1 +config-catalog.md: 89eac550fce4b767023d80d519ca3d60849a7e52 +config-catalog.zh.md: 7231b0867faa9998712e97be8889dc5e6f38a9ed diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 5882597232..89eac550fc 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -3459,7 +3459,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co - `@deepseek-ai/dsh-client-ui-commands` ([`packages/client/ui-commands/src/index.ts`](../packages/client/ui-commands/src/index.ts)) - `@deepseek-ai/dsh-client-ui-conversation` ([`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts)) - `@deepseek-ai/dsh-client-ui-cordis` ([`packages/extensions/ui-cordis/src/index.ts`](../packages/extensions/ui-cordis/src/index.ts)) -- `@deepseek-ai/dsh-client-ui-deliverables` — requires `systemPrompt` · `connection` · `sessionQuery` · `attachments` ([`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts)) +- `@deepseek-ai/dsh-client-ui-deliverables` — requires `systemPrompt` · `connection` · `sessionQuery` · `attachments` · `sessionController` ([`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts)) - `@deepseek-ai/dsh-client-ui-directory-picker-browse` ([`packages/client/ui-directory-picker-browse/src/index.ts`](../packages/client/ui-directory-picker-browse/src/index.ts)) - `@deepseek-ai/dsh-client-ui-directory-picker-native` ([`packages/client/ui-directory-picker-native/src/index.ts`](../packages/client/ui-directory-picker-native/src/index.ts)) - `@deepseek-ai/dsh-client-ui-goal` ([`packages/client/ui-goal/src/index.ts`](../packages/client/ui-goal/src/index.ts)) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 3b41c54931..7231b0867f 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -3461,7 +3461,7 @@ export interface Config { - `@deepseek-ai/dsh-client-ui-commands`([`packages/client/ui-commands/src/index.ts`](../packages/client/ui-commands/src/index.ts)) - `@deepseek-ai/dsh-client-ui-conversation`([`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts)) - `@deepseek-ai/dsh-client-ui-cordis`([`packages/extensions/ui-cordis/src/index.ts`](../packages/extensions/ui-cordis/src/index.ts)) -- `@deepseek-ai/dsh-client-ui-deliverables` — 需要 `systemPrompt` · `connection` · `sessionQuery` · `attachments`([`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts)) +- `@deepseek-ai/dsh-client-ui-deliverables` — 需要 `systemPrompt` · `connection` · `sessionQuery` · `attachments` · `sessionController`([`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts)) - `@deepseek-ai/dsh-client-ui-directory-picker-browse`([`packages/client/ui-directory-picker-browse/src/index.ts`](../packages/client/ui-directory-picker-browse/src/index.ts)) - `@deepseek-ai/dsh-client-ui-directory-picker-native`([`packages/client/ui-directory-picker-native/src/index.ts`](../packages/client/ui-directory-picker-native/src/index.ts)) - `@deepseek-ai/dsh-client-ui-goal`([`packages/client/ui-goal/src/index.ts`](../packages/client/ui-goal/src/index.ts)) diff --git a/packages/client/ui-deliverables/README.i18n.yaml b/packages/client/ui-deliverables/README.i18n.yaml index 4e53ba774c..d32df8879e 100644 --- a/packages/client/ui-deliverables/README.i18n.yaml +++ b/packages/client/ui-deliverables/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-deliverables/README.md -README.md: 721d7146e3d1d0499c45c5d768e5bb8e64a1fff5 -README.zh.md: 5922815b7fd558df73a62333dfb39c4b5d24bf82 +README.md: abf944d0115934d781ae44bf176914263822277e +README.zh.md: e8f556a7469800898b660fe1d81e16db02062b27 diff --git a/packages/client/ui-deliverables/README.md b/packages/client/ui-deliverables/README.md index 721d7146e3..abf944d011 100644 --- a/packages/client/ui-deliverables/README.md +++ b/packages/client/ui-deliverables/README.md @@ -30,9 +30,9 @@ Mount this plugin alongside `ui-conversation`; a finished turn then ends with th ### Explicit deliveries -The Web `standard`, `ptc`, and `cordis` presets expose `present` for final files, including files created through Bash. Call it with `files: [{ path, description? }]` after creating the files. The [present tool](../../fs/tool-present/README.md) owns snapshot creation, limits, and Session delivery records. The closing turn shows responsive file cards with names, types, sizes, descriptions, and download actions, and matching inline-code references download the same snapshots after source edits, deletion, or reload. Forks download through the viewed Session. Repeated delivery of a path selects its latest successful snapshot before the closing reply. +The Web `standard`, `ptc`, and `cordis` presets expose `present` for final files, including files created through Bash. Call it with `files: [{ path, description? }]` after creating the files. The [present tool](../../fs/tool-present/README.md) owns snapshot creation, limits, and Session delivery records. The closing turn shows responsive file cards with names, types, sizes, descriptions, and buttons that open a saved copy in the Host’s default application. Matching inline-code references open the same snapshots after source edits, deletion, or reload, without starting a browser download. Forks authorize opening through the viewed Session. Repeated delivery of a path selects its latest successful snapshot before the closing reply. -The `present` tool row shows running, delivered, failed, or interrupted status; expanding a settled row reveals its recorded result. File cards include every delivered file. +The `present` tool row shows running, delivered, failed, or interrupted status; expanding a settled row reveals its recorded result. File cards include every delivered file. Opening shows progress, confirmation, or a retryable error on the card. It requires a desktop and a suitable default application on the serving Host; a remote browser does not open applications on its own device. ### The row @@ -52,6 +52,8 @@ The closing prose carries the same vocabulary: an inline-code token resolves by The Node half registers the static `ui:deliverable-file-references` system-prompt section asking the model to mention primary files from successful creation or modification calls and to write those and any other changed-file references as Markdown inline code. The browser half registers a wrapper around `ProducedFiles` and explicit deliveries into the chat view's `conversation.chat.turnTail` hole. `deliverablesDefinition` folds each Turn's successful first-party mutation calls into `DeliverablesTurnData` from the validated raw arguments of `write`, `edit`, and mutating `str_replace_editor` commands. Reads, deletes, unsupported tools, malformed calls, and failed results contribute nothing. A new mutation tool needs an explicit Client contribution before it joins the list. The package also provides the `chatFileMentions` service the chat view consults per closing message; composing the plugin out removes both surfaces and leaves the view's empty chain at zero cost. +Native opening uses an authenticated POST addressed by Session, event sequence, and original file index. The Host streams the saved bytes into a private temporary copy and verifies the complete attachment before launching the default application. Each gesture creates a separate copy, so application edits cannot change the stored snapshot. Failed opens remove their copies; successful copies remain until plugin disposal because applications may read lazily. Disposal cancels and awaits pending work before cleanup. The authenticated GET download endpoint remains available to byte consumers. + ----- @@ -93,8 +95,8 @@ The section is static at first-party order 9000 for the lifetime of the package These limits define the current deliverables vocabulary. They are current package constraints, not a general file-linking comparison or a task backlog. - **Mention matching is exact path or unique basename only** — a suffix mention stays inert; widening the matcher is deferred until a real closing-message shape needs it. -- **Terminal-created files require explicit delivery** — call `present` to make their snapshots downloadable. -- **Transferred Session exports contain no delivered bytes** — download links require the same snapshots in the serving host’s attachment store; missing or pruned snapshots return 404. +- **Terminal-created files require explicit delivery** — call `present` to make their saved snapshots available. +- **Transferred Session exports contain no delivered bytes** — delivery actions require the same snapshots in the serving host’s attachment store; missing or pruned snapshots return 404. - **Directories have no destination** — chips open files in the right Sidebar's text preview, which shows files only; the former native folder handoff is gone rather than replaced. @@ -107,4 +109,4 @@ None. -**Runtime invariant:** No companion is published. The prompt section, slot, dictionary, download route, and optional service registrations are effect-owned with disposal proven by their plugin specs; the attachment service owns saved bytes, and the Session log owns delivery references. +**Runtime invariant:** No companion is published. The prompt section, slot, dictionary, file-action routes, and optional service registrations are effect-owned with disposal proven by their plugin specs; the attachment service owns saved bytes, and the Session log owns delivery references. diff --git a/packages/client/ui-deliverables/README.zh.md b/packages/client/ui-deliverables/README.zh.md index 5922815b7f..e8f556a746 100644 --- a/packages/client/ui-deliverables/README.zh.md +++ b/packages/client/ui-deliverables/README.zh.md @@ -30,9 +30,9 @@ kind: "package-reference" ### 显式交付 -Web 的 `standard`、`ptc` 与 `cordis` preset 提供 `present` 用于交付最终文件,包括通过 Bash 创建的文件。创建文件后,以 `files: [{ path, description? }]` 调用。[present 工具](../../fs/tool-present/README.zh.md)拥有快照创建、限制和 Session 交付记录。收尾 turn 显示响应式文件卡片,包含名称、类型、大小、说明和下载操作,匹配的行内代码引用也下载相同快照;修改或删除源文件、重新加载后仍可下载。Fork 通过当前查看的 Session 下载。同一路径重复交付时,选择收尾回复之前最近一次成功的快照。 +Web 的 `standard`、`ptc` 与 `cordis` preset 提供 `present` 用于交付最终文件,包括通过 Bash 创建的文件。创建文件后,以 `files: [{ path, description? }]` 调用。[present 工具](../../fs/tool-present/README.zh.md)拥有快照创建、限制和 Session 交付记录。收尾 turn 显示响应式文件卡片,包含名称、类型、大小、说明和在 Host 默认应用中打开保存副本的按钮。匹配的行内代码引用也打开相同快照;修改或删除源文件、重新加载后仍可打开,不触发浏览器下载。Fork 通过当前查看的 Session 授权打开。同一路径重复交付时,选择收尾回复之前最近一次成功的快照。 -`present` 工具行显示正在交付、已交付、失败或中断状态;展开已结束的调用可查看其记录的结果。文件卡片展示全部交付文件。 +`present` 工具行显示正在交付、已交付、失败或中断状态;展开已结束的调用可查看其记录的结果。文件卡片展示全部交付文件。打开时,卡片显示进度、成功确认或可重试的错误。服务 Host 必须具备桌面和合适的默认应用;远程浏览器不会打开其所在设备上的应用。 ### 该行 @@ -52,6 +52,8 @@ Web 的 `standard`、`ptc` 与 `cordis` preset 提供 `present` 用于交付最 Node 半部注册静态 `ui:deliverable-file-references` 系统提示词段,要求模型点名成功创建或修改的主要文件,并把这些文件以及正文中提到的其他本轮变更文件写成 Markdown 行内代码。浏览器半部把组合 `ProducedFiles` 与显式交付的包装组件注册进 chat 视图的 `conversation.chat.turnTail` 洞。`deliverablesDefinition` 根据 `write`、`edit` 和有修改作用的 `str_replace_editor` 命令中经过校验的原始参数,把每个轮次成功的第一方修改调用折叠进 `DeliverablesTurnData`。读取、删除、不受支持的工具、格式错误的调用和失败结果不贡献任何条目。新的修改工具必须增加显式 Client contribution 才能加入列表。本包还提供 chat 视图按收尾消息查询的 `chatFileMentions` 服务;把插件组合出去会同时移除两个表面,视图的空链以零成本留下。 +原生打开使用经过认证的 POST,通过 Session、事件序号和原始文件索引定位文件。Host 将保存的字节流写入私有临时副本,完整校验 attachment 后才启动默认应用。每次操作创建独立副本,因此应用内的编辑不会修改已保存的快照。打开失败时删除副本;成功副本保留到插件释放,因为应用可能延迟读取。释放时先取消并等待进行中的操作,再执行清理。经过认证的 GET 下载端点仍供字节读取方使用。 + ----- @@ -107,4 +109,4 @@ Node 半部注册静态 `ui:deliverable-file-references` 系统提示词段, -**运行时不变式:** 不发布伴生入口。prompt section、slot、dictionary、下载路由与可选 service 注册都归 effect 所有,释放由插件测试证明;attachment 服务拥有保存的字节,Session 日志拥有交付引用。 +**运行时不变式:** 不发布伴生入口。prompt section、slot、dictionary、文件操作路由与可选 service 注册都归 effect 所有,释放由插件测试证明;attachment 服务拥有保存的字节,Session 日志拥有交付引用。 diff --git a/packages/client/ui-deliverables/package.json b/packages/client/ui-deliverables/package.json index e07bd902a4..e43f6745b5 100644 --- a/packages/client/ui-deliverables/package.json +++ b/packages/client/ui-deliverables/package.json @@ -68,7 +68,8 @@ "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-attachment-local": "workspace:^", "@deepseek-ai/dsh-client-ui-tool": "workspace:^", - "@deepseek-ai/dsh-tool-present": "workspace:^" + "@deepseek-ai/dsh-tool-present": "workspace:^", + "@deepseek-ai/dsh-api-session-controller": "workspace:^" }, "files": [ "lib/index.js", diff --git a/packages/client/ui-deliverables/src/client/Deliverables.module.css b/packages/client/ui-deliverables/src/client/Deliverables.module.css index e83fc1eb34..97befd2946 100644 --- a/packages/client/ui-deliverables/src/client/Deliverables.module.css +++ b/packages/client/ui-deliverables/src/client/Deliverables.module.css @@ -2,7 +2,7 @@ .root { display: flex; flex-direction: column; gap: 8px; min-width: 0; margin-top: 12px; } .label { font-size: 12px; color: var(--dsw-alias-label-secondary); } .presented { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); gap: 8px; } -.file { display: flex; align-items: center; gap: 12px; min-width: 0; padding: 14px; border: 0.5px solid var(--dsw-alias-border-l2); border-radius: 12px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); text-decoration: none; } +.file { display: flex; align-items: center; gap: 12px; min-width: 0; padding: 14px; border: 0.5px solid var(--dsw-alias-border-l2); border-radius: 12px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); text-decoration: none; font: inherit; text-align: left; cursor: pointer; } .file:hover { background: var(--dsw-alias-bg-layer-2); border-color: var(--dsw-alias-border-l3); } .file:focus-visible { outline: 2px solid var(--dsw-alias-link); outline-offset: 2px; } .fileIcon { flex: 0 0 auto; width: 24px; height: 24px; } @@ -10,4 +10,5 @@ .fileName { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; font-weight: 500; } .metadata { color: var(--dsw-alias-label-tertiary); font-size: 11px; } .description { color: var(--dsw-alias-label-secondary); font-size: 12px; overflow-wrap: anywhere; } -.download { display: flex; flex-direction: column; align-items: center; gap: 4px; flex: 0 0 auto; color: var(--dsw-alias-link); font-size: 11px; } +.open { display: flex; flex-direction: column; align-items: center; gap: 4px; flex: 0 0 auto; color: var(--dsw-alias-link); font-size: 11px; } +.file:disabled { cursor: wait; opacity: 0.65; } diff --git a/packages/client/ui-deliverables/src/client/Deliverables.tsx b/packages/client/ui-deliverables/src/client/Deliverables.tsx index cfab1925bd..8f2dab5156 100644 --- a/packages/client/ui-deliverables/src/client/Deliverables.tsx +++ b/packages/client/ui-deliverables/src/client/Deliverables.tsx @@ -1,7 +1,9 @@ /** Existing changed-file chips and explicitly delivered snapshots for a closing turn. */ import type { TurnTailOwnerProps } from '@deepseek-ai/dsh-client-ui-chat/client' -import { LinkIcon, classifyLinkPath, fileSizeText, IconDownloadOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' -import type { PropsLocale, SessionStandardProps } from '@deepseek-ai/dsh-client-ui-slots' +import { LinkIcon, classifyLinkPath, fileSizeText, IconRightUpOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' +import type { InjectFace, PropsLocale, SessionStandardProps } from '@deepseek-ai/dsh-client-ui-slots' +import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store' +import type { PresentedOpenController } from './present-open.ts' import { ProducedFiles } from './ProducedFiles.tsx' import { basename, presentedForClosing, selectProducedFiles, type PresentedPath } from './turn-deliverables.ts' import type { NS } from './locales.ts' @@ -10,6 +12,12 @@ import css from './Deliverables.module.css' interface DeliverablesMatch { produced: readonly string[]; presented: readonly PresentedPath[] } +/** Native-open callbacks and shared gesture status supplied by the plugin. */ +export interface DeliverablesInjected { + hooks: { presentedOpen: ObservableSnapshot> } + openPresented: PresentedOpenController['open'] +} + /** * Claim turns containing modified paths or presented snapshots. * @param owner - closing turn. @@ -22,29 +30,34 @@ export function selectDeliverables(owner: TurnTailOwnerProps): DeliverablesMatch } /** - * Render workspace file actions and immutable snapshot downloads. + * Render workspace file actions and default-application buttons for saved deliveries. * @param props - matched files, workspace opener, and localized copy. * @returns the closing turn's file rows. */ -export function Deliverables({ matched, openFile, t, sessionId }: Pick & { +export function Deliverables({ matched, openFile, t, sessionId, openPresented, usePresentedOpen }: Pick & { matched: DeliverablesMatch -} & PropsLocale & Pick) { +} & PropsLocale & Pick & InjectFace) { + const states = usePresentedOpen(value => value) return <> {matched.produced.length > 0 && } {matched.presented.length > 0 &&
    {t('presented.label')}
    - {matched.presented.map(file => - - - {basename(file.path)} - {basename(file.path).match(/\.([^.]+)$/)?.[1]?.toUpperCase() ?? t('presented.file')} · {fileSizeText(file.bytes)} - {file.description && {file.description}} - - {t('presented.action')} - )} + {matched.presented.map((file) => { + const phase = states[presentedFileUrl(sessionId, file.seq, file.index, 'open')] + return })}
    } diff --git a/packages/client/ui-deliverables/src/client/index.ts b/packages/client/ui-deliverables/src/client/index.ts index db6355664f..09156448b4 100644 --- a/packages/client/ui-deliverables/src/client/index.ts +++ b/packages/client/ui-deliverables/src/client/index.ts @@ -13,9 +13,9 @@ import type { ChatFileMentions } from '@deepseek-ai/dsh-client-ui-chat/client' import type {} from '@deepseek-ai/dsh-client-locale/client' import type {} from '@deepseek-ai/dsh-client-ui-conversation/client' import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' -import { presentedFileUrl } from '../presented.ts' +import { PresentedOpenController } from './present-open.ts' import { PresentRow } from './PresentRow.tsx' -import { Deliverables, selectDeliverables } from './Deliverables.tsx' +import { Deliverables, selectDeliverables, type DeliverablesInjected } from './Deliverables.tsx' import { en, NS, zh, type DeliverablesKey } from './locales.ts' import { deliverablesDefinition, presentedForClosing, producedFileMentions, selectProducedFiles, @@ -39,6 +39,8 @@ export const inject = ['slots', 'locale', 'uiConversation', 'remote', 'remote.se * @param ctx - client root context. */ export function apply(ctx: ClientContext): void { + const opener = new PresentedOpenController() + ctx.effect(() => () => opener.dispose()) ctx.uiConversation.events.register(deliverablesDefinition) ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-deliverables: dictionaries') ctx.slots.inject( @@ -47,6 +49,10 @@ export function apply(ctx: ClientContext): void { name: 'conversation.chat.turnTail', select: selectDeliverables, locale: NS, + inject: (): DeliverablesInjected => ({ + hooks: { presentedOpen: opener.state }, + openPresented: (sessionId, seq, index) => opener.open(sessionId, seq, index), + }), }, Deliverables), ) ctx.slots.inject('tool.call.toolview', () => ctx.slots.register( @@ -66,13 +72,8 @@ export function apply(ctx: ClientContext): void { return producedFileMentions([...new Set([...paths ?? [], ...deliveries.keys()])], (path) => { const file = deliveries.get(path) if (file === undefined) owner.openFile(path) - else { - const link = document.createElement('a') - link.href = presentedFileUrl(sessionId, file.seq, file.index) - link.download = file.name - link.click() - } - }, path => t(deliveries.has(path) ? 'presented.download' : 'produced.open', { name: path })) + else void opener.open(sessionId, file.seq, file.index) + }, path => t(deliveries.has(path) ? 'presented.open' : 'produced.open', { name: path })) }, } ctx.provide('chatFileMentions', mentions) diff --git a/packages/client/ui-deliverables/src/client/locales.ts b/packages/client/ui-deliverables/src/client/locales.ts index af3cc1cfae..d9b468e15c 100644 --- a/packages/client/ui-deliverables/src/client/locales.ts +++ b/packages/client/ui-deliverables/src/client/locales.ts @@ -6,7 +6,10 @@ export const NS = 'deliverables' /** Simplified Chinese dictionary (the key-set source of truth). */ export const zh = { 'presented.label': '交付文件', - 'presented.action': '下载', + 'presented.action': '打开', + 'presented.opening': '正在打开…', + 'presented.opened': '已在默认程序中打开', + 'presented.error': '打开失败,点击重试', 'presented.file': '文件', 'row.title': '交付文件', 'row.running': '正在交付', @@ -14,7 +17,7 @@ export const zh = { 'row.error': '交付失败', 'row.stopped': '已中断', 'row.inspect': '查看调用', - 'presented.download': '下载 {name}', + 'presented.open': '在默认程序中打开 {name}', 'produced.label': '产物', 'produced.moreOne': '+ 1 个文件', 'produced.more': '+ {count} 个文件', @@ -24,7 +27,10 @@ export const zh = { /** English dictionary (same key set). */ export const en: Record = { 'presented.label': 'Deliverables', - 'presented.action': 'Download', + 'presented.action': 'Open', + 'presented.opening': 'Opening…', + 'presented.opened': 'Opened in default app', + 'presented.error': 'Could not open. Click to retry.', 'presented.file': 'File', 'row.title': 'Present files', 'row.running': 'Delivering', @@ -32,7 +38,7 @@ export const en: Record = { 'row.error': 'Delivery failed', 'row.stopped': 'Interrupted', 'row.inspect': 'Inspect call', - 'presented.download': 'Download {name}', + 'presented.open': 'Open {name} in default app', 'produced.label': 'Produced', 'produced.moreOne': '+ 1 file', 'produced.more': '+ {count} files', diff --git a/packages/client/ui-deliverables/src/client/present-open.ts b/packages/client/ui-deliverables/src/client/present-open.ts new file mode 100644 index 0000000000..350ac7d431 --- /dev/null +++ b/packages/client/ui-deliverables/src/client/present-open.ts @@ -0,0 +1,54 @@ +/** Shared native-open status for delivery cards and closing-message file mentions. */ +import { createSnapshotStore } from '@deepseek-ai/dsh-client-store' +import type { SessionId } from '@deepseek-ai/dsh-session/types' +import { presentedFileUrl } from '../presented.ts' + +/** State of the latest explicit open gesture for one saved file. */ +export type PresentedOpenPhase = 'opening' | 'opened' | 'error' + +/** One browser plugin's file-open requests, cancelled when that plugin is disposed. */ +export class PresentedOpenController { + /** File action URLs key the state across Sessions, turns, and both clickable surfaces. */ + readonly state = createSnapshotStore>({}) + private readonly lifetime = new AbortController() + private readonly pending = new Set>() + + /** + * Open a snapshot once while a request for the same coordinates is pending. + * Failures remain visible on the card and a later gesture retries them. + * @param sessionId - viewed Session, including a fork's own identity. + * @param seq - durable delivery event sequence. + * @param index - original file index within that event. + * @returns after the Host acknowledges opening or the error state is published. + */ + async open(sessionId: SessionId, seq: number, index: number): Promise { + const url = presentedFileUrl(sessionId, seq, index, 'open') + if (this.lifetime.signal.aborted || this.state.getSnapshot()[url] === 'opening') return + this.state.update((state) => { state[url] = 'opening' }) + const task = this.request(url) + this.pending.add(task) + try { + await task + } finally { + this.pending.delete(task) + } + } + + /** Cancel outstanding requests and wait until no request can publish state. */ + async dispose(): Promise { + this.lifetime.abort() + await Promise.all(this.pending) + } + + private async request(url: string): Promise { + let phase: PresentedOpenPhase = 'opened' + try { + const response = await fetch(url, { method: 'POST', signal: this.lifetime.signal }) + if (!response.ok) phase = 'error' + } catch { + // Transport failures share the retryable card state with Host open failures. + phase = 'error' + } + if (!this.lifetime.signal.aborted) this.state.update((state) => { state[url] = phase }) + } +} diff --git a/packages/client/ui-deliverables/src/index.ts b/packages/client/ui-deliverables/src/index.ts index 963d0c1700..de87c6e574 100644 --- a/packages/client/ui-deliverables/src/index.ts +++ b/packages/client/ui-deliverables/src/index.ts @@ -1,7 +1,7 @@ /** * Deliverables plugin, node half. Registers the response-format guidance that * lets the browser half recognize final-response file references and serves - * authenticated snapshot downloads. The browser + * authenticated snapshot downloads and native opens. The browser * half ships via exports["./client"], discovered through the package.json * dsh.client declaration. */ @@ -10,8 +10,8 @@ import type { Context } from '@deepseek-ai/cordis' import type {} from '@deepseek-ai/dsh-system-prompt' import { registerPresentDownload } from './present-download.ts' -/** Services required for file-reference guidance and authenticated snapshot downloads. */ -export const inject = ['systemPrompt', 'connection', 'sessionQuery', 'attachments'] +/** Services required for file-reference guidance and authenticated snapshot downloads and native opens. */ +export const inject = ['systemPrompt', 'connection', 'sessionQuery', 'attachments', 'sessionController'] /** Stable final-response guidance owned by the matching renderer. */ const FILE_REFERENCE_PROMPT = 'When you successfully create or modify files, mention the primary outputs in your final response. ' diff --git a/packages/client/ui-deliverables/src/present-download.ts b/packages/client/ui-deliverables/src/present-download.ts index 4d5d3ea75b..6bcbd42290 100644 --- a/packages/client/ui-deliverables/src/present-download.ts +++ b/packages/client/ui-deliverables/src/present-download.ts @@ -1,26 +1,34 @@ -/** Authorize downloads against the Session log, then stream only the saved Presented file bytes. */ +/** Authorize delivery actions against the Session log, using only saved attachment bytes. */ import { Readable } from 'node:stream' import type { Context } from '@deepseek-ai/cordis' import type {} from '@deepseek-ai/dsh-attachment' import type {} from '@deepseek-ai/dsh-client-connection' import type { SessionId, SessionSeq } from '@deepseek-ai/dsh-session' -import { isPresentedData, isPresentedFile, PRESENT_DOWNLOAD_PATH } from './presented.ts' +import { isPresentedData, isPresentedFile, PRESENT_DOWNLOAD_PATH, PRESENT_OPEN_PATH } from './presented.ts' +import { createPresentedOpener } from './present-open.ts' import type {} from '@deepseek-ai/dsh-session-query' /** - * Register a streaming download inside Connection's existing authentication fence. + * Register snapshot downloads and native opening inside Connection's authentication fence. * @param ctx - Host services owning Session reads, attachments, and HTTP routing. */ export function registerPresentDownload(ctx: Context): void { + const open = createPresentedOpener(ctx) ctx.connection.fetch.register({ path: PRESENT_DOWNLOAD_PATH, methods: ['GET'], requestBody: 'buffered', - fetch: request => download(ctx, request), + fetch: request => handleDelivery(ctx, request), + }) + ctx.connection.fetch.register({ + path: PRESENT_OPEN_PATH, + methods: ['POST'], + requestBody: 'buffered', + fetch: request => handleDelivery(ctx, request, open), }) } -async function download(ctx: Context, request: Request): Promise { +async function handleDelivery(ctx: Context, request: Request, open?: ReturnType): Promise { const query = new URL(request.url).searchParams const id = query.get('sessionId') const seq = query.get('seq') @@ -35,6 +43,10 @@ async function download(ctx: Context, request: Request): Promise { }, request.signal) const artifact = target.type === 'deliverables/presented' && isPresentedData(target.data) ? target.data.files[Number(index)] : undefined if (!isPresentedFile(artifact)) return new Response('Presented file not found in this Session result.', { status: 404 }) + if (open !== undefined) { + await open(artifact, request.signal) + return new Response(null, { status: 204, headers: { 'cache-control': 'no-store' } }) + } const filename = encodeURIComponent(artifact.name.toWellFormed()) .replace(/['()*]/g, character => `%${character.charCodeAt(0).toString(16).toUpperCase()}`) const iterator = ctx.attachments.readFileStream(artifact, request.signal)[Symbol.asyncIterator]() diff --git a/packages/client/ui-deliverables/src/present-open.ts b/packages/client/ui-deliverables/src/present-open.ts new file mode 100644 index 0000000000..e25acc39ed --- /dev/null +++ b/packages/client/ui-deliverables/src/present-open.ts @@ -0,0 +1,53 @@ +/** Private editable copies keep native applications away from immutable attachment objects. */ +import { createWriteStream } from 'node:fs' +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { pipeline } from 'node:stream/promises' +import type { Context } from '@deepseek-ai/cordis' +import type { FileAttachmentRef } from '@deepseek-ai/dsh-attachment' +import type {} from '@deepseek-ai/dsh-api-session-controller' +import { basename } from './presented.ts' + +/** + * Own native-open work and its temporary copies for one plugin lifetime. + * Successful copies remain until disposal because desktop applications may read lazily. + * @param ctx - attachment provider and plugin lifetime. + * @returns an opener that verifies the entire snapshot before launching its default application. + */ +export function createPresentedOpener(ctx: Context): (ref: FileAttachmentRef, signal: AbortSignal) => Promise { + const lifetime = new AbortController() + const directories = new Set() + const pending = new Set>() + ctx.effect(() => async () => { + lifetime.abort() + await Promise.allSettled(pending) + await Promise.all([...directories].map(directory => rm(directory, { recursive: true, force: true }))) + }) + + async function open(ref: FileAttachmentRef, signal: AbortSignal): Promise { + signal.throwIfAborted() + if (basename(ref.name) !== ref.name || ref.name === '.' || ref.name === '..' || ref.name.includes('\0')) { + throw new Error('Presented file name must be a leaf name.') + } + const directory = await mkdtemp(join(tmpdir(), 'dsh-present-')) + directories.add(directory) + try { + const path = join(directory, ref.name) + await pipeline(ctx.attachments.readFileStream(ref, signal), createWriteStream(path, { flags: 'wx', mode: 0o600 }), { signal }) + signal.throwIfAborted() + await ctx.sessionController.openWorkspacePath({ path }, signal) + } catch (error) { + await rm(directory, { recursive: true, force: true }) + directories.delete(directory) + throw error + } + } + + return (ref, signal) => { + const task = open(ref, AbortSignal.any([signal, lifetime.signal])) + pending.add(task) + void task.then(() => { pending.delete(task) }, () => { pending.delete(task) }) + return task + } +} diff --git a/packages/client/ui-deliverables/src/presented.ts b/packages/client/ui-deliverables/src/presented.ts index 0e58ff0c49..87eb0c97b4 100644 --- a/packages/client/ui-deliverables/src/presented.ts +++ b/packages/client/ui-deliverables/src/presented.ts @@ -6,6 +6,9 @@ import type { ToolCallId } from '@deepseek-ai/dsh-llm/brand' /** Authenticated route for saved file bytes. */ export const PRESENT_DOWNLOAD_PATH = '/api/present.download' +/** Authenticated POST route for opening a saved file on the Host desktop. */ +export const PRESENT_OPEN_PATH = '/api/present.open' + /** * Validate a saved delivery read from a Session log. * @param value - decoded durable data. @@ -22,14 +25,15 @@ export function isPresentedFile(value: unknown): value is PresentedFile { } /** - * Build an authenticated download coordinate for a saved delivery. + * Build authenticated coordinates for a saved delivery. * @param sessionId - owning Session. * @param seq - deliverables/presented event sequence. * @param index - original index in the event's files array. - * @returns same-origin download URL. + * @param action - retrieve the bytes or open a copy on the Host desktop. + * @returns same-origin file action URL. */ -export function presentedFileUrl(sessionId: SessionId, seq: number, index: number): string { - return `${PRESENT_DOWNLOAD_PATH}?${new URLSearchParams({ sessionId, seq: String(seq), index: String(index) })}` +export function presentedFileUrl(sessionId: SessionId, seq: number, index: number, action: 'download' | 'open' = 'download'): string { + return `${action === 'open' ? PRESENT_OPEN_PATH : PRESENT_DOWNLOAD_PATH}?${new URLSearchParams({ sessionId, seq: String(seq), index: String(index) })}` } /** diff --git a/packages/client/ui-deliverables/tests/present-download.host.spec.ts b/packages/client/ui-deliverables/tests/present-download.host.spec.ts index 45f039cf07..1d07213108 100644 --- a/packages/client/ui-deliverables/tests/present-download.host.spec.ts +++ b/packages/client/ui-deliverables/tests/present-download.host.spec.ts @@ -1,9 +1,9 @@ /** Saved Presented file downloads over the real Connection route and local attachment store. */ -import { mkdtemp, rm } from 'node:fs/promises' +import { mkdtemp, rm, readFile, writeFile, access } from 'node:fs/promises' import { once } from 'node:events' import { createServer } from 'node:http' import { tmpdir } from 'node:os' -import { join } from 'node:path' +import { join, basename, dirname } from 'node:path' import { Context } from '@deepseek-ai/cordis' import { AttachmentError, AttachmentId } from '@deepseek-ai/dsh-attachment' import LocalAttachmentStore from '@deepseek-ai/dsh-attachment-local' @@ -17,7 +17,7 @@ import { SessionQueryError } from '@deepseek-ai/dsh-session-query' import type { SessionEventReadRequest } from '@deepseek-ai/dsh-session-query' import { afterEach, describe, expect, it, vi } from 'vitest' import { registerPresentDownload } from '../src/present-download.ts' -import { presentedFileUrl, PRESENT_DOWNLOAD_PATH } from '../src/presented.ts' +import { presentedFileUrl, PRESENT_DOWNLOAD_PATH, PRESENT_OPEN_PATH } from '../src/presented.ts' const cleanups: Array<() => Promise> = [] afterEach(async () => { @@ -40,12 +40,16 @@ async function fixture() { return { target: { type: 'deliverables/presented', data: { turn: 1, callId: 'present-call', files: [artifact] } } as SessionEvent } }) ctx.provide('sessionQuery', { readEvent } as never) + const opener = vi.fn(async (_request: { path: string }, _signal: AbortSignal) => ({ opened: true as const })) + ctx.provide('sessionController', { openWorkspacePath: opener } as never) const connection = new HostConnectionService(ctx, [], {} as BrowserAuth) - const fiber = ctx.plugin({ inject: ['connection', 'sessionQuery', 'attachments'], apply: registerPresentDownload }) + const fiber = ctx.plugin({ inject: ['connection', 'sessionQuery', 'attachments', 'sessionController'], apply: registerPresentDownload }) await fiber const fetch = (query = '?sessionId=owner&seq=7&index=0', signal?: AbortSignal) => connection .createSharedFetchHandler('/api').fetch(new Request(`http://localhost${PRESENT_DOWNLOAD_PATH}${query}`, { signal: signal ?? null })) - return { ctx, fiber, artifact, readEvent, fetch, handler: connection.createSharedFetchHandler('/api') } + const open = (query = '?sessionId=owner&seq=7&index=0', signal?: AbortSignal) => connection + .createSharedFetchHandler('/api').fetch(new Request(`http://localhost${PRESENT_OPEN_PATH}${query}`, { method: 'POST', signal: signal ?? null })) + return { ctx, fiber, artifact, readEvent, fetch, open, opener, handler: connection.createSharedFetchHandler('/api') } } describe('Presented file download route', () => { @@ -196,3 +200,82 @@ describe('Presented file download route', () => { await expect(response.arrayBuffer()).rejects.toThrow('corrupt') }) }) + + +describe('Presented file native open route', () => { + it('opens separate verified copies with the original filename and cleans them at disposal', async () => { + const { open, fetch, fiber, artifact, handler, opener } = await fixture() + expect((await handler.fetch(new Request(`http://localhost${PRESENT_OPEN_PATH}?sessionId=owner&seq=7&index=0`))).status).toBe(404) + for (let i = 0; i < 2; i++) { + const response = await open() + expect(response.status).toBe(204) + expect(response.headers.get('content-disposition')).toBeNull() + const path = opener.mock.calls[i]![0].path + expect(basename(path)).toBe(artifact.name) + expect(await readFile(path)).toEqual(Buffer.from([80, 75, 0, 255])) + await writeFile(path, 'edited by desktop app') + } + expect(opener.mock.calls[0]![0].path).not.toBe(opener.mock.calls[1]![0].path) + expect(new Uint8Array(await (await fetch()).arrayBuffer())).toEqual(Uint8Array.of(80, 75, 0, 255)) + await fiber.dispose() + for (const [{ path }] of opener.mock.calls) await expect(access(dirname(path))).rejects.toMatchObject({ code: 'ENOENT' }) + expect((await open()).status).toBe(404) + }) + + it('refuses invalid coordinates, unrelated logs, missing snapshots, and corruption before native launch', async () => { + const { open, artifact, readEvent, opener } = await fixture() + expect((await open('?sessionId=owner&seq=-1&index=0')).status).toBe(400) + expect(readEvent).not.toHaveBeenCalled() + expect((await open('?sessionId=other&seq=7&index=0')).status).toBe(404) + expect((await open('?sessionId=owner&seq=7&index=1')).status).toBe(404) + readEvent.mockResolvedValueOnce({ target: { type: 'turn/start' } as SessionEvent }) + expect((await open()).status).toBe(404) + artifact.bytes = 0 + expect((await open()).status).toBe(500) + artifact.attachmentId = AttachmentId(`sha256:${'0'.repeat(64)}`) + expect((await open()).status).toBe(404) + expect(opener).not.toHaveBeenCalled() + }) + + it.each(['../escape.txt', 'folder\\escape.txt', '.', '..', 'bad\0name'])('rejects unsafe durable filenames: %j', async (name) => { + const { open, artifact, opener } = await fixture() + artifact.name = name + expect((await open()).status).toBe(500) + expect(opener).not.toHaveBeenCalled() + }) + + it('reports launcher failure, removes its copy, and allows retry', async () => { + const { open, opener } = await fixture() + opener.mockRejectedValueOnce(new Error('/private/host/path')) + const response = await open() + expect(response.status).toBe(500) + expect(await response.text()).not.toContain('/private/host/path') + await expect(access(dirname(opener.mock.calls[0]![0].path))).rejects.toMatchObject({ code: 'ENOENT' }) + expect((await open()).status).toBe(204) + }) + + it('disposal aborts a pending native launch and waits for it before deleting the copy', async () => { + const entered = Promise.withResolvers() + const aborted = Promise.withResolvers() + const release = Promise.withResolvers() + const { open, fiber, opener } = await fixture() + opener.mockImplementation(async (_request, signal) => { + signal.addEventListener('abort', () => { aborted.resolve(undefined) }, { once: true }) + entered.resolve(undefined) + await release.promise + signal.throwIfAborted() + return { opened: true } + }) + const request = open() + await entered.promise + const path = opener.mock.calls[0]![0].path + let disposed = false + const disposal = fiber.dispose().then(() => { disposed = true }) + await aborted.promise + expect(disposed).toBe(false) + await access(path) + release.resolve(undefined) + await Promise.all([request, disposal]) + await expect(access(dirname(path))).rejects.toMatchObject({ code: 'ENOENT' }) + }) +}) diff --git a/packages/client/ui-deliverables/tests/present-open.client.spec.ts b/packages/client/ui-deliverables/tests/present-open.client.spec.ts new file mode 100644 index 0000000000..b201895445 --- /dev/null +++ b/packages/client/ui-deliverables/tests/present-open.client.spec.ts @@ -0,0 +1,63 @@ +/** Delivery gestures share pending state, report failures, and cancel with the plugin. */ +import { afterEach, expect, it, vi } from 'vitest' +import { SessionId } from '@deepseek-ai/dsh-session/types' +import { PresentedOpenController } from '../src/client/present-open.ts' + +afterEach(() => { vi.unstubAllGlobals() }) + +const id = SessionId('fork') +const url = '/api/present.open?sessionId=fork&seq=2&index=1' + +it('coalesces concurrent card and mention gestures, then allows another open', async () => { + const reply = Promise.withResolvers() + const fetcher = vi.fn().mockReturnValue(reply.promise) + vi.stubGlobal('fetch', fetcher) + const controller = new PresentedOpenController() + const first = controller.open(id, 2, 1) + await controller.open(id, 2, 1) + expect(fetcher).toHaveBeenCalledTimes(1) + expect(fetcher).toHaveBeenCalledWith(url, { method: 'POST', signal: expect.any(AbortSignal) as AbortSignal }) + expect(controller.state.getSnapshot()[url]).toBe('opening') + reply.resolve(new Response(null, { status: 204 })) + await first + expect(controller.state.getSnapshot()[url]).toBe('opened') + await controller.open(id, 2, 1) + expect(fetcher).toHaveBeenCalledTimes(2) + await controller.dispose() +}) + +it.each(['http', 'network'])('publishes retryable %s failures', async (failure) => { + const fetcher = vi.fn() + if (failure === 'http') fetcher.mockResolvedValueOnce(new Response(null, { status: 500 })) + else fetcher.mockRejectedValueOnce(new Error('offline')) + fetcher.mockResolvedValue(new Response(null, { status: 204 })) + vi.stubGlobal('fetch', fetcher) + const controller = new PresentedOpenController() + await controller.open(id, 2, 1) + expect(controller.state.getSnapshot()[url]).toBe('error') + await controller.open(id, 2, 1) + expect(controller.state.getSnapshot()[url]).toBe('opened') + await controller.dispose() +}) + +it('awaits cancellation and prevents late state publication or new requests after disposal', async () => { + const aborted = Promise.withResolvers() + const release = Promise.withResolvers() + const fetcher = vi.fn((_url: string, { signal }: RequestInit) => { + signal!.addEventListener('abort', () => { aborted.resolve(undefined) }, { once: true }) + return release.promise + }) + vi.stubGlobal('fetch', fetcher) + const controller = new PresentedOpenController() + const open = controller.open(id, 2, 1) + const state = controller.state.getSnapshot() + let disposed = false + const disposal = controller.dispose().then(() => { disposed = true }) + await aborted.promise + expect(disposed).toBe(false) + release.resolve(new Response(null, { status: 204 })) + await Promise.all([open, disposal]) + expect(controller.state.getSnapshot()).toBe(state) + await controller.open(id, 2, 1) + expect(fetcher).toHaveBeenCalledOnce() +}) diff --git a/packages/client/ui-deliverables/tests/produced-files.client.spec.tsx b/packages/client/ui-deliverables/tests/produced-files.client.spec.tsx index 058db67a8b..1bfe183592 100644 --- a/packages/client/ui-deliverables/tests/produced-files.client.spec.tsx +++ b/packages/client/ui-deliverables/tests/produced-files.client.spec.tsx @@ -21,7 +21,8 @@ import { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client' import { apply as applyLocale, inject as localeInject } from '@deepseek-ai/dsh-client-locale/client' import type { ChatFileMentions, TurnTailOwnerProps } from '@deepseek-ai/dsh-client-ui-chat/client' import { makeTranslate, stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime' -import { Deliverables, selectDeliverables } from '../src/client/Deliverables.tsx' +import { Deliverables, selectDeliverables, type DeliverablesInjected } from '../src/client/Deliverables.tsx' +import { PresentedOpenController } from '../src/client/present-open.ts' import { ProducedFiles } from '../src/client/ProducedFiles.tsx' import { basename, deliverablesDefinition, presentedForClosing, producedFileMentions, producedForClosing, selectProducedFiles, @@ -32,6 +33,14 @@ import { en, zh } from '../src/client/locales.ts' import { SessionId } from '@deepseek-ai/dsh-session/types' import type { SessionEvent } from '@deepseek-ai/dsh-session/types' +function openProps(controller = new PresentedOpenController()) { + return { + openPresented: vi.fn((...args: Parameters) => controller.open(...args)), + usePresentedOpen: (select: (state: ReturnType) => T): T => + select(controller.state.getSnapshot()), + } +} + afterEach(() => { cleanup() vi.restoreAllMocks() @@ -516,9 +525,7 @@ describe('plugin registration', () => { const [entry] = ctx.slots.entries('conversation.chat.turnTail') expect(entry).toBeDefined() expect(ctx.slots.entries('tool.call.toolview')).toHaveLength(1) - // The row needs no injected Host capability: it hands a path to its owner - // and nothing in it reaches the local machine. - expect(entry?.inject).toBeUndefined() + expect(entry?.inject).toBeDefined() // The prose face is live while the plugin is: a produced turn yields a // resolver whose matches open through the owner-supplied opener. @@ -532,13 +539,14 @@ describe('plugin registration', () => { const mentions = service?.forClosing(owner, SessionId('viewed-session')) mentions?.resolve('report.html')?.open() expect(opened).toEqual(['site/report.html']) - const click = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(function (this: HTMLAnchorElement) { - expect(this.getAttribute('href')).toBe('/api/present.download?sessionId=child-session&seq=2&index=0') - expect(this.download).toBe('report.docx') - }) + const fetcher = vi.fn().mockResolvedValue(new Response(null, { status: 204 })) + vi.stubGlobal('fetch', fetcher) const delivered = tailOwner({ produced: [], presented: [{ path: 'report.docx', name: 'report.docx', bytes: 4, attachmentId: 'saved' as never, seq: 2, index: 0 }] }, 3) service?.forClosing(delivered, SessionId('child-session'))?.resolve('report.docx')?.open() - expect(click).toHaveBeenCalledOnce() + expect(fetcher).toHaveBeenCalledWith('/api/present.open?sessionId=child-session&seq=2&index=0', { method: 'POST', signal: expect.any(AbortSignal) as AbortSignal }) + const face = entry!.inject!(SessionId('child-session') as never) as unknown as DeliverablesInjected + await face.openPresented(SessionId('child-session'), 2, 0) + expect(face.hooks.presentedOpen.getSnapshot()['/api/present.open?sessionId=child-session&seq=2&index=0']).toBe('opened') // A turn that produced nothing yields no vocabulary at all. expect(service?.forClosing(tailOwner(undefined, 2), SessionId('viewed-session'))).toBeUndefined() @@ -569,16 +577,20 @@ describe('presented files', () => { expect(selectDeliverables(tailOwner(deliverablesOf(value, 2), 9))).toBeNull() }) - it('uses the viewed fork Session in every download and retains all delivered files', () => { + it('uses the viewed fork Session in every open action and retains all delivered files', () => { const value = assembler([ at(1, 'turn/start', { turn: 1 }), at(2, 'deliverables/presented', { turn: 1, callId: 'nested', files: Array.from({ length: 8 }, (_, i) => file(`report-${i}.docx`)) }), ]) const owner = tailOwner(deliverablesOf(value), 3) const matched = selectDeliverables(owner)! - const view = render() - expect(view.getAllByRole('link')).toHaveLength(8) - expect(view.getAllByRole('link')[0]?.getAttribute('href')).toBe('/api/present.download?sessionId=child-session&seq=2&index=0') + const props = openProps() + props.openPresented.mockResolvedValue(undefined) + const view = render() + expect(view.getAllByRole('button')).toHaveLength(8) + expect(view.queryByRole('link')).toBeNull() + fireEvent.click(view.getByRole('button', { name: 'Open report-0.docx in default app' })) + expect(props.openPresented).toHaveBeenCalledWith('child-session', 2, 0) expect(view.queryByText('Produced')).toBeNull() }) }) @@ -596,18 +608,30 @@ it.each([null, [], 'invalid', {}, { turn: '1', callId: 'bad', files: [] }, ]) const owner = tailOwner(deliverablesOf(value), 5) const matched = selectDeliverables(owner)! - const view = render() + const view = render() expect(view.getByText('Produced')).toBeTruthy() expect(view.queryByText('Deliverables')).toBeNull() }) it('shows file metadata and descriptions without hiding extensionless deliveries', () => { - const view = render( {}} sessionId={SessionId('session')} t={makeTranslate(en)} />) expect(view.getByText('Quarterly summary')).toBeTruthy() expect(view.getByText('TXT · 4.0KB')).toBeTruthy() expect(view.getByText('File · 0B')).toBeTruthy() - expect(view.getByRole('link', { name: 'Download out/report.txt' }).getAttribute('title')).toBe('out/report.txt') + expect(view.getByRole('button', { name: 'Open out/report.txt in default app' }).getAttribute('title')).toBe('Open out/report.txt in default app') +}) + + +it.each(['opening', 'opened', 'error'] as const)('shows the %s state and permits retries after failure', (phase) => { + const controller = new PresentedOpenController() + controller.state.set({ '/api/present.open?sessionId=session&seq=2&index=0': phase }) + const props = openProps(controller) + const view = render( {}} sessionId={SessionId('session')} t={makeTranslate(en)} />) + expect(view.getByRole('status').textContent).toBe(en[`presented.${phase}`]) + expect((view.getByRole('button') as HTMLButtonElement).disabled).toBe(phase === 'opening') }) diff --git a/packages/client/ui-deliverables/tests/prompt.host.spec.ts b/packages/client/ui-deliverables/tests/prompt.host.spec.ts index 042699d6d3..84e9d50fc7 100644 --- a/packages/client/ui-deliverables/tests/prompt.host.spec.ts +++ b/packages/client/ui-deliverables/tests/prompt.host.spec.ts @@ -19,6 +19,7 @@ describe('ui-deliverables node plugin', () => { ctx.provide('connection', { fetch: { register: () => () => {} } } as never) ctx.provide('sessionQuery', {} as never) ctx.provide('attachments', {} as never) + ctx.provide('sessionController', {} as never) const mounted = ctx.plugin({ apply, inject }) await mounted.await() diff --git a/packages/client/ui-deliverables/tsconfig.host.json b/packages/client/ui-deliverables/tsconfig.host.json index 39f2f8a2cc..991e8a4a82 100644 --- a/packages/client/ui-deliverables/tsconfig.host.json +++ b/packages/client/ui-deliverables/tsconfig.host.json @@ -8,7 +8,8 @@ "files": [ "src/index.ts", "src/present-download.ts", - "src/presented.ts" + "src/presented.ts", + "src/present-open.ts" ], "references": [ { @@ -34,6 +35,9 @@ }, { "path": "../../fs/tool-present" + }, + { + "path": "../../api/session-controller/tsconfig.host.json" } ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9cd52b76f6..6857fc6816 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2693,6 +2693,9 @@ importers: '@deepseek-ai/dsh-api-remotes': specifier: workspace:^ version: link:../../api/remotes + '@deepseek-ai/dsh-api-session-controller': + specifier: workspace:^ + version: link:../../api/session-controller '@deepseek-ai/dsh-attachment': specifier: workspace:^ version: link:../../attachment/attachment diff --git a/snapshots/web/present/ui.expected.md b/snapshots/web/present/ui.expected.md index 3004a21a11..d023374f68 100644 --- a/snapshots/web/present/ui.expected.md +++ b/snapshots/web/present/ui.expected.md @@ -56,25 +56,25 @@ - code: present - text: succeeded for - code: - - button "Download report.txt": report.txt + - button "Open report.txt in default app": report.txt - text: and - code: - - button "Download 说明.txt": 说明.txt + - button "Open 说明.txt in default app": 说明.txt - text: ", after which the program deliberately threw the string" - code: AFTER_PRESENT - text: — no retries, no extra files. - paragraph: PRESENT_DONE - text: Deliverables -- link "Download report.txt": - - /url: /api/present.download?sessionId=session-{{uuid}}&seq=19&index=0 +- button "Open report.txt in default app": - text: report.txt TXT · 17B delivered report + - status: Opened in default app - img - - text: Download -- link "Download 说明.txt": - - /url: /api/present.download?sessionId=session-{{uuid}}&seq=19&index=1 + - text: Open +- button "Open 说明.txt in default app": - text: 说明.txt TXT · 15B delivered note + - status: Opened in default app - img - - text: Download + - text: Open - button "Copy": - img - button "Good response": From 387e437af4552af908d5d0bbb55f34ea560f2e6d Mon Sep 17 00:00:00 2001 From: yudshj Date: Tue, 8 Sep 2026 18:37:53 +0800 Subject: [PATCH 50/58] fix(web): use the preview-compatible stream promises entry --- packages/client/ui-deliverables/src/present-open.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/client/ui-deliverables/src/present-open.ts b/packages/client/ui-deliverables/src/present-open.ts index e25acc39ed..120d771a82 100644 --- a/packages/client/ui-deliverables/src/present-open.ts +++ b/packages/client/ui-deliverables/src/present-open.ts @@ -3,7 +3,7 @@ import { createWriteStream } from 'node:fs' import { mkdtemp, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { pipeline } from 'node:stream/promises' +import { promises as streamPromises } from 'node:stream' import type { Context } from '@deepseek-ai/cordis' import type { FileAttachmentRef } from '@deepseek-ai/dsh-attachment' import type {} from '@deepseek-ai/dsh-api-session-controller' @@ -34,7 +34,11 @@ export function createPresentedOpener(ctx: Context): (ref: FileAttachmentRef, si directories.add(directory) try { const path = join(directory, ref.name) - await pipeline(ctx.attachments.readFileStream(ref, signal), createWriteStream(path, { flags: 'wx', mode: 0o600 }), { signal }) + await streamPromises.pipeline( + ctx.attachments.readFileStream(ref, signal), + createWriteStream(path, { flags: 'wx', mode: 0o600 }), + { signal }, + ) signal.throwIfAborted() await ctx.sessionController.openWorkspacePath({ path }, signal) } catch (error) { From c9038b4b31ce605cfcfd95fbac4e51d49deddfe6 Mon Sep 17 00:00:00 2001 From: yudshj Date: Tue, 8 Sep 2026 19:39:49 +0800 Subject: [PATCH 51/58] refactor(present): declare and open workspace source files --- ...09-08-web-explicit-file-delivery.i18n.yaml | 4 +- .../2026-09-08-web-explicit-file-delivery.md | 1 + ...026-09-08-web-explicit-file-delivery.zh.md | 1 + .agents/notes/archived/manifest.json | 3 + ...8-present-workspace-source-files.i18n.yaml | 6 + ...26-09-08-present-workspace-source-files.md | 37 +++ ...09-08-present-workspace-source-files.zh.md | 37 +++ apps/web/tests/present.e2e.ts | 49 ++- docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 10 +- docs/config-catalog.zh.md | 10 +- docs/persistence-catalog.i18n.yaml | 4 +- docs/persistence-catalog.md | 4 +- docs/persistence-catalog.zh.md | 4 +- docs/tool-catalog.i18n.yaml | 4 +- docs/tool-catalog.md | 6 +- docs/tool-catalog.zh.md | 6 +- .../client/ui-deliverables/README.i18n.yaml | 4 +- packages/client/ui-deliverables/README.md | 10 +- packages/client/ui-deliverables/README.zh.md | 12 +- packages/client/ui-deliverables/package.json | 2 - .../src/client/Deliverables.tsx | 12 +- .../src/client/present-open.ts | 4 +- .../src/client/turn-deliverables.ts | 11 +- packages/client/ui-deliverables/src/index.ts | 10 +- .../ui-deliverables/src/present-download.ts | 84 ------ .../ui-deliverables/src/present-open.ts | 100 ++++--- .../client/ui-deliverables/src/presented.ts | 27 +- .../tests/present-download.host.spec.ts | 281 ------------------ .../tests/present-open.host.spec.ts | 178 +++++++++++ .../tests/produced-files.client.spec.tsx | 24 +- .../ui-deliverables/tests/prompt.host.spec.ts | 1 - .../ui-deliverables/tsconfig.client.json | 3 - .../client/ui-deliverables/tsconfig.host.json | 4 - packages/fs/tool-present/README.i18n.yaml | 4 +- packages/fs/tool-present/README.md | 31 +- packages/fs/tool-present/README.zh.md | 31 +- packages/fs/tool-present/package.json | 5 +- packages/fs/tool-present/src/index.ts | 44 +-- packages/fs/tool-present/src/types.ts | 7 +- .../fs/tool-present/tests/built-errors.e2e.ts | 13 +- .../fs/tool-present/tests/present.spec.ts | 48 +-- packages/fs/tool-present/tsconfig.json | 3 - pnpm-lock.yaml | 12 - scripts/gen-tool-catalog.ts | 5 +- .../tool-schemas.expected.json | 2 +- .../tool-schemas.expected.json | 2 +- snapshots/web/present/session.v2.jsonl | 4 +- snapshots/web/present/ui.expected.md | 4 +- .../web/ptc-round/system-prompt.expected.md | 5 +- 50 files changed, 517 insertions(+), 660 deletions(-) rename .agents/notes/{implemented => archived}/feature/2026-09-08-web-explicit-file-delivery.i18n.yaml (68%) rename .agents/notes/{implemented => archived}/feature/2026-09-08-web-explicit-file-delivery.md (99%) rename .agents/notes/{implemented => archived}/feature/2026-09-08-web-explicit-file-delivery.zh.md (99%) create mode 100644 .agents/notes/implemented/feature/2026-09-08-present-workspace-source-files.i18n.yaml create mode 100644 .agents/notes/implemented/feature/2026-09-08-present-workspace-source-files.md create mode 100644 .agents/notes/implemented/feature/2026-09-08-present-workspace-source-files.zh.md delete mode 100644 packages/client/ui-deliverables/src/present-download.ts delete mode 100644 packages/client/ui-deliverables/tests/present-download.host.spec.ts create mode 100644 packages/client/ui-deliverables/tests/present-open.host.spec.ts diff --git a/.agents/notes/implemented/feature/2026-09-08-web-explicit-file-delivery.i18n.yaml b/.agents/notes/archived/feature/2026-09-08-web-explicit-file-delivery.i18n.yaml similarity index 68% rename from .agents/notes/implemented/feature/2026-09-08-web-explicit-file-delivery.i18n.yaml rename to .agents/notes/archived/feature/2026-09-08-web-explicit-file-delivery.i18n.yaml index 0925919414..71c2bd9db9 100644 --- a/.agents/notes/implemented/feature/2026-09-08-web-explicit-file-delivery.i18n.yaml +++ b/.agents/notes/archived/feature/2026-09-08-web-explicit-file-delivery.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-08-web-explicit-file-delivery.md -2026-09-08-web-explicit-file-delivery.md: 5ab211d8e8ea3c7f008cf39307f1ddfdd77acfee -2026-09-08-web-explicit-file-delivery.zh.md: 3077b23a78f0617a1a377bfc37de201169bbdc82 +2026-09-08-web-explicit-file-delivery.md: ff2ddeb59ded05b70006dce217df2966eab9d4f2 +2026-09-08-web-explicit-file-delivery.zh.md: 85b09ac82c83365b1c198168b78aa7ac84ef216f diff --git a/.agents/notes/implemented/feature/2026-09-08-web-explicit-file-delivery.md b/.agents/notes/archived/feature/2026-09-08-web-explicit-file-delivery.md similarity index 99% rename from .agents/notes/implemented/feature/2026-09-08-web-explicit-file-delivery.md rename to .agents/notes/archived/feature/2026-09-08-web-explicit-file-delivery.md index 5ab211d8e8..ff2ddeb59d 100644 --- a/.agents/notes/implemented/feature/2026-09-08-web-explicit-file-delivery.md +++ b/.agents/notes/archived/feature/2026-09-08-web-explicit-file-delivery.md @@ -1,6 +1,7 @@ # Agent Note: Web delivers explicit file snapshots Status: implemented +Archived: 2026-09-08 English | [中文](2026-09-08-web-explicit-file-delivery.zh.md) diff --git a/.agents/notes/implemented/feature/2026-09-08-web-explicit-file-delivery.zh.md b/.agents/notes/archived/feature/2026-09-08-web-explicit-file-delivery.zh.md similarity index 99% rename from .agents/notes/implemented/feature/2026-09-08-web-explicit-file-delivery.zh.md rename to .agents/notes/archived/feature/2026-09-08-web-explicit-file-delivery.zh.md index 3077b23a78..85b09ac82c 100644 --- a/.agents/notes/implemented/feature/2026-09-08-web-explicit-file-delivery.zh.md +++ b/.agents/notes/archived/feature/2026-09-08-web-explicit-file-delivery.zh.md @@ -1,6 +1,7 @@ # Agent Note: Web 显式交付文件快照 Status: implemented +Archived: 2026-09-08 [English](2026-09-08-web-explicit-file-delivery.md) | 中文 diff --git a/.agents/notes/archived/manifest.json b/.agents/notes/archived/manifest.json index f540320056..cded9f6c7e 100644 --- a/.agents/notes/archived/manifest.json +++ b/.agents/notes/archived/manifest.json @@ -1381,6 +1381,9 @@ "feature/2026-09-01-web-superellipse-corner-smoothing.i18n.yaml": "sha256:50afdbe5b5e19889918af6d86ab3218c05205be35938b6d33d158c60777e3b58", "feature/2026-09-01-web-superellipse-corner-smoothing.md": "sha256:b1445101c49e74bbcb4f607af850cd6df105d4034828d0dd47081e8079148f15", "feature/2026-09-01-web-superellipse-corner-smoothing.zh.md": "sha256:1a278c417c0d7de3b4c3c35061b419303b4a1a0707831c283d8f862ab9b6fd23", + "feature/2026-09-08-web-explicit-file-delivery.i18n.yaml": "sha256:99daae539cc8fd7376ce0265538bee21e1e33f3c0d77c8cc4e011f94b4e9568a", + "feature/2026-09-08-web-explicit-file-delivery.md": "sha256:bb416b1e8be081e6cb6af17792eb1a442172ff114c3a3577e5ef06a77eb57093", + "feature/2026-09-08-web-explicit-file-delivery.zh.md": "sha256:00a642380e1f6ac9d5cd840e021f4e3e4ae68a289cb6344eb3dcd73a6fd81f4d", "process/2026-06-11-doc-sync-enforcement.i18n.yaml": "sha256:33b6d5874427bd7a2bd82e7e2f4f482b12448b2464aef15a9c57975edb48554d", "process/2026-06-11-doc-sync-enforcement.md": "sha256:aa2fe83d519fc30d48dff19e596e83c8922aacc9e063e14fe2cc35b769b9100e", "process/2026-06-11-doc-sync-enforcement.zh.md": "sha256:698017bd35f030fdea3eac51df9e43138c48140f504739d687b7251d13fced2b", diff --git a/.agents/notes/implemented/feature/2026-09-08-present-workspace-source-files.i18n.yaml b/.agents/notes/implemented/feature/2026-09-08-present-workspace-source-files.i18n.yaml new file mode 100644 index 0000000000..27af3c5438 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-08-present-workspace-source-files.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-08-present-workspace-source-files.md +2026-09-08-present-workspace-source-files.md: 6239c9bd920849f3c8cf4fdee2e1ded4b758b01d +2026-09-08-present-workspace-source-files.zh.md: 7aa5bebc97558b9b0cb74406303d038483c39d94 diff --git a/.agents/notes/implemented/feature/2026-09-08-present-workspace-source-files.md b/.agents/notes/implemented/feature/2026-09-08-present-workspace-source-files.md new file mode 100644 index 0000000000..6239c9bd92 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-08-present-workspace-source-files.md @@ -0,0 +1,37 @@ +# Agent Note: Present declares workspace source files + +Status: implemented + +English | [中文](2026-09-08-present-workspace-source-files.zh.md) + +## Problem + +Users need to open and edit the files produced in their workspace, including shell-created files that have no editor mutation records. Preserving an independent delivered version adds content storage, copy verification, temporary-file retention, and a second editing destination to this workflow. + +## Decision + +The [present tool](../../../../packages/fs/tool-present/README.md) declares existing regular files inside the calling Session's workspace. It records paths and optional descriptions without reading or copying contents. The [deliverables plugin](../../../../packages/client/ui-deliverables/README.md) opens current workspace sources in the Host's default application. Edits are visible on the next open; deletion or movement makes the declaration unavailable. File-content preservation and copy-on-write storage are deferred until a persistence design owns them. + +The tool remains an ordinary package with shared filesystem and tool error classes. Its pure type entry owns the delivery event without importing Host code into the browser. The `standard`, `ptc`, and `cordis` presets mount it; `minimal` retains its two tools. Each plugin instance correlates its executions with successful final `tools/result` notifications before appending `deliverables/presented`. Native and nested calls share this rule. A later enclosing program failure does not revoke a completed nested declaration; blocked results publish none, and same-name scoped replacements cannot publish another instance's results. + +An authenticated POST selects a declaration by viewed Session, event sequence, and original file index. The event carries no owning Session ID; relative paths in inherited history resolve against the viewed Session's workspace. The Host rechecks canonical workspace containment and regular-file existence before native opening. Route disposal cancels and awaits pending commands. The existing produced-file row retains its separate text-preview behavior. + +## Alternatives considered + +**Immutable attachment snapshots and editable temporary copies** preserve delivered versions after source edits or deletion, but make desktop edits diverge from workspace files and introduce retention work without a current product requirement. This decision supersedes the [snapshot-delivery design](../../archived/feature/2026-09-08-web-explicit-file-delivery.md). Neither a download endpoint nor a fallback copy remains; both require an explicit future product decision. + +**Opening attachment-store files directly** lets editors mutate immutable objects. A future persistent delivery system needs an owned editing and retention policy, such as copy-on-write, before exposing saved versions to applications. + +**Generic artifact fields or a Host tool subpath inside the UI package** broaden unrelated APIs or couple preset installation to browser packaging. A tool-owned event and ordinary package preserve existing extension points and publication rules. + +**Tool text as the durable index** cannot survive post-processing or result spill reliably. Execution identity and final successful results retain declaration ownership independently of displayed tool text. + +**Descriptor-bound filesystem extensions** would change every provider without making an external desktop application's later path lookup atomic. Current checks reject ordinary escapes; concurrent swap-and-restore remains outside the path API's guarantees. + +## Consequences + +The Session log persists declarations but no attachment references or file contents from `present`. Session ZIP exports contain these declarations; transferring the log does not transfer workspace files. The event remains required-on-read because silently losing delivery declarations would alter reconstructed or forked history. Released Session format generations remain unchanged. + +The removed file-size cap has no role in a metadata-only declaration; the configurable file-count limit still bounds result size. Cards show file names, types, and descriptions without stale byte-size metadata. No artifact service or speculative storage fallback is introduced. + +Focused tests cover content-free declarations, invalid inputs, blocked results, source-path identity, current bytes after edits, missing files, workspace escapes, fork-relative paths, retry, cancellation, and disposal. The recorded Web scenario covers nested completion followed by enclosing failure, source edits, reload, deletion errors, card and prose opens without browser downloads, and content-free Session export. diff --git a/.agents/notes/implemented/feature/2026-09-08-present-workspace-source-files.zh.md b/.agents/notes/implemented/feature/2026-09-08-present-workspace-source-files.zh.md new file mode 100644 index 0000000000..7aa5bebc97 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-08-present-workspace-source-files.zh.md @@ -0,0 +1,37 @@ +# Agent Note:Present 声明交付工作区源文件 + +Status: implemented + +[English](2026-09-08-present-workspace-source-files.md) | 中文 + +## 问题 + +用户需要打开并编辑工作区中产出的文件,包括没有编辑器修改记录的 shell 产出文件。保存独立交付版本会为这一流程增加内容存储、副本校验、临时文件保留,以及第二个编辑目标。 + +## 决策 + +[present 工具](../../../../packages/fs/tool-present/README.zh.md)声明交付调用方 Session 工作区中已存在的普通文件。它记录路径和可选说明,不读取或复制内容。[交付插件](../../../../packages/client/ui-deliverables/README.zh.md)使用 Host 默认应用打开当前工作区源文件。下次打开会看到编辑后的内容;删除或移动文件会使声明不可用。文件内容保留与写时复制存储延期到有持久化设计负责时实现。 + +工具保持为普通包,共享文件系统和工具错误类型。其纯类型入口拥有交付事件,不向浏览器导入 Host 代码。`standard`、`ptc` 与 `cordis` preset 挂载工具;`minimal` 保持两个工具。每个插件实例将其执行与成功的最终 `tools/result` 通知关联,再追加 `deliverables/presented`。原生与嵌套调用遵循同一规则。外层程序随后失败不会撤销已完成的嵌套声明;被阻止的结果不发布声明,同名作用域替换也不能发布其他实例的结果。 + +经过认证的 POST 按当前查看的 Session、事件序号和原始文件索引选择声明。事件不携带所属 Session ID;继承历史中的相对路径按当前查看的 Session 工作区解析。Host 在原生打开前重新检查规范路径的工作区包含关系和普通文件是否存在。路由释放时取消并等待进行中的命令。原有产出文件行保留独立的文本预览行为。 + +## 考虑过的替代方案 + +**不可变附件快照和可编辑临时副本**可在源文件编辑或删除后保留交付版本,但会使桌面编辑与工作区文件分离,并在缺少当前产品需求时引入保留工作。本决策取代[快照交付设计](../../archived/feature/2026-09-08-web-explicit-file-delivery.md)。不保留下载端点或回退副本;两者都需要未来明确的产品决策。 + +**直接打开附件存储文件**会让编辑器修改不可变对象。未来持久化交付系统需要先明确编辑和保留策略,例如写时复制,再将保存版本暴露给应用。 + +**通用 artifact 字段或 UI 包内的 Host 工具子路径**会扩展无关 API,或将 preset 安装与浏览器打包耦合。工具拥有的事件与普通包保留现有扩展点和发布规则。 + +**以工具文本作为持久索引**无法可靠应对后处理或结果溢出。执行身份与最终成功结果使声明归属独立于展示的工具文本。 + +**绑定文件描述符的文件系统扩展**会改动所有提供方,却无法使外部桌面应用随后按路径打开的动作原子化。当前检查拒绝普通越界;并发替换后复原仍不在路径 API 的保证范围内。 + +## 影响 + +Session 日志持久化声明,不保存来自 `present` 的附件引用或文件内容。Session ZIP 导出包含这些声明;转移日志不会转移工作区文件。该事件仍要求读取端识别,因为静默丢失交付声明会改变重建或 fork 的历史。已发布 Session 格式代际保持不变。 + +仅声明元数据不需要文件大小上限,因此删除该限制;可配置的文件数量上限仍限制结果大小。卡片展示文件名称、类型和说明,不展示可能过时的字节大小。不引入 artifact 服务或推测性的存储回退。 + +定向测试覆盖不读取内容的声明、无效输入、被阻止的结果、源路径身份、编辑后的当前字节、缺失文件、工作区越界、fork 相对路径、重试、取消与释放。录制的 Web 场景覆盖嵌套成功后外层失败、源文件编辑、重新加载、删除错误、卡片与正文打开且无浏览器下载,以及不包含交付内容的 Session 导出。 diff --git a/apps/web/tests/present.e2e.ts b/apps/web/tests/present.e2e.ts index 0f48cac2f2..dd55988dc2 100644 --- a/apps/web/tests/present.e2e.ts +++ b/apps/web/tests/present.e2e.ts @@ -1,5 +1,5 @@ -/** Recorded delivery, source deletion, reload, and Session ZIP behavior. */ -import { readFile, unlink, mkdir, mkdtemp, writeFile, rm } from 'node:fs/promises' +/** Recorded source-file delivery, edits, reload, deletion, and Session ZIP behavior. */ +import { readFile, unlink, mkdir, mkdtemp, writeFile, rm, realpath } from 'node:fs/promises' import { join, delimiter } from 'node:path' import { fileURLToPath } from 'node:url' import { chromium, type Browser, type Page } from 'playwright' @@ -35,7 +35,7 @@ describe.skipIf(process.platform === 'win32' || release().toLowerCase().includes const events: SessionEvent[] = [] let nativeRoot: string | undefined let openLog: string - const opened = async (): Promise => (await readFile(openLog, 'utf8')).split('\n').filter(Boolean).map(line => JSON.parse(line) as string) + const opened = async (): Promise> => (await readFile(openLog, 'utf8')).split('\n').filter(Boolean).map(line => JSON.parse(line) as { path: string; content: string }) const downloads: string[] = [] beforeAll(async () => { @@ -46,7 +46,7 @@ describe.skipIf(process.platform === 'win32' || release().toLowerCase().includes const command = process.platform === 'darwin' ? 'open' : 'xdg-open' await writeFile(join(nativeRoot, command), `#!${process.execPath} const fs = require('node:fs'); -fs.appendFileSync(${JSON.stringify(openLog)}, JSON.stringify(fs.readFileSync(process.argv[2], 'utf8')) + '\\n'); +fs.appendFileSync(${JSON.stringify(openLog)}, JSON.stringify({ path: process.argv[2], content: fs.readFileSync(process.argv[2], 'utf8') }) + '\\n'); `, { mode: 0o700 }) vi.stubEnv('PATH', `${nativeRoot}${delimiter}${process.env.PATH ?? ''}`) await mkdir(DIR, { recursive: true }) @@ -79,7 +79,7 @@ fs.appendFileSync(${JSON.stringify(openLog)}, JSON.stringify(fs.readFileSync(pro } }) - it('delivers nested snapshots even when the enclosing program subsequently fails', async () => { + it('declares nested deliveries even when the enclosing program subsequently fails', async () => { if (MODE !== 'record') expect(fixtureUserPrompts(await readFile(FIXTURE, 'utf8'))).toEqual([PROMPT]) const settled = scaffold.whenTurnSettled() const input = page.locator('[data-composer-input]').first() @@ -94,13 +94,21 @@ fs.appendFileSync(${JSON.stringify(openLog)}, JSON.stringify(fs.readFileSync(pro await assertFinalWorkspaceSnapshot(DIR, cwd) expect(events.filter(event => event.type === 'deliverables/presented').flatMap(event => event.data.files.map(file => file.path))) .toEqual(['report.txt', '说明.txt']) + for (const event of events) { + if (event.type === 'deliverables/presented') { + expect(event.data.files).toEqual([ + { path: 'report.txt', description: 'delivered report' }, + { path: '说明.txt', description: 'delivered note' }, + ]) + } + } expect(events.some(event => event.type === 'tool/code-dispatch' && event.data.name === 'present' && event.data.isError)).toBe(true) expect(events.some(event => event.type === 'tool/result' && event.data.message.content[0].isError)).toBe(true) }, 200_000) - it('opens saved copies after source deletion and reload, while Session ZIP contains only references', async () => { - await unlink(join(cwd, 'report.txt')) - await unlink(join(cwd, '说明.txt')) + it('opens current source files after edits and reload, and reports deletion without downloading', async () => { + await writeFile(join(cwd, 'report.txt'), 'EDITED_REPORT\n') + await writeFile(join(cwd, '说明.txt'), 'EDITED_NOTE\n') for (const reload of [false, true]) { if (reload) { const warningStart = tripwire.warnings.length @@ -111,14 +119,14 @@ fs.appendFileSync(${JSON.stringify(openLog)}, JSON.stringify(fs.readFileSync(pro const row = page.locator('[data-presented-files-row]') await row.waitFor() expect(await row.getByRole('button').count()).toBe(2) - for (const [name, bytes] of [['report.txt', 'DELIVERED_REPORT\n'], ['说明.txt', 'DELIVERED_NOTE\n']]) { + for (const [name, bytes] of [['report.txt', 'EDITED_REPORT\n'], ['说明.txt', 'EDITED_NOTE\n']] as const) { const count = (await opened()).length const response = page.waitForResponse(response => response.url().includes('/api/present.open?') && response.request().method() === 'POST') await row.getByRole('button', { name: `Open ${name} in default app`, exact: true }).click() expect((await response).status()).toBe(204) await page.waitForFunction(() => document.querySelector('[data-presented-files-row] button:disabled') === null) expect(await opened()).toHaveLength(count + 1) - expect((await opened()).at(-1)).toBe(bytes) + expect((await opened()).at(-1)).toEqual({ path: await realpath(join(cwd, name)), content: bytes }) } } const count = (await opened()).length @@ -127,13 +135,22 @@ fs.appendFileSync(${JSON.stringify(openLog)}, JSON.stringify(fs.readFileSync(pro await page.waitForFunction(() => document.querySelector('[data-presented-files-row] button:disabled') === null) expect((await openedResponse).status()).toBe(204) expect(await opened()).toHaveLength(count + 1) - expect((await opened()).at(-1)).toBe('DELIVERED_REPORT\n') + expect((await opened()).at(-1)).toEqual({ path: await realpath(join(cwd, 'report.txt')), content: 'EDITED_REPORT\n' }) expect(downloads).toEqual([]) const response = await page.request.get(new URL(`/api/session.export?sessionId=${sessionId}`, scaffold.authenticatedUrl).href) expect(response.status()).toBe(200) const entries = unzipSync(await response.body()) expect(Object.keys(entries)).toHaveLength(1) - expect(strFromU8(Object.values(entries)[0]!)).toContain('deliverables/presented') + const exported = strFromU8(Object.values(entries)[0]!) + expect(exported).toContain('deliverables/presented') + const declarations = exported.trim().split('\n').map(line => JSON.parse(line) as SessionEvent) + .filter(event => event.type === 'deliverables/presented') + expect(declarations).toHaveLength(1) + expect(declarations[0]!.data.files).toEqual([ + { path: 'report.txt', description: 'delivered report' }, + { path: '说明.txt', description: 'delivered note' }, + ]) + expect(exported).not.toContain('EDITED_REPORT') if (MODE !== 'record') { const aria = await captureExpandedTurnProcessAria(page, '[class*="centerCol"]', scaffold.workspaceCwd) await compareOrRefreshGolden(join(DIR, 'ui.expected.md'), aria, MODE) @@ -155,6 +172,14 @@ fs.appendFileSync(${JSON.stringify(openLog)}, JSON.stringify(fs.readFileSync(pro expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(480) } } + const beforeDelete = (await opened()).length + await unlink(join(cwd, 'report.txt')) + const missing = page.waitForResponse(response => response.url().includes('/api/present.open?')) + await page.locator('[data-presented-files-row]').getByRole('button', { name: 'Open report.txt in default app', exact: true }).click() + expect((await missing).status()).toBe(404) + await page.getByText('Could not open. Click to retry.', { exact: true }).waitFor() + expect(await opened()).toHaveLength(beforeDelete) + expect(downloads).toEqual([]) expect(tripwire.pageErrors).toEqual([]) expect(tripwire.warnings).toEqual([]) }) diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 9006428dc3..7fa075c800 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-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/config-catalog.md -config-catalog.md: 89eac550fce4b767023d80d519ca3d60849a7e52 -config-catalog.zh.md: 7231b0867faa9998712e97be8889dc5e6f38a9ed +config-catalog.md: 0a06bdf478ccc25b3a3cedb45df18ee0dca285a9 +config-catalog.zh.md: 580c11bd2b3cf6c598206eaa6b1bb0398e85713d diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 89eac550fc..0a06bdf478 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -2873,19 +2873,17 @@ Source: [`packages/lsp/tool-lsp/src/index.ts:57`](../packages/lsp/tool-lsp/src/i ## `@deepseek-ai/dsh-tool-present` -Requires: `tools` · `fs` · `attachments` · `sessionProjections` +Requires: `tools` · `fs` · `sessionProjections` ```ts config-catalog -/** Per-call snapshot limits. */ +/** Per-call delivery limit. */ export interface Config { - /** Inclusive per-file byte cap; at most 100 MiB. */ - maxFileBytes: number /** Maximum number of files in one call. */ maxFiles: number } ``` -Source: [`packages/fs/tool-present/src/index.ts:16`](../packages/fs/tool-present/src/index.ts) +Source: [`packages/fs/tool-present/src/index.ts:15`](../packages/fs/tool-present/src/index.ts) @@ -3459,7 +3457,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co - `@deepseek-ai/dsh-client-ui-commands` ([`packages/client/ui-commands/src/index.ts`](../packages/client/ui-commands/src/index.ts)) - `@deepseek-ai/dsh-client-ui-conversation` ([`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts)) - `@deepseek-ai/dsh-client-ui-cordis` ([`packages/extensions/ui-cordis/src/index.ts`](../packages/extensions/ui-cordis/src/index.ts)) -- `@deepseek-ai/dsh-client-ui-deliverables` — requires `systemPrompt` · `connection` · `sessionQuery` · `attachments` · `sessionController` ([`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts)) +- `@deepseek-ai/dsh-client-ui-deliverables` — requires `systemPrompt` · `connection` · `sessionQuery` · `sessionController` ([`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts)) - `@deepseek-ai/dsh-client-ui-directory-picker-browse` ([`packages/client/ui-directory-picker-browse/src/index.ts`](../packages/client/ui-directory-picker-browse/src/index.ts)) - `@deepseek-ai/dsh-client-ui-directory-picker-native` ([`packages/client/ui-directory-picker-native/src/index.ts`](../packages/client/ui-directory-picker-native/src/index.ts)) - `@deepseek-ai/dsh-client-ui-goal` ([`packages/client/ui-goal/src/index.ts`](../packages/client/ui-goal/src/index.ts)) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 7231b0867f..580c11bd2b 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -2875,19 +2875,17 @@ export interface Config { ## `@deepseek-ai/dsh-tool-present` -依赖: `tools` · `fs` · `attachments` · `sessionProjections` +依赖: `tools` · `fs` · `sessionProjections` ```ts config-catalog -/** Per-call snapshot limits. */ +/** Per-call delivery limit. */ export interface Config { - /** Inclusive per-file byte cap; at most 100 MiB. */ - maxFileBytes: number /** Maximum number of files in one call. */ maxFiles: number } ``` -来源: [`packages/fs/tool-present/src/index.ts:16`](../packages/fs/tool-present/src/index.ts) +来源: [`packages/fs/tool-present/src/index.ts:15`](../packages/fs/tool-present/src/index.ts) @@ -3461,7 +3459,7 @@ export interface Config { - `@deepseek-ai/dsh-client-ui-commands`([`packages/client/ui-commands/src/index.ts`](../packages/client/ui-commands/src/index.ts)) - `@deepseek-ai/dsh-client-ui-conversation`([`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts)) - `@deepseek-ai/dsh-client-ui-cordis`([`packages/extensions/ui-cordis/src/index.ts`](../packages/extensions/ui-cordis/src/index.ts)) -- `@deepseek-ai/dsh-client-ui-deliverables` — 需要 `systemPrompt` · `connection` · `sessionQuery` · `attachments` · `sessionController`([`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts)) +- `@deepseek-ai/dsh-client-ui-deliverables` — 需要 `systemPrompt` · `connection` · `sessionQuery` · `sessionController`([`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts)) - `@deepseek-ai/dsh-client-ui-directory-picker-browse`([`packages/client/ui-directory-picker-browse/src/index.ts`](../packages/client/ui-directory-picker-browse/src/index.ts)) - `@deepseek-ai/dsh-client-ui-directory-picker-native`([`packages/client/ui-directory-picker-native/src/index.ts`](../packages/client/ui-directory-picker-native/src/index.ts)) - `@deepseek-ai/dsh-client-ui-goal`([`packages/client/ui-goal/src/index.ts`](../packages/client/ui-goal/src/index.ts)) diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index c3b0738000..8049e2d430 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-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/persistence-catalog.md -persistence-catalog.md: 1749faa99beb39940e2581bc58d0543ea5984fd1 -persistence-catalog.zh.md: b3e13c5f4bb9cf973249eaf7b8cf7d93366be96f +persistence-catalog.md: a9e1221a564a4ad1af1353278f1215bb33fb9c4f +persistence-catalog.zh.md: 2b74b03b4b783f848385e66e15c40d73f50a788b diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 1749faa99b..a9e1221a56 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -403,13 +403,13 @@ Source: [`packages/compaction/compaction/src/types.ts:34`](../packages/compactio #### `deliverables/presented` — log-only ```ts persistence-catalog -/** Saved deliveries from a successful final present result, including nested calls. */ +/** Declared workspace files from a successful final present result, including nested calls. */ 'deliverables/presented': { turn: number; callId: ToolCallId; files: PresentedFile[] } ``` Types: [ToolCallId](subsystems/core.md) -Source: [`packages/fs/tool-present/src/types.ts:16`](../packages/fs/tool-present/src/types.ts) +Source: [`packages/fs/tool-present/src/types.ts:15`](../packages/fs/tool-present/src/types.ts) ### `feedback/*` diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index b3e13c5f4b..2b74b03b4b 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -405,13 +405,13 @@ export type SessionEvent = { #### `deliverables/presented` — 仅日志 ```ts persistence-catalog -/** Saved deliveries from a successful final present result, including nested calls. */ +/** Declared workspace files from a successful final present result, including nested calls. */ 'deliverables/presented': { turn: number; callId: ToolCallId; files: PresentedFile[] } ``` 类型: [ToolCallId](subsystems/core.zh.md) -来源: [`packages/fs/tool-present/src/types.ts:16`](../packages/fs/tool-present/src/types.ts) +来源: [`packages/fs/tool-present/src/types.ts:15`](../packages/fs/tool-present/src/types.ts) ### `feedback/*` diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml index c43efdd85f..49f6610bd2 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: 5a09b07e0a7a24e654fc45a7a477838ce21f30d7 -tool-catalog.zh.md: 528f3a37680ecb5f9482606c5671c4fb056092f1 +tool-catalog.md: c2ea95ff460b65fbba789738b16b0c67a7c91948 +tool-catalog.zh.md: 41fc1eaa3c6a0acf17bde4b69c97413b64872930 diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index 5a09b07e0a..c2ea95ff46 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -19,7 +19,7 @@ This table connects model-visible tool names to the plugin package and service s | `@deepseek-ai/dsh-tools` | `run_code` | `ctx.tools`, `ctx.codeRuntime (execution time)`, `ctx.systemPrompt` | `tool/call`, `one tool/ptc-dispatch-start + tool/ptc-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-present` | `present` | `ctx.tools`, `ctx.fs`, `ctx.attachments`, `ctx.sessionProjections` | `tool/call`, `deliverables/presented after a successful final result`, `tool/result` | - | Deliveries belong to the calling Session; Web ui-deliverables supplies authenticated downloads and cards. | +| `@deepseek-ai/dsh-tool-present` | `present` | `ctx.tools`, `ctx.fs`, `ctx.sessionProjections` | `tool/call`, `deliverables/presented after a successful final result`, `tool/result` | - | Deliveries belong to the calling Session; Web ui-deliverables supplies source-file opening and cards. | | `@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. | | `@deepseek-ai/dsh-tool-cordis` | `cordis_define`, `cordis_inspect_list`, `cordis_inspect_query`, `cordis_inspect_self`, `cordis_run`, `cordis_stop`, `cordis_undefine` | `ctx.tools`, `ctx.dynamicCordisRunner` | `tool/call`, `tool/result`, `process-local dynamic package lifecycle` | - | Not in any shipped tree (a deliberate opt-in — dynamic package code reaches the real runtime, see .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). The toolset injects `ctx.dynamicCordisRunner` from `@deepseek-ai/dsh-cordis-host-runner`, which owns the definition registry and the vm sandbox; a composition missing it never activates the tools. A running package may register ADDITIONAL model-visible tools until it is stopped, undefined, or DSH restarts; a full changed request header logs those tool-set changes. | | `@deepseek-ai/dsh-tool-bash-persistent` | `bash` | `ctx.tools`, `ctx.terminals`, `an owning Agent at execution time` | `tool/call`, `PTY shell state`, `tool/result` | - | One owner-isolated persistent bash tool; deployment composition supplies the PTY backend and may override the model-facing environment description. | @@ -225,7 +225,7 @@ The bash tool is the model-facing consumer of the bash executor seam. A `run_in_ ### `present` -Deliver final files to the user. Saves a snapshot of each existing workspace file so it remains downloadable after edits or deletion. Create the files before calling this tool. +Declare existing workspace files as final deliverables. The user opens the current source files; their contents are not copied or preserved. Create the files before calling this tool. ```json { @@ -260,7 +260,7 @@ Deliver final files to the user. Saves a snapshot of each existing workspace fil Source: [`packages/fs/tool-present/src/index.ts`](../packages/fs/tool-present/src/index.ts) -Deliveries belong to the calling Session; Web ui-deliverables supplies authenticated downloads and cards. +Deliveries belong to the calling Session; Web ui-deliverables supplies source-file opening and cards. diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md index 528f3a3768..41fc1eaa3c 100644 --- a/docs/tool-catalog.zh.md +++ b/docs/tool-catalog.zh.md @@ -23,7 +23,7 @@ | `@deepseek-ai/dsh-tools` | `run_code` | `ctx.tools`、`ctx.codeRuntime (execution time)`、`ctx.systemPrompt` | `tool/call`、`one tool/ptc-dispatch-start + tool/ptc-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-present` | `present` | `ctx.tools`, `ctx.fs`, `ctx.attachments`, `ctx.sessionProjections` | `tool/call`, `deliverables/presented 在成功的最终结果之后`, `tool/result` | - | 交付归调用方 Session 所有;Web ui-deliverables 提供认证下载与卡片。 | +| `@deepseek-ai/dsh-tool-present` | `present` | `ctx.tools`, `ctx.fs`, `ctx.sessionProjections` | `tool/call`, `deliverables/presented 在成功的最终结果之后`, `tool/result` | - | 交付归调用方 Session 所有;Web ui-deliverables 提供源文件打开与卡片。 | | `@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`。 | | `@deepseek-ai/dsh-tool-cordis` | `cordis_define`、`cordis_inspect_list`、`cordis_inspect_query`、`cordis_inspect_self`、`cordis_run`、`cordis_stop`、`cordis_undefine` | `ctx.tools`、`ctx.dynamicCordisRunner` | `tool/call`、`tool/result`、`process-local dynamic package lifecycle` | - | 不在任何随产品发布的树中,需要显式选择启用;动态 Package 代码可以访问真实运行时,见 .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md。该工具集注入 `@deepseek-ai/dsh-cordis-host-runner` 提供的 `ctx.dynamicCordisRunner`,后者拥有定义注册表和 vm 沙箱;组合缺少它时这些工具不会激活。运行中的 Package 在停止、undefine 或 DSH 重启前可以注册**额外的**模型可见工具;发生这类工具集变化时,系统会记录完整且有变动的请求头。 | | `@deepseek-ai/dsh-tool-bash-persistent` | `bash` | `ctx.tools`、`ctx.terminals`、`an owning Agent at execution time` | `tool/call`、`PTY shell state`、`tool/result` | - | 一个按所有者隔离的持久 bash 工具;部署组合提供 PTY 后端,并可覆盖面向模型的环境描述。 | @@ -229,7 +229,7 @@ bash 工具是 bash 执行器 seam 面向模型的消费方。使用 `run_in_bac ### `present` -向用户交付最终文件。保存每个已有工作区文件的快照,使其在编辑或删除后仍可下载。调用工具前先创建文件。 +声明交付已有的工作区文件。用户打开当前源文件;不复制或保存其内容。调用工具前先创建文件。 ```json { @@ -264,7 +264,7 @@ bash 工具是 bash 执行器 seam 面向模型的消费方。使用 `run_in_bac 来源: [`packages/fs/tool-present/src/index.ts`](../packages/fs/tool-present/src/index.ts) -交付归调用方 Session 所有;Web ui-deliverables 提供认证下载与卡片。 +交付归调用方 Session 所有;Web ui-deliverables 提供源文件打开与卡片。 diff --git a/packages/client/ui-deliverables/README.i18n.yaml b/packages/client/ui-deliverables/README.i18n.yaml index d32df8879e..9cc543ee66 100644 --- a/packages/client/ui-deliverables/README.i18n.yaml +++ b/packages/client/ui-deliverables/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-deliverables/README.md -README.md: abf944d0115934d781ae44bf176914263822277e -README.zh.md: e8f556a7469800898b660fe1d81e16db02062b27 +README.md: ae42abb5a69d29332bd7cfd6d2d2a1191381e79a +README.zh.md: 664dfa3cd2e0d526b43464a80290604a8ecf958c diff --git a/packages/client/ui-deliverables/README.md b/packages/client/ui-deliverables/README.md index abf944d011..ae42abb5a6 100644 --- a/packages/client/ui-deliverables/README.md +++ b/packages/client/ui-deliverables/README.md @@ -30,7 +30,7 @@ Mount this plugin alongside `ui-conversation`; a finished turn then ends with th ### Explicit deliveries -The Web `standard`, `ptc`, and `cordis` presets expose `present` for final files, including files created through Bash. Call it with `files: [{ path, description? }]` after creating the files. The [present tool](../../fs/tool-present/README.md) owns snapshot creation, limits, and Session delivery records. The closing turn shows responsive file cards with names, types, sizes, descriptions, and buttons that open a saved copy in the Host’s default application. Matching inline-code references open the same snapshots after source edits, deletion, or reload, without starting a browser download. Forks authorize opening through the viewed Session. Repeated delivery of a path selects its latest successful snapshot before the closing reply. +The Web `standard`, `ptc`, and `cordis` presets expose `present` for final workspace files, including files created through Bash. Call it with `files: [{ path, description? }]` after creating the files. The [present tool](../../fs/tool-present/README.md) owns file-count limits and Session declarations. The closing turn shows responsive cards with file names, types, descriptions, and buttons that open the source in the Host’s default application. Matching inline-code references open the same source files without starting a browser download. Repeated declaration of a path selects its latest description before the closing reply. The `present` tool row shows running, delivered, failed, or interrupted status; expanding a settled row reveals its recorded result. File cards include every delivered file. Opening shows progress, confirmation, or a retryable error on the card. It requires a desktop and a suitable default application on the serving Host; a remote browser does not open applications on its own device. @@ -52,7 +52,7 @@ The closing prose carries the same vocabulary: an inline-code token resolves by The Node half registers the static `ui:deliverable-file-references` system-prompt section asking the model to mention primary files from successful creation or modification calls and to write those and any other changed-file references as Markdown inline code. The browser half registers a wrapper around `ProducedFiles` and explicit deliveries into the chat view's `conversation.chat.turnTail` hole. `deliverablesDefinition` folds each Turn's successful first-party mutation calls into `DeliverablesTurnData` from the validated raw arguments of `write`, `edit`, and mutating `str_replace_editor` commands. Reads, deletes, unsupported tools, malformed calls, and failed results contribute nothing. A new mutation tool needs an explicit Client contribution before it joins the list. The package also provides the `chatFileMentions` service the chat view consults per closing message; composing the plugin out removes both surfaces and leaves the view's empty chain at zero cost. -Native opening uses an authenticated POST addressed by Session, event sequence, and original file index. The Host streams the saved bytes into a private temporary copy and verifies the complete attachment before launching the default application. Each gesture creates a separate copy, so application edits cannot change the stored snapshot. Failed opens remove their copies; successful copies remain until plugin disposal because applications may read lazily. Disposal cancels and awaits pending work before cleanup. The authenticated GET download endpoint remains available to byte consumers. +Native opening uses an authenticated POST addressed by the viewed Session, event sequence, and original file index. The Host resolves the declaration against that Session’s workspace and checks the current file exists within it before launching the default application. Edits affect subsequent opens; deletion returns an error. No file-content copy or attachment is created. Plugin disposal cancels and awaits pending native-open requests. @@ -95,8 +95,8 @@ The section is static at first-party order 9000 for the lifetime of the package These limits define the current deliverables vocabulary. They are current package constraints, not a general file-linking comparison or a task backlog. - **Mention matching is exact path or unique basename only** — a suffix mention stays inert; widening the matcher is deferred until a real closing-message shape needs it. -- **Terminal-created files require explicit delivery** — call `present` to make their saved snapshots available. -- **Transferred Session exports contain no delivered bytes** — delivery actions require the same snapshots in the serving host’s attachment store; missing or pruned snapshots return 404. +- **Terminal-created files require explicit delivery** — call `present` to declare them for native opening. +- **Declarations do not preserve file contents** — reopening or transferring a Session requires the source files in the viewed Session’s workspace. Missing files return 404; paths resolving outside the workspace return 403. - **Directories have no destination** — chips open files in the right Sidebar's text preview, which shows files only; the former native folder handoff is gone rather than replaced. @@ -109,4 +109,4 @@ None. -**Runtime invariant:** No companion is published. The prompt section, slot, dictionary, file-action routes, and optional service registrations are effect-owned with disposal proven by their plugin specs; the attachment service owns saved bytes, and the Session log owns delivery references. +**Runtime invariant:** No companion is published. Prompt, slot, dictionary, file-action route, and optional service registrations are effect-owned; the Session log owns declarations and the workspace owns file contents. diff --git a/packages/client/ui-deliverables/README.zh.md b/packages/client/ui-deliverables/README.zh.md index e8f556a746..664dfa3cd2 100644 --- a/packages/client/ui-deliverables/README.zh.md +++ b/packages/client/ui-deliverables/README.zh.md @@ -30,7 +30,7 @@ kind: "package-reference" ### 显式交付 -Web 的 `standard`、`ptc` 与 `cordis` preset 提供 `present` 用于交付最终文件,包括通过 Bash 创建的文件。创建文件后,以 `files: [{ path, description? }]` 调用。[present 工具](../../fs/tool-present/README.zh.md)拥有快照创建、限制和 Session 交付记录。收尾 turn 显示响应式文件卡片,包含名称、类型、大小、说明和在 Host 默认应用中打开保存副本的按钮。匹配的行内代码引用也打开相同快照;修改或删除源文件、重新加载后仍可打开,不触发浏览器下载。Fork 通过当前查看的 Session 授权打开。同一路径重复交付时,选择收尾回复之前最近一次成功的快照。 +Web 的 `standard`、`ptc` 与 `cordis` preset 提供 `present` 用于声明交付最终工作区文件,包括通过 Bash 创建的文件。创建文件后,以 `files: [{ path, description? }]` 调用。[present 工具](../../fs/tool-present/README.zh.md)拥有文件数量限制和 Session 声明。收尾 turn 显示响应式卡片,包含文件名称、类型、说明和在 Host 默认应用中打开源文件的按钮。匹配的行内代码引用打开相同源文件,不触发浏览器下载。同一路径重复声明时,选择收尾回复之前最近一次的说明。 `present` 工具行显示正在交付、已交付、失败或中断状态;展开已结束的调用可查看其记录的结果。文件卡片展示全部交付文件。打开时,卡片显示进度、成功确认或可重试的错误。服务 Host 必须具备桌面和合适的默认应用;远程浏览器不会打开其所在设备上的应用。 @@ -52,7 +52,7 @@ Web 的 `standard`、`ptc` 与 `cordis` preset 提供 `present` 用于交付最 Node 半部注册静态 `ui:deliverable-file-references` 系统提示词段,要求模型点名成功创建或修改的主要文件,并把这些文件以及正文中提到的其他本轮变更文件写成 Markdown 行内代码。浏览器半部把组合 `ProducedFiles` 与显式交付的包装组件注册进 chat 视图的 `conversation.chat.turnTail` 洞。`deliverablesDefinition` 根据 `write`、`edit` 和有修改作用的 `str_replace_editor` 命令中经过校验的原始参数,把每个轮次成功的第一方修改调用折叠进 `DeliverablesTurnData`。读取、删除、不受支持的工具、格式错误的调用和失败结果不贡献任何条目。新的修改工具必须增加显式 Client contribution 才能加入列表。本包还提供 chat 视图按收尾消息查询的 `chatFileMentions` 服务;把插件组合出去会同时移除两个表面,视图的空链以零成本留下。 -原生打开使用经过认证的 POST,通过 Session、事件序号和原始文件索引定位文件。Host 将保存的字节流写入私有临时副本,完整校验 attachment 后才启动默认应用。每次操作创建独立副本,因此应用内的编辑不会修改已保存的快照。打开失败时删除副本;成功副本保留到插件释放,因为应用可能延迟读取。释放时先取消并等待进行中的操作,再执行清理。经过认证的 GET 下载端点仍供字节读取方使用。 +原生打开使用经过认证的 POST,通过当前查看的 Session、事件序号和原始文件索引定位声明。Host 按该 Session 的工作区解析路径,检查当前文件存在且位于工作区内,再启动默认应用。编辑会影响后续打开的内容;删除后返回错误。不创建文件内容副本或附件。插件释放时取消并等待进行中的原生打开请求。 @@ -95,9 +95,9 @@ Node 半部注册静态 `ui:deliverable-file-references` 系统提示词段, 这些限制界定了当前产出物词表。它们是当前包约束,不是通用文件链接对比或任务积压。 - **提及匹配只认精确路径或唯一 basename**——后缀式提及保持惰性;等真实的收尾消息形态产生需求后再放宽匹配规则。 -- **终端创建的文件需要显式交付**——调用 `present` 使其快照可供下载。 -- **转移的 Session 导出不含交付字节** — 下载链接依赖服务主机附件存储中的同一快照;快照缺失或被清理时返回 404。 -- **原生文件夹交接以 Host 桌面为目标**——经非 loopback authority 访问的浏览器会省略该动作,报告没有原生打开器的部署也一样;若 SSH 转发让远端 Host 看似 loopback 本地,部署必须为 Session Controller 设置 `nativeOpen: false`。 +- **终端创建的文件需要显式交付**——调用 `present` 声明文件,以便原生打开。 +- **声明不保存文件内容**——重新打开或转移 Session 后,需要当前查看的 Session 工作区中仍有源文件。文件缺失返回 404;解析到工作区外的路径返回 403。 +- **目录没有打开目标**——标签项在右侧 Sidebar 的文本预览中打开文件,该预览仅支持文件,不提供原生文件夹打开动作。 ### 开发备注 @@ -109,4 +109,4 @@ Node 半部注册静态 `ui:deliverable-file-references` 系统提示词段, -**运行时不变式:** 不发布伴生入口。prompt section、slot、dictionary、文件操作路由与可选 service 注册都归 effect 所有,释放由插件测试证明;attachment 服务拥有保存的字节,Session 日志拥有交付引用。 +**运行时不变式:** 不发布伴生入口。提示词、slot、dictionary、文件操作路由与可选 service 注册归 effect 所有;Session 日志拥有声明,工作区拥有文件内容。 diff --git a/packages/client/ui-deliverables/package.json b/packages/client/ui-deliverables/package.json index e43f6745b5..67970786a0 100644 --- a/packages/client/ui-deliverables/package.json +++ b/packages/client/ui-deliverables/package.json @@ -63,10 +63,8 @@ "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", - "@deepseek-ai/dsh-attachment": "workspace:^", "@deepseek-ai/dsh-session-query": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", - "@deepseek-ai/dsh-attachment-local": "workspace:^", "@deepseek-ai/dsh-client-ui-tool": "workspace:^", "@deepseek-ai/dsh-tool-present": "workspace:^", "@deepseek-ai/dsh-api-session-controller": "workspace:^" diff --git a/packages/client/ui-deliverables/src/client/Deliverables.tsx b/packages/client/ui-deliverables/src/client/Deliverables.tsx index 8f2dab5156..2faf259ad4 100644 --- a/packages/client/ui-deliverables/src/client/Deliverables.tsx +++ b/packages/client/ui-deliverables/src/client/Deliverables.tsx @@ -1,6 +1,6 @@ -/** Existing changed-file chips and explicitly delivered snapshots for a closing turn. */ +/** Existing changed-file chips and explicitly declared files for a closing turn. */ import type { TurnTailOwnerProps } from '@deepseek-ai/dsh-client-ui-chat/client' -import { LinkIcon, classifyLinkPath, fileSizeText, IconRightUpOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' +import { LinkIcon, classifyLinkPath, IconRightUpOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' import type { InjectFace, PropsLocale, SessionStandardProps } from '@deepseek-ai/dsh-client-ui-slots' import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store' import type { PresentedOpenController } from './present-open.ts' @@ -19,7 +19,7 @@ export interface DeliverablesInjected { } /** - * Claim turns containing modified paths or presented snapshots. + * Claim turns containing modified paths or declared files. * @param owner - closing turn. * @returns matched files, or null for an empty turn. */ @@ -30,7 +30,7 @@ export function selectDeliverables(owner: TurnTailOwnerProps): DeliverablesMatch } /** - * Render workspace file actions and default-application buttons for saved deliveries. + * Render workspace file actions and default-application buttons for declared files. * @param props - matched files, workspace opener, and localized copy. * @returns the closing turn's file rows. */ @@ -44,7 +44,7 @@ export function Deliverables({ matched, openFile, t, sessionId, openPresented, u {t('presented.label')}
    {matched.presented.map((file) => { - const phase = states[presentedFileUrl(sessionId, file.seq, file.index, 'open')] + const phase = states[presentedFileUrl(sessionId, file.seq, file.index)] return