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 1ab141230b..27e43ef4b2 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 @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write -2026-07-10-single-file-executable-sdk-runtime-distribution.md: 39cfb2999dea7767a18702ad7d160c9e88d7bf20 -2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: e1a21c40647e1418d4afd02c0bc6b44ef0d4a8cf +# 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: f749d6a72b4c32a189a9f848595076457819d9b9 +2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 2b511573bc68e5378279cec8d22ce960af0966e9 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 39cfb2999d..f749d6a72b 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 @@ -40,15 +40,15 @@ The deploy root is [`python/sdk-runtime/package.json`](../../../../python/sdk-ru ### Build pipeline and artifacts -[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-jsonrpc-agent-pkg deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → inject the pkg configuration (`bin` points at `node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js` inside the closure, `assets` is a full glob — dynamic import is invisible to pkg's static analysis, so everything must be packed in explicitly) → one `pkg --sea` per target → the executables `dsh-jsonrpc-agent-pkg--` land in `dist-exe/` and are copied back into the runtime directory. CI treats them as intermediate test inputs and retains their platform wheels. All four deploy flags are grounded in measurement: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted yields a zero-symlink file tree (most stable for the pkg VFS, physically guaranteeing a single cordis instance); disabling automatic peer installation keeps unpublished package names from triggering registry resolution; link-workspace-packages points the closure at workspace/vendor sources. +[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-jsonrpc-agent-pkg deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → inject the pkg configuration (`bin` points at `node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js` inside the closure, `assets` is a full glob — dynamic import is invisible to pkg's static analysis, so everything must be packed in explicitly) → stage the target `node-pty` addon → one `pkg --sea` per target → the executables `dsh-jsonrpc-agent-pkg--` land in `dist-exe/` and are copied back into the runtime directory. Linux installs build `pty.node` from source, so the builder copies it from the root install into the staged closure because legacy deploy omits that side-effect directory; macOS uses its target prebuild and emits the required `-spawn-helper` beside the executable. CI treats these products as intermediate test inputs and retains their platform wheels. All four deploy flags are grounded in measurement: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted yields a zero-symlink file tree (most stable for the pkg VFS, physically guaranteeing a single cordis instance); disabling automatic peer installation keeps unpublished package names from triggering registry resolution; link-workspace-packages points the closure at workspace/vendor sources. CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml), triggered explicitly only — `workflow_dispatch`, or the `build-exe` label on a pull request; native builds on the three platforms linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64, with `~/.pkg-cache` cached; macOS ad-hoc signing is handled by pkg. Each leg drives a mock SSE model through the SDK with the default config and a custom `cordis.yml`, drives the exe directly over NDJSON JSON-RPC, verifies the JSONL and final response, and installs release-shaped wheels into a clean venv without `runtime_bin`; Linux additionally inspects GLIBC requirements and runs in a manylinux 2.28 container. A full three-target run retains four artifacts, each containing one release file: the platform-independent SDK wheel and three native runtime wheels; a subset dispatch retains the SDK wheel and selected runtime wheels. Bare executables and source bundles remain intermediate test inputs. [`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) accepts only `python-vX.Y.Z` tag pipelines whose version matches the root `package.json`, builds one SDK wheel and three native runtime wheels, then a single serialized job checks and publishes all four to the project PyPI registry. Windows is a non-goal. ### Python SDK distribution: two carriers, exe for production, node for development -The Python SDK lives at [`python/`](../../../../python/README.md): `python/sdk` (the client) + `python/sdk-runtime` (the runtime carrier package). The runtime package's data directory holds three kinds of content: the checked-in default `runtime/cordis.yml`, the build-injected platform exe, and the build-injected `runtime/node/` closure tree. `resolve_bundled_launch_args()` automatic resolution **finds the exe only**; the node carrier is enabled only by an explicit `DSH_RUNTIME_MODE=node` (running `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js`, requiring a system node ≥22.19), positioned as the development-verification channel for members of this repo, and does not enter wheel distributions. +The Python SDK lives at [`python/`](../../../../python/README.md): `python/sdk` (the client) + `python/sdk-runtime` (the runtime carrier package). The runtime package's data directory holds the checked-in default `runtime/cordis.yml`, the build-injected platform exe and optional helper, and the build-injected `runtime/node/` closure tree. `resolve_bundled_launch_args()` automatic resolution **finds the exe only**; the node carrier is enabled only by an explicit `DSH_RUNTIME_MODE=node` (running `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js`, requiring a system node ≥22.19), positioned as the development-verification channel for members of this repo, and does not enter wheel distributions. -[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) reads the authoritative stable `X.Y.Z` from the repository root `package.json` and stages both packages at that version, with the SDK depending exactly on `deepseek-harness-runtime-bin==X.Y.Z`. An optional `python-vX.Y.Z` release tag is a consistency assertion and is rejected when it differs from the repository version; the source `pyproject.toml` development sentinel never determines a release version. The SDK is a `py3-none-any` wheel; the wheel-only runtime package contains exactly one exe and uses one of `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, or `py3-none-macosx_11_0_arm64`. Its Hatch hook rejects sdists, universal tags, mixed executable payloads, and unsupported platforms. +[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) reads the authoritative stable `X.Y.Z` from the repository root `package.json` and stages both packages at that version, with the SDK depending exactly on `deepseek-harness-runtime-bin==X.Y.Z`. An optional `python-vX.Y.Z` release tag is a consistency assertion and is rejected when it differs from the repository version; the source `pyproject.toml` development sentinel never determines a release version. The SDK is a `py3-none-any` wheel; each wheel-only runtime package contains one exe, and the macOS wheel also contains its architecture-matched helper. Runtime wheels use one of `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, or `py3-none-macosx_11_0_arm64`; the Hatch hook rejects sdists, universal tags, mixed-platform payloads, missing or extra helpers, and unsupported platforms. The exe's "must be explicitly configured" hard semantic is unchanged; the zero-config experience is restored by the wrapper: when the caller gave no `cordis`, named no explicit runtime, and the environment has no `DSH_CORDIS_CONFIG`, the client explicitly injects the checked-in default `cordis.yml` (agent-core + preloaded llm-deepseek + JSONL persistence + bash-local + the `dsh-jsonrpc` serving entry, with `!!js` environment-variable fallbacks) via `DSH_CORDIS_CONFIG`. @@ -62,7 +62,7 @@ The exe's "must be explicitly configured" hard semantic is unchanged; the zero-c ## 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 completes a turn against a mock endpoint through the default SDK path, a custom config, and the direct binary protocol, with final text and JSONL checked. The custom config additionally drives `run_code` and a zero-agent `workflow` through their real worker files inside the packaged VFS. The same build leg runs 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 comparison normalizes the SDK result and notification stream plus the parent and two child JSONL logs. This harness stays separate from ACP's `pnpm run test:snapshot` because the protocols and build artifacts differ. The platform wheel is then installed in a clean venv and run without `runtime_bin`. +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 completes a turn against a mock endpoint through the default SDK path, a custom config, and the direct binary protocol, with final text and JSONL checked. The custom config additionally drives `run_code` and a zero-agent `workflow` through their real worker files inside the packaged VFS. The same build leg runs 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 IDs in the SDK result and notification stream plus the parent and two child JSONL logs. This harness stays separate from ACP's `pnpm run test:snapshot` because the protocols and build artifacts differ. The platform wheel is then installed in a clean venv and run without `runtime_bin`. 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 e1a21c4064..2b511573bc 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 @@ -40,15 +40,15 @@ exe 的 VFS 内是**构建产物形态的真实包树**(各包的 `lib/` + 真 ### 构建管线与产物 -[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-jsonrpc-agent-pkg deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → 注入 pkg 配置(`bin` 指向闭包内的 `node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js`;`assets` 使用全量 glob,因为动态 `import()` 对 pkg 静态分析不可见,必须显式打入全部内容)→ 每个构建目标调用一次 `pkg --sea` → 可执行文件 `dsh-jsonrpc-agent-pkg--` 写入 `dist-exe/`,并拷回运行时目录。CI 将这些文件作为测试中间输入,只保留对应平台的 wheel 包。四个部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 产出无符号链接的文件树(对 pkg VFS 最稳定,并从物理上保证只有一个 Cordis 实例);关闭对等依赖自动安装可避免未发布包名触发注册表解析;`link-workspace-packages` 让闭包指向工作区/vendor 源码。 +[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-jsonrpc-agent-pkg deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → 注入 pkg 配置(`bin` 指向闭包内的 `node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js`;`assets` 使用全量 glob,因为动态 `import()` 对 pkg 静态分析不可见,必须显式打入全部内容)→ 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 可执行文件 `dsh-jsonrpc-agent-pkg--` 写入 `dist-exe/`,并拷回运行时目录。Linux 安装会从源码构建 `pty.node`,而 `--legacy` 部署会省略该副作用目录,因此构建器会把它从根安装目录复制到暂存闭包;macOS 使用对应目标的预构建产物,并在可执行文件旁生成所需的 `-spawn-helper`。CI 将这些产物作为测试中间输入,只保留对应平台的 wheel 包。四个部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 产出无符号链接的文件树(对 pkg VFS 最稳定,并从物理上保证只有一个 Cordis 实例);关闭对等依赖自动安装可避免未发布包名触发注册表解析;`link-workspace-packages` 让闭包指向工作区/vendor 源码。 CI 使用 [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml),且只允许显式触发:手动派发 `workflow_dispatch`,或给 PR 添加 `build-exe` 标签。linux-x64、linux-arm64(`ubuntu-24.04-arm`)和 macos-arm64 三个平台分别进行原生构建,并缓存 `~/.pkg-cache`;macOS 的 ad-hoc 签名由 pkg 处理。每个平台都使用模拟 SSE 模型,分别通过默认配置和自定义 `cordis.yml` 驱动 SDK,再通过 NDJSON JSON-RPC 直接驱动 exe,校验 JSONL 与最终响应;最后把发布形态的 wheel 包安装到干净的 venv 中,并在不传 `runtime_bin` 的情况下运行。Linux 还会检查 GLIBC 依赖,并在 manylinux 2.28 容器中运行。完整构建三个目标时保留 4 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 3 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-vX.Y.Z` 标签流水线,构建一个 SDK wheel 包和 3 个原生运行时 wheel 包,再由单个串行任务校验并将这 4 个文件发布到项目的 PyPI 注册表。Windows 不在目标范围内。 ### Python SDK 分发:双载体,exe 用于生产,`node` 用于开发 -Python SDK 位于 [`python/`](../../../../python/README.md):`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含三类内容:检入的默认 `runtime/cordis.yml`、构建注入的平台 exe,以及构建注入的 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 的自动解析**只查找 exe**;`node` 载体仅在显式设置 `DSH_RUNTIME_MODE=node` 时启用(运行 `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js`,需要系统 Node ≥22.19),定位为本仓库成员的开发验证通道,不随 wheel 包分发。 +Python SDK 位于 [`python/`](../../../../python/README.md):`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含检入的默认 `runtime/cordis.yml`、构建注入的平台 exe 与可选 helper,以及构建注入的 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 的自动解析**只查找 exe**;`node` 载体仅在显式设置 `DSH_RUNTIME_MODE=node` 时启用(运行 `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js`,需要系统 Node ≥22.19),定位为本仓库成员的开发验证通道,不随 wheel 包分发。 -[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) 从仓库根目录的 `package.json` 读取权威的稳定版本 `X.Y.Z`,以该版本暂存两个包,并让 SDK 精确依赖 `deepseek-harness-runtime-bin==X.Y.Z`。可选的 `python-vX.Y.Z` 发布标签只是一项一致性断言,与仓库版本不同时会被拒绝;源码 `pyproject.toml` 中的开发占位版本从不决定发布版本。SDK 是 `py3-none-any` wheel 包;只提供 wheel 包的运行时包恰好包含一个 exe,标签为 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64` 或 `py3-none-macosx_11_0_arm64`。其 Hatch 钩子拒绝 sdist、通用标签、混合可执行载荷以及不支持的平台。 +[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) 从仓库根目录的 `package.json` 读取权威的稳定版本 `X.Y.Z`,以该版本暂存两个包,并让 SDK 精确依赖 `deepseek-harness-runtime-bin==X.Y.Z`。可选的 `python-vX.Y.Z` 发布标签只是一项一致性断言,与仓库版本不同时会被拒绝;源码 `pyproject.toml` 中的开发占位版本从不决定发布版本。SDK 是 `py3-none-any` wheel 包;每个只提供 wheel 包的运行时包都包含一个 exe,macOS wheel 包还包含与其架构匹配的 helper。运行时 wheel 包使用 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64` 或 `py3-none-macosx_11_0_arm64` 三种标签之一;Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、helper 缺失或多余,以及不支持的平台。 exe“必须显式配置”的硬语义不变;零配置体验由包装层恢复:调用方没有提供 `cordis`、没有显式指定运行时,且环境中没有 `DSH_CORDIS_CONFIG` 时,客户端将检入的默认 `cordis.yml`(`agent-core` + 预载的 `llm-deepseek` + JSONL 持久化 + `bash-local` + `dsh-jsonrpc` 对外服务条目,并通过 `!!js` 使用环境变量兜底)显式注入 `DSH_CORDIS_CONFIG`。 @@ -62,7 +62,7 @@ exe 内支持 `dsh-workflow-workerthread` 与 `dsh-code-runtime-worker`。两个 ## 测试 -验证面分三层。机制层:`--sea` 链路的实测结论内嵌在“决策”各节(VFS 内 ESM 动态 `import()`、单一 Cordis 实例、明确报错的配置链路、`node:sqlite`、macOS ad-hoc 签名可运行)。SDK 层:完整的无密钥 pytest 套件以假运行时对端覆盖客户端协议、子进程清理、绝对 `cwd` 传递、双载体启动与载体解析;根 CI 在 Python 3.10 上运行全部用例。端到端层:每个平台构建都通过默认 SDK 路径、自定义配置和直接二进制协议,对模拟端点完成一个轮次,并校验最终文本与 JSONL。自定义配置还会通过打包进 VFS 的真实工作线程文件执行 `run_code` 和不启动 agent 的 `workflow`。同一构建任务还会经 Python SDK 运行一组检入的 exe 专用快照:无密钥脚本化模型挂载一个会注册工具的 Cordis 插件,从 `run_code` 调用该工具,运行一个由 spawn 提供方直接启动的 subagent(子 agent)和一个会通过 spawn 启动第二个子 agent 的工作流,随后卸载该插件。比较时会规范化 SDK 结果与通知流,以及父会话和两个子会话的 JSONL 日志。该 harness 与 ACP 的 `pnpm run test:snapshot` 保持独立,因为二者的协议和构建产物不同。随后把平台 wheel 包安装进干净的 venv,并在不传 `runtime_bin` 的情况下运行。 +验证面分三层。机制层:`--sea` 链路的实测结论内嵌在“决策”各节(VFS 内 ESM 动态 `import()`、单一 Cordis 实例、明确报错的配置链路、`node:sqlite`、macOS ad-hoc 签名可运行)。SDK 层:完整的无密钥 pytest 套件以假运行时对端覆盖客户端协议、子进程清理、绝对 `cwd` 传递、双载体启动与载体解析;根 CI 在 Python 3.10 上运行全部用例。端到端层:每个平台构建都通过默认 SDK 路径、自定义配置和直接二进制协议,对模拟端点完成一个轮次,并校验最终文本与 JSONL。自定义配置还会通过打包进 VFS 的真实工作线程文件执行 `run_code` 和不启动 agent 的 `workflow`。同一构建任务还会经 Python SDK 运行一组检入的 exe 专用快照:无密钥脚本化模型挂载一个会注册工具的 Cordis 插件,从 `run_code` 调用该工具,运行一个由 spawn 提供方直接启动的 subagent(子 agent)和一个会通过 spawn 启动第二个子 agent 的工作流,随后卸载该插件。该 fixture(测试前置数据)会显式禁用组合包中未使用的 Bash 和本地 skill(技能)发现,使其工具集不依赖仓库外部状态;比较时会规范化以下各处的不透明消息 ID:SDK 结果与通知流,以及父会话和两个子会话的 JSONL 日志。该 harness 与 ACP 的 `pnpm run test:snapshot` 保持独立,因为二者的协议和构建产物不同。随后把平台 wheel 包安装进干净的 venv,并在不传 `runtime_bin` 的情况下运行。 手工驱动注意:`bin` 将 stdin EOF 视为“客户端已离开”并立即 dispose,短命管道会中止进行中的轮次——管道驱动必须保持 stdin 打开,直到轮次结束。 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 e3b6121e19..a16f07a63c 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: 286a53c1c686cc515b65119ed4b1a01a57b0614b -2026-07-29-persistent-bash-str-replace-editor.zh.md: d2417708c8a1334e9f8930481f4218cbefc5a87b +2026-07-29-persistent-bash-str-replace-editor.md: a97af750bdd80ddf38dc2d126e70c245ed035f35 +2026-07-29-persistent-bash-str-replace-editor.zh.md: 0c2ab26693d90c91d5c41a128ebb77a3c6cc2e7f 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 286a53c1c6..a97af750bd 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 @@ -10,13 +10,13 @@ Some deployments need a one-call Bash schema whose shell state survives across m ## Decision -`@deepseek-ai/dsh-tool-bash-persistent` consumes `ctx.pty` and registers one `bash(command)` tool. It lazily creates one interactive shell per exact Agent and serializes that owner's calls. Cwd, exported variables, activated environments, functions, and background jobs persist. Random private markers delimit command output. Retained scrollback is paged backward to recover the command's original prefix; a dropped prefix is reported explicitly. Timeout or cancellation closes the shell before another call can reuse uncertain state, and model-visible timeout/exit results disclose that reset. The configurable description defaults to persistence facts only, so network and package-mirror claims remain deployment-owned. +`@deepseek-ai/dsh-tool-bash-persistent` consumes `ctx.pty` and registers one `bash(command)` tool. It lazily creates one interactive shell per exact Agent and serializes that owner's calls. Cwd, exported variables, activated environments, functions, and background jobs persist. Random private markers delimit command output. Retained scrollback is paged backward to recover the command's original prefix; a dropped prefix is reported explicitly. A nonzero wrapped command appends `[exit code: N]`; a shell that dies before reporting that status instead appends `[shell exited: code N]`, `[shell killed by signal: SIG]`, or `[shell exited]` when the backend supplies neither. `maxOutputChars` bounds retained command output, while fixed diagnostics can extend the returned string. Timeout or cancellation closes the shell before another call can reuse uncertain state, and model-visible timeout/exit results disclose that reset. Cancellation always resets and discards the result, even when a complete status marker is already observable, so state changes the model never saw cannot survive. The configurable description defaults to persistence facts only, so network and package-mirror claims remain deployment-owned. -`@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. The public schema and failures use only `old_str`; canonical mode requires absolute paths and expands tabs before mutations. Deployments with an intentional session-cwd contract can disable the absolute-path requirement. The plugin can compose with persistent Bash, one-shot Bash, sandboxed Bash, or no shell. +`@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`. The plugin can compose with persistent Bash, one-shot Bash, sandboxed Bash, or no shell. `dsh-system-prompt` accepts `includeHarnessIdentity: false`, while `dsh-agent-spine-demo` forwards that setting and accepts `toolBash: false`. A deployment can therefore own an exact persona and replace the spine's native Bash 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`, each packaged runtime executable ships with an architecture-matched `-spawn-helper` sibling. A pinned `node-pty` patch resolves that sibling only when present (or when `DSH_NODE_PTY_SPAWN_HELPER` explicitly selects one), preserving upstream lookup in ordinary Node runs; the executable and runtime-wheel builders fail before publication when the helper is absent, mismatched, 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. ## Alternatives considered @@ -30,4 +30,4 @@ Both plugins are included in the Python runtime closure. The persistent Bash clo ## 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. Runtime-wheel consumers still need no Node installation, but the wheel now contains a main executable plus its private native helper rather than one physical file. +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. 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 d2417708c8..0c2ab26693 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 @@ -10,13 +10,13 @@ ## 决策 -`@deepseek-ai/dsh-tool-bash-persistent` 消费 `ctx.pty` 并注册一个 `bash(command)` 工具。它为每个精确 Agent 惰性创建一个交互式 shell,并串行化该所有者的调用。Cwd、导出的变量、已激活环境、函数和后台任务会保留。随机私有标记划分命令输出;保留的 scrollback 会向前分页,以恢复命令真正的输出前缀,若前缀已被丢弃则明确告知。超时或取消会先关闭 shell,避免下一次调用复用状态不确定的会话,模型可见的超时/退出结果也会说明该重置。可配置描述默认只声明持久性事实,因此网络和软件包镜像等声明仍归部署所有。 +`@deepseek-ai/dsh-tool-bash-persistent` 消费 `ctx.pty` 并注册一个 `bash(command)` 工具。它为每个精确 Agent 惰性创建一个交互式 shell,并串行化该所有者的调用。Cwd、导出的变量、已激活环境、函数和后台任务会保留。随机私有标记划分命令输出;保留的 scrollback 会向前分页,以恢复命令真正的输出前缀,若前缀已被丢弃则明确告知。经封装的命令以非零状态结束时,会追加 `[exit code: N]`;若 shell 在报告该状态前终止,则改为追加 `[shell exited: code N]`、`[shell killed by signal: SIG]`,或在后端既未提供退出码也未提供信号时追加 `[shell exited]`。`maxOutputChars` 限制保留的命令输出,而固定诊断可能使返回字符串更长。超时或取消会先关闭 shell,避免下一次调用复用状态不确定的会话,模型可见的超时/退出结果也会说明该重置。取消始终会重置 shell 并丢弃结果,即使已经能观察到完整状态标记也是如此,从而不会让模型未曾看到的状态变更得以保留。可配置描述默认只声明持久性事实,因此网络和软件包镜像等声明仍归部署所有。 -`@deepseek-ai/dsh-tool-str-replace-editor` 独立消费 `ctx.fs`,注册包含 `view`、`create`、`str_replace` 与 `insert` 的 `str_replace_editor`。它提供带行号文本查看、过滤后的两层目录列表、唯一字面量替换、规范插入边界和有界输出。公开 schema 与错误只使用 `old_str`;规范模式要求绝对路径,并在变更前展开制表符。有明确 session-cwd 契约的部署可以关闭绝对路径要求。它可以与持久 Bash、一次性 Bash、沙箱 Bash 或无 shell 组合。 +`@deepseek-ai/dsh-tool-str-replace-editor` 独立消费 `ctx.fs`,注册包含 `view`、`create`、`str_replace` 与 `insert` 的 `str_replace_editor`。它提供带行号文本查看、过滤后的两层目录列表、唯一字面量替换、规范插入边界和有界输出。路径必须为绝对路径;文件查看会保留内容中的制表符,因此复制的文本仍可作为有效的字面量替换输入;变更会保留请求编辑范围之外的制表符;公开 schema 与错误则只使用 `old_str`。它可以与持久 Bash、一次性 Bash、沙箱 Bash 或无 shell 组合。 `dsh-system-prompt` 接受 `includeHarnessIdentity: false`;`dsh-agent-spine-demo` 会转发该设置,并接受 `toolBash: false`。因此部署可以拥有精确 persona,并替换 spine 的原生 Bash,而不会重复注册提示词或工具。既有默认值不变。 -两个插件都进入 Python runtime 闭包。持久 Bash 的闭包还包含 PTY 服务/本地后端,以及该后端要求的沙箱服务。由于 `node-pty` 会执行原生 `spawn-helper`,每个打包后的运行时可执行文件都会携带一个架构匹配的 `-spawn-helper` 伴随文件。固定版本的 `node-pty` 补丁只在该伴随文件存在时解析它(也可由 `DSH_NODE_PTY_SPAWN_HELPER` 显式指定),普通 Node 运行仍保留上游查找方式;若 helper 缺失、架构不匹配或不可执行,可执行文件与 runtime wheel 构建会在发布前失败。 +两个插件都进入 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 构建器会在发布前失败。 ## 考虑过的替代方案 @@ -30,4 +30,4 @@ ## 后果 -Profile 可以通过配置 persona 和描述复现外部 Agent,而底层包保持通用。持久 Bash 需要拥有它的 Agent 与真实 PTY 后端;shell 退出、超时或取消会丢失状态。编辑器把安全与变更策略委托给挂载的文件系统栈。runtime wheel 的使用者仍不需要安装 Node,但 wheel 现在包含主可执行文件及其私有原生 helper,而不是单个物理文件。 +Profile 可以通过配置 persona 和描述复现外部 Agent,而底层包保持通用。持久 Bash 需要拥有它的 Agent 与真实 PTY 后端;shell 退出、超时或取消会丢失状态。编辑器把安全与变更策略委托给挂载的文件系统栈。运行时 wheel 包的消费方仍无需安装 Node;Linux wheel 包包含一个可执行文件,macOS wheel 包还包含其私有原生 helper。 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 2e6ffaf3d8..4aca3810c8 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -1593,7 +1593,7 @@ export interface Config { } ``` -Source: [`packages/pty/tool-bash-persistent/src/index.ts:373`](../packages/pty/tool-bash-persistent/src/index.ts) +Source: [`packages/pty/tool-bash-persistent/src/index.ts:405`](../packages/pty/tool-bash-persistent/src/index.ts) ## `@deepseek-ai/dsh-tool-cordis` @@ -1764,14 +1764,10 @@ export interface Config { maxOutputChars?: number /** Model-facing tool description. */ description?: string - /** Require local absolute paths like the canonical editor contract (default true). */ - requireAbsolutePath?: boolean - /** Expand tabs across the full file before each mutation, matching the canonical editor (default true). */ - expandTabsOnMutation?: boolean } ``` -Source: [`packages/fs/tool-str-replace-editor/src/index.ts:539`](../packages/fs/tool-str-replace-editor/src/index.ts) +Source: [`packages/fs/tool-str-replace-editor/src/index.ts:496`](../packages/fs/tool-str-replace-editor/src/index.ts) ## `@deepseek-ai/dsh-tool-subagent` diff --git a/examples/jsonrpc-agent/README.i18n.yaml b/examples/jsonrpc-agent/README.i18n.yaml index 1ff308b60f..bc0cd4facd 100644 --- a/examples/jsonrpc-agent/README.i18n.yaml +++ b/examples/jsonrpc-agent/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write examples/jsonrpc-agent/README.md -README.md: 9a4c715e8988f52b647dbbd6b14a478cc1357d92 -README.zh.md: fa792f9cf6bdad4f32a980f478a131f19de97611 +README.md: 5e7b7d79415a4af0b0d16c61b0a4590dce57e545 +README.zh.md: 8f2cca582853d420a542606c8a0f3e534e9c9bec diff --git a/examples/jsonrpc-agent/README.md b/examples/jsonrpc-agent/README.md index 9a4c715e89..5e7b7d7941 100644 --- a/examples/jsonrpc-agent/README.md +++ b/examples/jsonrpc-agent/README.md @@ -33,8 +33,8 @@ Pass the config path through the Python SDK's `cordis` option or `DSH_CORDIS_CON - owner-scoped persistent `bash` - `str_replace_editor` with `view`, `create`, `str_replace`, and `insert` -It composes the real local PTY, filesystem intent policy, and session sandbox policy. The keyless behavior snapshot drives the shipped JSON-RPC runtime through both tools and proves that shell cwd/environment survive across calls: +It composes the real local PTY, filesystem intent policy, and session sandbox policy. The keyless SDK snapshot drives the shipped JSON-RPC runtime through both tools, proves that shell cwd/environment survive across calls, and pins the notification stream, turn result, and persisted JSONL: ```bash -pnpm exec vitest run examples/jsonrpc-agent/tests/persistent-tools.snapshot.spec.ts +pnpm exec vitest run --config vitest.snapshot.config.ts -t persistent-tools ``` diff --git a/examples/jsonrpc-agent/README.zh.md b/examples/jsonrpc-agent/README.zh.md index fa792f9cf6..8f2cca5828 100644 --- a/examples/jsonrpc-agent/README.zh.md +++ b/examples/jsonrpc-agent/README.zh.md @@ -33,8 +33,8 @@ - agent 独占、状态持久的 `bash` - 提供 `view`、`create`、`str_replace` 与 `insert` 的 `str_replace_editor` -它组合真实本地 PTY、文件系统 intent 策略与 session 沙箱策略。无密钥行为快照会通过正式 JSON-RPC runtime 驱动这两个工具,并验证 shell 的 cwd 与环境变量能跨调用保留: +它组合真实本地 PTY、文件系统 intent 策略与 session 沙箱策略。无密钥 SDK 快照会通过正式 JSON-RPC runtime 驱动这两个工具,验证 shell 的 cwd 与环境变量能跨调用保留,并锁定通知流、轮次结果与已持久化的 JSONL: ```bash -pnpm exec vitest run examples/jsonrpc-agent/tests/persistent-tools.snapshot.spec.ts +pnpm exec vitest run --config vitest.snapshot.config.ts -t persistent-tools ``` diff --git a/examples/jsonrpc-agent/persistent-tools.snapshot.cordis.yml b/examples/jsonrpc-agent/persistent-tools.snapshot.cordis.yml new file mode 100644 index 0000000000..5bda5ac6a5 --- /dev/null +++ b/examples/jsonrpc-agent/persistent-tools.snapshot.cordis.yml @@ -0,0 +1,19 @@ +# Keyless replay keeps the persistent-tool composition intact and replaces +# only its live DeepSeek adapter with the fixture-backed provider. +- id: base + name: '@cordisjs/plugin-include' + config: + path: ./persistent-tools.cordis.yml + patches: + - 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 + name: DeepSeek + models: + - id: deepseek-v4-flash diff --git a/examples/jsonrpc-agent/tests/persistent-tools.snapshot.spec.ts b/examples/jsonrpc-agent/tests/persistent-tools.snapshot.spec.ts deleted file mode 100644 index 18d1454837..0000000000 --- a/examples/jsonrpc-agent/tests/persistent-tools.snapshot.spec.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { createServer } from 'node:http' -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 { describe, expect, it } from 'vitest' -import { resolveExampleLaunch } from '@deepseek-ai/dsh-loader-smoke' -import { DeepSeekHarness } from '@deepseek-ai/dsh-sdk-client' - -const repoRoot = fileURLToPath(new URL('../../..', import.meta.url)) -const configPath = fileURLToPath(new URL('../persistent-tools.cordis.yml', import.meta.url)) -const runtimeBin = fileURLToPath(new URL('../../../packages/examples/jsonrpc-demo/src/bin.ts', import.meta.url)) -const repoTsconfig = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)) -const expectedPath = fileURLToPath(new URL('./snapshots/persistent-tools/behavior.expected.json', import.meta.url)) - -interface ModelRequest { - messages?: Array> - tools?: Array<{ function?: { name?: string; parameters?: { required?: string[] } } }> -} - -function sseToolCall(id: string, name: string, args: Record): string[] { - return [ - 'data: {"choices":[{"delta":{"role":"assistant","content":null}}]}\n\n', - `data: ${JSON.stringify({ - choices: [{ - delta: { - tool_calls: [{ - index: 0, - id, - type: 'function', - function: { name, arguments: JSON.stringify(args) }, - }], - }, - }], - })}\n\n`, - 'data: {"choices":[{"delta":{},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":3,"completion_tokens":3}}\n\n', - 'data: [DONE]\n\n', - ] -} - -function sseText(text: string): string[] { - return [ - 'data: {"choices":[{"delta":{"role":"assistant","content":null}}]}\n\n', - `data: ${JSON.stringify({ choices: [{ delta: { content: text } }] })}\n\n`, - 'data: {"choices":[{"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":3}}\n\n', - 'data: [DONE]\n\n', - ] -} - -function messageText(content: unknown): string { - if (typeof content === 'string') return content - if (!Array.isArray(content)) return '' - return content.flatMap((block) => { - if (typeof block !== 'object' || block === null) return [] - const text = (block as { text?: unknown }).text - return typeof text === 'string' ? [text] : [] - }).join('') -} - -function latestToolCall(messages: Array>): { id: string; name: string } { - for (const message of messages.toReversed()) { - const calls = message.tool_calls - if (!Array.isArray(calls)) continue - const call = (calls as unknown[]).at(-1) - if (typeof call !== 'object' || call === null) continue - const id = (call as { id?: unknown }).id - const fn = (call as { function?: { name?: unknown } }).function - if (typeof id === 'string' && typeof fn?.name === 'string') return { id, name: fn.name } - } - throw new Error('model request has no preceding tool call') -} - -function normalize(value: string, cwd: string): string { - return value.replaceAll(cwd, '{{cwd}}') -} - -describe('jsonrpc persistent tools snapshot', () => { - it('runs persistent shell state and editor mutations keylessly', async () => { - const cwd = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-persistent-tools-')) - const sessionRoot = join(cwd, '.sessions') - const target = join(cwd, 'note.txt') - const requests: ModelRequest[] = [] - const modelServer = createServer((request, response) => { - let body = '' - request.setEncoding('utf8') - request.on('data', (chunk: string) => { body += chunk }) - request.on('end', () => { - const parsed = JSON.parse(body) as ModelRequest - requests.push(parsed) - const messages = parsed.messages ?? [] - const latest = messages.at(-1) - if (latest === undefined) throw new Error('model request has no messages') - let chunks: string[] - if (latest.role !== 'tool') { - chunks = sseToolCall('bash-1', 'bash', { - command: 'cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf "COUNT=%s CWD=%s\\n" "$DSH_EXAMPLE_COUNT" "$PWD"', - }) - } else { - const call = latestToolCall(messages) - const toolText = messageText(latest.content) - if (call.id === 'bash-1') { - expect(toolText).toContain('COUNT=1 CWD=/tmp') - chunks = sseToolCall('bash-2', 'bash', { - command: 'DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf "COUNT=%s CWD=%s\\n" "$DSH_EXAMPLE_COUNT" "$PWD"', - }) - } else if (call.id === 'bash-2') { - expect(toolText).toContain('COUNT=2 CWD=/tmp') - chunks = sseToolCall('editor-create', 'str_replace_editor', { - command: 'create', - path: target, - file_text: 'alpha\n', - }) - } else if (call.id === 'editor-create') { - expect(toolText).toContain('New file created successfully') - chunks = sseToolCall('editor-replace', 'str_replace_editor', { - command: 'str_replace', - path: target, - old_str: 'alpha', - new_str: 'beta', - }) - } else if (call.id === 'editor-replace') { - expect(toolText).toContain('has been edited successfully') - chunks = sseText('PERSISTENT_TOOLS_OK') - } else { - throw new Error(`unexpected tool call ${call.id}`) - } - } - response.writeHead(200, { 'content-type': 'text/event-stream' }) - for (const chunk of chunks) response.write(chunk) - response.end() - }) - }) - await new Promise(resolve => modelServer.listen(0, '127.0.0.1', resolve)) - const address = modelServer.address() - if (address === null || typeof address === 'string') throw new Error('model server did not bind') - const launch = resolveExampleLaunch({ - srcBin: runtimeBin, - configArgs: [], - tsconfigPath: repoTsconfig, - }) - const harness = new DeepSeekHarness({ - launch: { - command: launch.command, - args: launch.args, - cwd: repoRoot, - env: { - ...Object.fromEntries(Object.entries(process.env).filter(([, value]) => value !== undefined)) as Record, - ...Object.fromEntries(Object.entries(launch.env).filter(([, value]) => value !== undefined)) as Record, - DSH_CORDIS_CONFIG: configPath, - DSH_CWD: cwd, - DSH_SESSION_ROOT: sessionRoot, - DEEPSEEK_API_KEY: 'keyless-local-mock', - DEEPSEEK_BASE_URL: `http://127.0.0.1:${address.port}`, - NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '), - }, - requestTimeoutMs: 60_000, - }, - cwd, - provider: 'deepseek', - model: 'deepseek-v4-flash', - }) - - try { - const result = await harness.run( - 'Prove that bash state persists, then create and edit note.txt.', - { sessionId: 'persistent-tools-snapshot' }, - ) - const calls = result.events.flatMap((event) => { - if (event.type !== 'tool/call') return [] - return [{ - name: event.data.name, - arguments: normalize(event.data.arguments, cwd), - }] - }) - const results = result.events.flatMap((event) => { - if (event.type !== 'tool/result') return [] - return event.data.message.content.flatMap((block) => { - if (block.type !== 'tool-result') return [] - return block.content.flatMap(content => - content.type === 'text' - ? [{ text: normalize(content.text, cwd) }] - : []) - }) - }) - const tools = (requests[0]?.tools ?? []).map(tool => ({ - name: tool.function?.name, - required: tool.function?.parameters?.required ?? [], - })).sort((left, right) => { - const leftName = String(left.name) - const rightName = String(right.name) - return leftName < rightName ? -1 : leftName > rightName ? 1 : 0 - }) - const behavior = { - tools, - calls, - results, - final: { - status: result.status, - reason: result.reason, - response: result.finalResponse, - file: await readFile(target, 'utf8'), - }, - } - if (process.env.DSH_SNAPSHOT === 'refresh') { - await writeFile(expectedPath, `${JSON.stringify(behavior, null, 2)}\n`) - } - expect(behavior).toEqual(JSON.parse(await readFile(expectedPath, 'utf8'))) - } finally { - await harness.close() - await new Promise(resolve => modelServer.close(() => { resolve() })) - await rm(cwd, { recursive: true, force: true }) - } - }, 75_000) -}) diff --git a/examples/jsonrpc-agent/tests/sdk.snapshot.ts b/examples/jsonrpc-agent/tests/sdk.snapshot.ts index c54812e3e5..11c7615c48 100644 --- a/examples/jsonrpc-agent/tests/sdk.snapshot.ts +++ b/examples/jsonrpc-agent/tests/sdk.snapshot.ts @@ -11,7 +11,7 @@ import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' -import { delimiter, join } from 'node:path' +import { basename, delimiter, join } from 'node:path' import { fileURLToPath } from 'node:url' import { describe, expect, it } from 'vitest' import { @@ -31,6 +31,8 @@ const testsDir = dirOf(import.meta.url) const snapshotsDir = join(testsDir, 'snapshots') const liveConfig = join(testsDir, '..', 'cordis.yml') const replayConfig = join(testsDir, '..', 'cordis.snapshot.yml') +const persistentToolsLiveConfig = join(testsDir, '..', 'persistent-tools.cordis.yml') +const persistentToolsReplayConfig = join(testsDir, '..', 'persistent-tools.snapshot.cordis.yml') const runtimeBin = fileURLToPath(new URL('../../../packages/examples/jsonrpc-demo/src/bin.ts', import.meta.url)) const repoTsconfig = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)) @@ -51,6 +53,12 @@ interface SdkScenario { sessionId: string /** How many child sessions the turn persists (subagent scenarios). */ children: number + /** Optional scenario-specific live and replay compositions. */ + configs?: { live: string; replay: string } + /** Cwd-relative files whose final contents are part of the scenario contract. */ + expectedFiles?: Readonly> + /** Assembled model-facing tool names and required argument keys. */ + expectedTools?: Readonly> } const SCENARIOS: SdkScenario[] = [ @@ -72,6 +80,15 @@ const SCENARIOS: SdkScenario[] = [ sessionId: 'sdk-snapshot-subagent', children: 1, }, + { + name: 'persistent-tools', + prompt: 'Prove that bash state persists. 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.', + sessionId: 'persistent-tools-snapshot', + children: 0, + configs: { live: persistentToolsLiveConfig, replay: persistentToolsReplayConfig }, + expectedFiles: { 'note.txt': 'target:\n\tnew\n' }, + expectedTools: { bash: ['command'], str_replace_editor: ['command', 'path'] }, + }, ] interface PersistedLog { @@ -80,6 +97,10 @@ interface PersistedLog { readonly header: Record } +interface MissingFile { + readonly missing: true +} + async function jsonlFiles(dir: string): Promise { const entries = await readdir(dir, { recursive: true }) return entries.filter(entry => entry.endsWith('.jsonl')).map(entry => join(dir, entry)).sort() @@ -94,6 +115,20 @@ async function persistedLogs(sessionsRoot: string): Promise { })) } +interface LoggedRequestHeader { + type?: string + data?: { header?: { tools?: Array<{ name: string; parameters: { required?: string[] } }> } } +} + +function assembledToolRequirements(log: PersistedLog): Record { + const event = log.content.trimEnd().split('\n') + .map(line => JSON.parse(line) as LoggedRequestHeader) + .find(candidate => candidate.type === 'request/header') + const tools = event?.data?.header?.tools + if (tools === undefined) throw new Error('session log has no request/header tools') + return Object.fromEntries(tools.map(tool => [tool.name, tool.parameters.required ?? []])) +} + function contextOf(logs: readonly { content: string; header: Record }[], cwd: string): NormalizeContext { return { sessionIds: logs.flatMap(log => typeof log.header.id === 'string' ? [log.header.id] : []), @@ -109,6 +144,25 @@ function contextOfContents(contents: readonly string[]): NormalizeContext { } } +async function hydrateReplayFixtures(scenario: SdkScenario, cwd: string): Promise { + const root = join(cwd, '.replay-fixtures') + await mkdir(root, { recursive: true }) + return Promise.all(fixtureFiles(scenario).map(async (source) => { + const destination = join(root, basename(source)) + await writeFile(destination, (await readFile(source, 'utf8')).replaceAll('{{cwd}}', cwd)) + return destination + })) +} + +async function readExpectedFile(path: string): Promise { + try { + return await readFile(path, 'utf8') + } catch (error: unknown) { + if (error instanceof Error && (error as NodeJS.ErrnoException).code === 'ENOENT') return { missing: true } + throw error + } +} + /** * Normalize the SDK-visible notification stream: embedded `session.event` * envelopes get the session-log treatment (times zeroed, headers tokenized), @@ -147,30 +201,30 @@ async function runScenario(scenario: SdkScenario): Promise<{ result: TurnResult notifications: HarnessNotification[] logs: PersistedLog[] + observedFiles: Record cwd: string }> { const cwd = await mkdtemp(join(tmpdir(), `sdk-snapshot-${scenario.name}-`)) const sessionsRoot = join(cwd, '.sessions') - const scenarioDir = join(snapshotsDir, scenario.name) + const replayFixtures = recording ? [] : await hydrateReplayFixtures(scenario, cwd) const launch = resolveExampleLaunch({ srcBin: runtimeBin, configArgs: [], tsconfigPath: repoTsconfig, }) - const childFixtures = Array.from( - { length: scenario.children }, - (_, index) => join(scenarioDir, `session.${index + 1}.jsonl`), - ) + const [parentFixture, ...childFixtures] = replayFixtures const env: Record = { ...Object.fromEntries(Object.entries(process.env).filter(([, value]) => value !== undefined)) as Record, ...Object.fromEntries(Object.entries(launch.env).filter(([, value]) => value !== undefined)) as Record, - DSH_CORDIS_CONFIG: recording ? liveConfig : replayConfig, + DSH_CORDIS_CONFIG: recording + ? scenario.configs?.live ?? liveConfig + : scenario.configs?.replay ?? replayConfig, DSH_SESSION_ROOT: sessionsRoot, DSH_CWD: cwd, DSH_SNAPSHOT: mode, NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '), - ...recording ? {} : { - DSH_SNAPSHOT_FILE: join(scenarioDir, 'session.jsonl'), + ...parentFixture === undefined ? {} : { + DSH_SNAPSHOT_FILE: parentFixture, ...childFixtures.length > 0 ? { DSH_SNAPSHOT_CHILD_FILES: childFixtures.join(delimiter) } : {}, }, } @@ -189,13 +243,19 @@ async function runScenario(scenario: SdkScenario): Promise<{ }) try { const notifications: HarnessNotification[] = [] - const result = await harness.run(scenario.prompt, { + const result = await harness.run(scenario.prompt.replaceAll('{{cwd}}', cwd), { sessionId: scenario.sessionId, onNotification: (notification) => { notifications.push(notification) }, }) await harness.close() const logs = await persistedLogs(sessionsRoot) - return { result, notifications, logs, cwd } + const observedFiles = Object.fromEntries(await Promise.all( + Object.keys(scenario.expectedFiles ?? {}).map(async (path): Promise<[string, string | MissingFile]> => [ + path, + await readExpectedFile(join(cwd, path)), + ]), + )) + return { result, notifications, logs, observedFiles, cwd } } finally { await harness.close() await rm(cwd, { recursive: true, force: true }) @@ -227,7 +287,7 @@ describe('TypeScript SDK snapshots over the jsonrpc runtime', () => { const notificationsExpectedPath = join(scenarioDir, 'notifications.expected.jsonl') const resultExpectedPath = join(scenarioDir, 'result.expected.json') - const { result, notifications, logs, cwd } = await runScenario(scenario) + const { result, notifications, logs, observedFiles, cwd } = await runScenario(scenario) const ordered = orderLogs(logs, scenario) const actualContext = contextOf(ordered, cwd) @@ -293,6 +353,12 @@ describe('TypeScript SDK snapshots over the jsonrpc runtime', () => { // Wire-shape invariants that must hold in every mode. expect(result.status).toBe('ok') expect(notifications.at(-1)?.method).toBe('session.finished') + expect(observedFiles).toEqual(scenario.expectedFiles ?? {}) + if (scenario.expectedTools !== undefined) { + const parent = ordered[0] + if (parent === undefined) throw new Error(`${scenario.name} has no parent session log`) + expect(assembledToolRequirements(parent)).toEqual(scenario.expectedTools) + } if (scenario.children > 0) { expect(notifications.some(n => n.method === 'subagent.started')).toBe(true) expect(notifications.some(n => n.method === 'subagent.finished')).toBe(true) diff --git a/examples/jsonrpc-agent/tests/snapshots/persistent-tools/behavior.expected.json b/examples/jsonrpc-agent/tests/snapshots/persistent-tools/behavior.expected.json deleted file mode 100644 index 18b54e9312..0000000000 --- a/examples/jsonrpc-agent/tests/snapshots/persistent-tools/behavior.expected.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "tools": [ - { - "name": "bash", - "required": [ - "command" - ] - }, - { - "name": "str_replace_editor", - "required": [ - "command", - "path" - ] - } - ], - "calls": [ - { - "name": "bash", - "arguments": "{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}" - }, - { - "name": "bash", - "arguments": "{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}" - }, - { - "name": "str_replace_editor", - "arguments": "{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"alpha\\n\"}" - }, - { - "name": "str_replace_editor", - "arguments": "{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"alpha\",\"new_str\":\"beta\"}" - } - ], - "results": [ - { - "text": "COUNT=1 CWD=/tmp" - }, - { - "text": "COUNT=2 CWD=/tmp" - }, - { - "text": "New file created successfully at: {{cwd}}/note.txt" - }, - { - "text": "The file {{cwd}}/note.txt has been edited successfully." - } - ], - "final": { - "status": "ok", - "reason": { - "kind": "completed" - }, - "response": "PERSISTENT_TOOLS_OK", - "file": "beta\n" - } -} diff --git a/examples/jsonrpc-agent/tests/snapshots/persistent-tools/notifications.expected.jsonl b/examples/jsonrpc-agent/tests/snapshots/persistent-tools/notifications.expected.jsonl new file mode 100644 index 0000000000..e69b5d95ee --- /dev/null +++ b/examples/jsonrpc-agent/tests/snapshots/persistent-tools/notifications.expected.jsonl @@ -0,0 +1,74 @@ +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/start","seq":0,"time":0,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"Prove that bash state persists. 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":"session/title","seq":2,"time":0,"data":{"title":"Prove that bash state persists.","messageSeqs":[1],"source":{"kind":"fallback"}}}}} +{"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":"request/header","seq":4,"time":0,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":5,"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":6,"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":7,"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":8,"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":9,"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":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","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[5,6,7,8,9],"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":"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/chunk","seq":15,"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":16,"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":17,"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":18,"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":19,"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":20,"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","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[15,16,17,18,19],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":21,"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":22,"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":[21],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":23,"time":0,"data":{"turn":1,"step":2}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":24,"time":0,"data":{"turn":1,"step":3}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":25,"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":26,"time":0,"data":{"turn":1,"step":3,"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\"}"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":27,"time":0,"data":{"turn":1,"step":3,"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\"}"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":28,"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":29,"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":30,"time":0,"data":{"turn":1,"step":3,"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\"}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[25,26,27,28,29],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":31,"time":0,"data":{"turn":1,"step":3,"callId":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\"}"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":32,"time":0,"data":{"turn":1,"step":3,"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":[31],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":33,"time":0,"data":{"turn":1,"step":3}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":34,"time":0,"data":{"turn":1,"step":4}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":35,"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":36,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-view","name":"str_replace_editor","argumentsDelta":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\"}"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":37,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\"}"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":38,"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":39,"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":40,"time":0,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\"}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[35,36,37,38,39],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":41,"time":0,"data":{"turn":1,"step":4,"callId":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\"}"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":42,"time":0,"data":{"turn":1,"step":4,"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":[41],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":43,"time":0,"data":{"turn":1,"step":4}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":44,"time":0,"data":{"turn":1,"step":5}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":45,"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":46,"time":0,"data":{"turn":1,"step":5,"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\"}"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":47,"time":0,"data":{"turn":1,"step":5,"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\"}"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":48,"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":49,"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":50,"time":0,"data":{"turn":1,"step":5,"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\"}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[45,46,47,48,49],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":51,"time":0,"data":{"turn":1,"step":5,"callId":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\"}"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":52,"time":0,"data":{"turn":1,"step":5,"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":[51],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":53,"time":0,"data":{"turn":1,"step":5}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":54,"time":0,"data":{"turn":1,"step":6}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":55,"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":56,"time":0,"data":{"turn":1,"step":6,"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":57,"time":0,"data":{"turn":1,"step":6,"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":58,"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":59,"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":60,"time":0,"data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[55,56,57,58,59],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":61,"time":0,"data":{"turn":1,"step":6,"callId":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":62,"time":0,"data":{"turn":1,"step":6,"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":[61],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":63,"time":0,"data":{"turn":1,"step":6}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":64,"time":0,"data":{"turn":1,"step":7}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":65,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":66,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"text-delta","index":0,"text":"PERSISTENT_TOOLS_OK"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":67,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PERSISTENT_TOOLS_OK"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":68,"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":69,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":70,"time":0,"data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"text","text":"PERSISTENT_TOOLS_OK"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[65,66,67,68,69],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":71,"time":0,"data":{"turn":1,"step":7}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":72,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}}} +{"method":"session.finished","params":{"sessionId":"{{sessionId}}","status":"ok","reason":{"kind":"completed"}}} diff --git a/examples/jsonrpc-agent/tests/snapshots/persistent-tools/result.expected.json b/examples/jsonrpc-agent/tests/snapshots/persistent-tools/result.expected.json new file mode 100644 index 0000000000..989372e15b --- /dev/null +++ b/examples/jsonrpc-agent/tests/snapshots/persistent-tools/result.expected.json @@ -0,0 +1 @@ +{"status":"ok","reason":{"kind":"completed"},"finalResponse":"PERSISTENT_TOOLS_OK"} diff --git a/examples/jsonrpc-agent/tests/snapshots/persistent-tools/session.jsonl b/examples/jsonrpc-agent/tests/snapshots/persistent-tools/session.jsonl new file mode 100644 index 0000000000..8a288888d5 --- /dev/null +++ b/examples/jsonrpc-agent/tests/snapshots/persistent-tools/session.jsonl @@ -0,0 +1,74 @@ +{"type":"session","version":0,"id":"persistent-tools-snapshot","createdAt":1785331618309,"cwd":"{{cwd}}","delegationDepth":0} +{"type":"turn/start","seq":0,"time":1785331618311,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":1,"time":1785331618311,"data":{"content":[{"type":"text","text":"Prove that bash state persists. 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":"d0534fe8-a74b-4fcf-913f-d78e36f486bb"},"surfaceOp":"append"} +{"type":"session/title","seq":2,"time":1785331618312,"data":{"title":"Prove that bash state persists.","messageSeqs":[1],"source":{"kind":"fallback"}}} +{"type":"step/start","seq":3,"time":1785331618312,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":4,"time":1785331618313,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":5,"time":1785331618325,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":6,"time":1785331618325,"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","seq":7,"time":1785331618326,"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","seq":8,"time":1785331618326,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","seq":9,"time":1785331618326,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":10,"time":1785331618327,"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","model":"deepseek-v4-flash"},"id":"68f0912b-5e3a-417e-a324-00871206cdf7"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[5,6,7,8,9],"surfaceOp":"append"} +{"type":"tool/call","seq":11,"time":1785331618327,"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","seq":12,"time":1785331618649,"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":"a83a469c-0321-4f8b-a40e-913c1b433b9d"}},"sourceEventSeqs":[11],"surfaceOp":"append"} +{"type":"step/end","seq":13,"time":1785331618649,"data":{"turn":1,"step":1}} +{"type":"step/start","seq":14,"time":1785331618649,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":15,"time":1785331618652,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":16,"time":1785331618652,"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","seq":17,"time":1785331618652,"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","seq":18,"time":1785331618652,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","seq":19,"time":1785331618652,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":20,"time":1785331618652,"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","model":"deepseek-v4-flash"},"id":"425c837c-b7e5-48ef-bc97-282bf5a10221"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[15,16,17,18,19],"surfaceOp":"append"} +{"type":"tool/call","seq":21,"time":1785331618652,"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","seq":22,"time":1785331618759,"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":"1d3fcea8-51d9-47a1-8e8e-283c7b9cf53a"}},"sourceEventSeqs":[21],"surfaceOp":"append"} +{"type":"step/end","seq":23,"time":1785331618759,"data":{"turn":1,"step":2}} +{"type":"step/start","seq":24,"time":1785331618759,"data":{"turn":1,"step":3}} +{"type":"assistant/chunk","seq":25,"time":1785331618762,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":26,"time":1785331618762,"data":{"turn":1,"step":3,"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\"}"}}} +{"type":"assistant/chunk","seq":27,"time":1785331618762,"data":{"turn":1,"step":3,"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\"}"}}}} +{"type":"assistant/chunk","seq":28,"time":1785331618762,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","seq":29,"time":1785331618762,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":30,"time":1785331618762,"data":{"turn":1,"step":3,"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\"}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"6407aec3-f75c-427a-8783-a61bd99327bb"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[25,26,27,28,29],"surfaceOp":"append"} +{"type":"tool/call","seq":31,"time":1785331618762,"data":{"turn":1,"step":3,"callId":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\"}"}} +{"type":"tool/result","seq":32,"time":1785331618782,"data":{"turn":1,"step":3,"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":"121833da-381d-492e-9d6c-82eaa9694ef1"}},"sourceEventSeqs":[31],"surfaceOp":"append"} +{"type":"step/end","seq":33,"time":1785331618782,"data":{"turn":1,"step":3}} +{"type":"step/start","seq":34,"time":1785331618782,"data":{"turn":1,"step":4}} +{"type":"assistant/chunk","seq":35,"time":1785331618784,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":36,"time":1785331618784,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-view","name":"str_replace_editor","argumentsDelta":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\"}"}}} +{"type":"assistant/chunk","seq":37,"time":1785331618784,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\"}"}}}} +{"type":"assistant/chunk","seq":38,"time":1785331618784,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","seq":39,"time":1785331618784,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":40,"time":1785331618784,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\"}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"1cf1d34c-faee-464d-bdd7-413ba7233e23"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[35,36,37,38,39],"surfaceOp":"append"} +{"type":"tool/call","seq":41,"time":1785331618784,"data":{"turn":1,"step":4,"callId":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\"}"}} +{"type":"tool/result","seq":42,"time":1785331618799,"data":{"turn":1,"step":4,"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":"c88746c2-208d-46aa-8c3d-79ccc88c7f6d"}},"sourceEventSeqs":[41],"surfaceOp":"append"} +{"type":"step/end","seq":43,"time":1785331618799,"data":{"turn":1,"step":4}} +{"type":"step/start","seq":44,"time":1785331618799,"data":{"turn":1,"step":5}} +{"type":"assistant/chunk","seq":45,"time":1785331618801,"data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":46,"time":1785331618801,"data":{"turn":1,"step":5,"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\"}"}}} +{"type":"assistant/chunk","seq":47,"time":1785331618801,"data":{"turn":1,"step":5,"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\"}"}}}} +{"type":"assistant/chunk","seq":48,"time":1785331618801,"data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","seq":49,"time":1785331618801,"data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":50,"time":1785331618802,"data":{"turn":1,"step":5,"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\"}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"b8832049-1795-4127-b0e0-e31528da0e99"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[45,46,47,48,49],"surfaceOp":"append"} +{"type":"tool/call","seq":51,"time":1785331618802,"data":{"turn":1,"step":5,"callId":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\"}"}} +{"type":"tool/result","seq":52,"time":1785331618803,"data":{"turn":1,"step":5,"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":"ee874ae7-c4d9-4075-9b40-45e643a4b159"}},"sourceEventSeqs":[51],"surfaceOp":"append"} +{"type":"step/end","seq":53,"time":1785331618803,"data":{"turn":1,"step":5}} +{"type":"step/start","seq":54,"time":1785331618803,"data":{"turn":1,"step":6}} +{"type":"assistant/chunk","seq":55,"time":1785331618804,"data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":56,"time":1785331618804,"data":{"turn":1,"step":6,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-exit","name":"bash","argumentsDelta":"{\"command\":\"exit 9\"}"}}} +{"type":"assistant/chunk","seq":57,"time":1785331618804,"data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}}}} +{"type":"assistant/chunk","seq":58,"time":1785331618804,"data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","seq":59,"time":1785331618804,"data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":60,"time":1785331618805,"data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"8e39f4fe-5538-46be-b24a-84296d638c44"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[55,56,57,58,59],"surfaceOp":"append"} +{"type":"tool/call","seq":61,"time":1785331618805,"data":{"turn":1,"step":6,"callId":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}} +{"type":"tool/result","seq":62,"time":1785331618806,"data":{"turn":1,"step":6,"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":"cb4bf07d-474f-46de-a945-94666c849a5f"}},"sourceEventSeqs":[61],"surfaceOp":"append"} +{"type":"step/end","seq":63,"time":1785331618806,"data":{"turn":1,"step":6}} +{"type":"step/start","seq":64,"time":1785331618806,"data":{"turn":1,"step":7}} +{"type":"assistant/chunk","seq":65,"time":1785331618807,"data":{"turn":1,"step":7,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} +{"type":"assistant/chunk","seq":66,"time":1785331618807,"data":{"turn":1,"step":7,"chunk":{"type":"text-delta","index":0,"text":"PERSISTENT_TOOLS_OK"}}} +{"type":"assistant/chunk","seq":67,"time":1785331618807,"data":{"turn":1,"step":7,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PERSISTENT_TOOLS_OK"}}}} +{"type":"assistant/chunk","seq":68,"time":1785331618807,"data":{"turn":1,"step":7,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","seq":69,"time":1785331618807,"data":{"turn":1,"step":7,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":70,"time":1785331618808,"data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"text","text":"PERSISTENT_TOOLS_OK"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"42e7f4c0-f936-4616-8af3-4f486f27fbb5"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[65,66,67,68,69],"surfaceOp":"append"} +{"type":"step/end","seq":71,"time":1785331618808,"data":{"turn":1,"step":7}} +{"type":"turn/end","seq":72,"time":1785331618808,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/packages/fs/tool-str-replace-editor/README.i18n.yaml b/packages/fs/tool-str-replace-editor/README.i18n.yaml index 10b1f19182..7d08a33f9a 100644 --- a/packages/fs/tool-str-replace-editor/README.i18n.yaml +++ b/packages/fs/tool-str-replace-editor/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/tool-str-replace-editor/README.md -README.md: 8ac6a22f24ddcdd3818b346e3426e58e718027e2 -README.zh.md: cf82b132b63730af209c159066a70f6a18b77f39 +README.md: 97e9e0ab9ade7c7241c1aac3e2489e055d01ff8f +README.zh.md: 48358eb3c9d81ddad6a83c4ff3ef0cf6542096b1 diff --git a/packages/fs/tool-str-replace-editor/README.md b/packages/fs/tool-str-replace-editor/README.md index 8ac6a22f24..97e9e0ab9a 100644 --- a/packages/fs/tool-str-replace-editor/README.md +++ b/packages/fs/tool-str-replace-editor/README.md @@ -10,12 +10,10 @@ Standalone model-facing `str_replace_editor` over `ctx.fs`. It can be composed w |---|---:|---| | `maxOutputChars` | `16000` | Prefix characters retained for file and directory views. | | `description` | Editor command guide | Model-facing tool description. | -| `requireAbsolutePath` | `true` | Reject relative paths; disable only for deployments with a deliberate session-cwd contract. | -| `expandTabsOnMutation` | `true` | Preserve the canonical Claude SWE behavior that expands tabs across the whole file before replace/insert. Set `false` for atomic literal replacement that preserves unrelated tabs. | ## Tool -The schema provides `view`, `create`, `str_replace`, and `insert`. File views use one-based line numbers; directory views omit hidden, dependency, and Python-cache entries and descend two levels. Replacement requires one unique literal match and reports errors only in the public `old_str` vocabulary. Insert follows the selected zero-based insertion boundary without adding an implicit trailing newline. +The schema provides `view`, `create`, `str_replace`, and `insert` over absolute paths. File views use one-based line numbers and preserve content tabs, so displayed text remains valid literal replacement input; directory views omit hidden, dependency, and Python-cache entries and descend two levels. Replacement requires one unique literal match and reports errors only 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. ## Model Experience @@ -51,5 +49,4 @@ Append-only tool results follow the reusable request prefix. - Operations target UTF-8 text; binary files are unsupported. - `str_replace` intentionally rejects zero or multiple matches and has no `replace_all` argument. -- Canonical mode (`expandTabsOnMutation: true`) expands tabs in the entire file before replacement or insertion, including lines outside the edited region. Set it to `false` for Makefiles and other tab-sensitive files. - Every mutation goes through `fs/write-intent` or `fs/edit-intent`, resolves the current session sandbox policy, and delegates enforcement to the mounted filesystem 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 index cf82b132b6..48358eb3c9 100644 --- a/packages/fs/tool-str-replace-editor/README.zh.md +++ b/packages/fs/tool-str-replace-editor/README.zh.md @@ -10,12 +10,10 @@ |---|---:|---| | `maxOutputChars` | `16000` | 文件和目录查看结果保留的前缀字符数。 | | `description` | 编辑器命令指南 | 面向模型的工具描述。 | -| `requireAbsolutePath` | `true` | 拒绝相对路径;仅当部署明确约定 session cwd 时才应关闭。 | -| `expandTabsOnMutation` | `true` | 保留 Claude SWE 参考行为:替换/插入前展开整个文件的制表符。设为 `false` 时使用原子字面量替换,并保留未触及的制表符。 | ## 工具 -Schema 提供 `view`、`create`、`str_replace` 与 `insert`。文件查看使用从一开始的行号;目录查看忽略隐藏、依赖与 Python 缓存条目并下探两层。替换要求字面量唯一匹配,错误只使用公开的 `old_str` 词汇。插入遵循所选的零基插入边界,不会隐式补尾换行。 +Schema 提供针对绝对路径的 `view`、`create`、`str_replace` 与 `insert`。文件查看使用从一开始的行号,并保留内容中的制表符,因此显示的文本仍可作为有效的字面量替换输入;目录查看忽略隐藏、依赖与 Python 缓存条目并下探两层。替换要求字面量唯一匹配,错误只使用公开的 `old_str` 词汇。插入遵循所选的零基插入边界,不会隐式补尾换行。修改操作会保留请求编辑范围之外的制表符。 ## 模型体验 @@ -51,5 +49,4 @@ Schema 提供 `view`、`create`、`str_replace` 与 `insert`。文件查看使 - 操作面向 UTF-8 文本,不支持二进制文件。 - `str_replace` 刻意拒绝零匹配或多匹配,且没有 `replace_all` 参数。 -- 规范模式(`expandTabsOnMutation: true`)会在替换或插入前展开整个文件中的制表符,包括未编辑区域。Makefile 等依赖制表符的文件应设为 `false`。 - 每个修改操作都会经过 `fs/write-intent` 或 `fs/edit-intent`,解析当前 session 的沙箱策略,并交由挂载的文件系统与策略插件执行。 diff --git a/packages/fs/tool-str-replace-editor/src/index.ts b/packages/fs/tool-str-replace-editor/src/index.ts index 7ff0773107..6d0f65c48c 100644 --- a/packages/fs/tool-str-replace-editor/src/index.ts +++ b/packages/fs/tool-str-replace-editor/src/index.ts @@ -35,23 +35,6 @@ function maybeTruncate(content: string, maxOutputChars: number): string { : content.slice(0, maxOutputChars) + TRUNCATED_MESSAGE } -function expandTabs(content: string, tabSize = 8): string { - let column = 0 - let result = '' - for (const character of content) { - if (character === '\t') { - const spaces = tabSize - (column % tabSize) - result += ' '.repeat(spaces) - column += spaces - continue - } - result += character - if (character === '\n' || character === '\r') column = 0 - else column += 1 - } - return result -} - function codepointCompare(left: string, right: string): number { return left < right ? -1 : left > right ? 1 : 0 } @@ -105,16 +88,13 @@ class MutationPolicy { async function resolveTarget( ctx: Context, path: string, - requireAbsolutePath: boolean, - exec: ToolRunContext, - workspaceRoot?: string, + signal: AbortSignal, ): Promise { if (path.trim().length === 0) throw new Error('path must be a non-empty string') - if (requireAbsolutePath && !isAbsolute(path)) { + if (!isAbsolute(path)) { throw new Error(`The path ${path} is not an absolute path, it should start with \`/\`. Maybe you meant /${path}?`) } - const cwd = exec.agent?.session.header.cwd ?? workspaceRoot - return ctx.fs.resolve(path, cwd === undefined ? { signal: exec.signal } : { cwd, signal: exec.signal }) + return ctx.fs.resolve(path, { signal }) } async function statExisting( @@ -195,9 +175,9 @@ function formatFileView( : allLines.slice(initialLine - 1, finalLine) prompt += ` with view_range=[${initialLine}, ${finalLine}]` } - const numbered = expandTabs(lines - .map((line, index) => `${String(initialLine + index).padStart(6, ' ')}\t${line}`) - .join('\n')) + const numbered = lines + .map((line, index) => `${String(initialLine + index).padStart(6, ' ')} ${line}`) + .join('\n') return maybeTruncate(`${prompt}:\n${numbered}\n`, maxOutputChars) } @@ -237,10 +217,9 @@ async function viewPath( path: string, viewRange: number[] | undefined, maxOutputChars: number, - requireAbsolutePath: boolean, exec: ToolRunContext, ): Promise { - const target = await resolveTarget(ctx, path, requireAbsolutePath, exec) + const target = await resolveTarget(ctx, path, exec.signal) const info = await statExisting(ctx, target, 'view', exec) if (info.type === 'directory') { if (viewRange !== undefined) { @@ -261,12 +240,11 @@ async function createFile( policy: MutationPolicy, path: string, fileText: string | undefined, - requireAbsolutePath: boolean, exec: ToolRunContext, ): Promise { const content = requiredForCommand(fileText, 'file_text', 'create') const sandboxPolicy = policy.resolve(exec) - const target = await resolveTarget(ctx, path, requireAbsolutePath, exec, sandboxPolicy?.workspaceRoot) + 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\`.`) } @@ -298,24 +276,21 @@ async function replaceInFile( path: string, oldStr: string | undefined, newStr: string | undefined, - requireAbsolutePath: boolean, - expandTabsOnMutation: boolean, exec: ToolRunContext, ): Promise { const sandboxPolicy = policy.resolve(exec) - const target = await resolveTarget(ctx, path, requireAbsolutePath, exec, sandboxPolicy?.workspaceRoot) + const target = await resolveTarget(ctx, path, exec.signal) const intent = await ctx.waterfall('fs/edit-intent', target, exec, () => undefined) - const rawOldValue = requiredForCommand(oldStr, 'old_str', 'str_replace', false) - const oldValue = expandTabsOnMutation ? expandTabs(rawOldValue) : rawOldValue - const newValue = expandTabsOnMutation ? expandTabs(newStr ?? '') : newStr ?? '' + 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 rawBefore = await ctx.fs.readText(target, exec.signal) - const before = expandTabsOnMutation ? expandTabs(rawBefore) : rawBefore + const before = await ctx.fs.readText(target, exec.signal) const offsets = matchOffsets(before, oldValue) - if (offsets.length === 0) { + 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', @@ -330,23 +305,15 @@ async function replaceInFile( } let outcome try { - outcome = expandTabsOnMutation - ? await ctx.fs.writeText( - target, - before.replace(oldValue, newValue), - intent === undefined - ? { kind: 'replaceIfVersion', version: info.version } - : { kind: 'replaceIfVersion', version: intent.version }, - exec.signal, - sandboxPolicy, - ) - : await ctx.fs.editText( - target, - { oldString: oldValue, newString: newValue, replaceAll: false }, - intent ?? { version: info.version }, - exec.signal, - sandboxPolicy, - ) + 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) } @@ -360,22 +327,18 @@ async function insertInFile( path: string, insertLine: number | undefined, newStr: string | undefined, - requireAbsolutePath: boolean, - expandTabsOnMutation: boolean, exec: ToolRunContext, ): Promise { if (insertLine === undefined) throw new Error('Parameter `insert_line` is required for command: insert') - const rawValue = requiredForCommand(newStr, 'new_str', 'insert') - const value = expandTabsOnMutation ? expandTabs(rawValue) : rawValue + const value = requiredForCommand(newStr, 'new_str', 'insert') const sandboxPolicy = policy.resolve(exec) - const target = await resolveTarget(ctx, path, requireAbsolutePath, exec, sandboxPolicy?.workspaceRoot) + 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 rawBefore = await ctx.fs.readText(target, exec.signal) - const before = expandTabsOnMutation ? expandTabs(rawBefore) : rawBefore + 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( @@ -403,8 +366,6 @@ async function insertInFile( interface ResolvedConfig { maxOutputChars: number description: string - requireAbsolutePath: boolean - expandTabsOnMutation: boolean } function presentEditorCall(args: { @@ -501,9 +462,9 @@ function registerStrReplaceEditor(ctx: Context, config: ResolvedConfig): void { async execute(args, exec) { switch (args.command) { case 'view': - return viewPath(ctx, args.path, args.view_range, config.maxOutputChars, config.requireAbsolutePath, exec) + return viewPath(ctx, args.path, args.view_range, config.maxOutputChars, exec) case 'create': - return createFile(ctx, policy, args.path, args.file_text, config.requireAbsolutePath, exec) + return createFile(ctx, policy, args.path, args.file_text, exec) case 'str_replace': return replaceInFile( ctx, @@ -511,8 +472,6 @@ function registerStrReplaceEditor(ctx: Context, config: ResolvedConfig): void { args.path, args.old_str, args.new_str, - config.requireAbsolutePath, - config.expandTabsOnMutation, exec, ) case 'insert': @@ -522,8 +481,6 @@ function registerStrReplaceEditor(ctx: Context, config: ResolvedConfig): void { args.path, args.insert_line, args.new_str, - config.requireAbsolutePath, - config.expandTabsOnMutation, exec, ) } @@ -541,18 +498,12 @@ export interface Config { maxOutputChars?: number /** Model-facing tool description. */ description?: string - /** Require local absolute paths like the canonical editor contract (default true). */ - requireAbsolutePath?: boolean - /** Expand tabs across the full file before each mutation, matching the canonical editor (default true). */ - expandTabsOnMutation?: boolean } /** 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), - requireAbsolutePath: z.boolean().default(true), - expandTabsOnMutation: z.boolean().default(true), }) /** Register one `str_replace_editor` tool over `ctx.fs`. */ @@ -560,8 +511,6 @@ export function apply(ctx: Context, config: Config): void { const resolved: ResolvedConfig = { maxOutputChars: config.maxOutputChars ?? 16_000, description: config.description ?? DEFAULT_DESCRIPTION, - requireAbsolutePath: config.requireAbsolutePath ?? true, - expandTabsOnMutation: config.expandTabsOnMutation ?? true, } if (!Number.isSafeInteger(resolved.maxOutputChars) || resolved.maxOutputChars <= 0) { throw new Error('tool-str-replace-editor: maxOutputChars must be a positive safe integer') diff --git a/packages/fs/tool-str-replace-editor/tests/tools.spec.ts b/packages/fs/tool-str-replace-editor/tests/tools.spec.ts index fb1e0d4b58..9a948ed164 100644 --- a/packages/fs/tool-str-replace-editor/tests/tools.spec.ts +++ b/packages/fs/tool-str-replace-editor/tests/tools.spec.ts @@ -78,13 +78,13 @@ async function setup( await ctx.plugin(SandboxedFileSystem, { cwd: root }) } if (options.fsPolicy === true) await ctx.plugin(FsPolicy) - await ctx.plugin(ToolStrReplaceEditor, config) - return { ctx, root, owner: agent(ctx, root) } + 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 } = await setup({ description: 'custom editor description' }) + 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') @@ -147,6 +147,10 @@ describe('tool-str-replace-editor', () => { })).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 () => { @@ -190,6 +194,21 @@ describe('tool-str-replace-editor', () => { expect(await readFile(sample, 'utf8')).toBe('one\nbetween\n\nthree\n') }) + 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 }) @@ -316,6 +335,16 @@ describe('tool-str-replace-editor', () => { 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') @@ -378,13 +407,6 @@ describe('tool-str-replace-editor', () => { })).error).toMatchObject({ info: { code: 'FS_NOT_REGULAR_FILE' } }) }) - it('can opt into session-relative paths for non-canonical deployments', async () => { - const { ctx, root, owner } = await setup({ requireAbsolutePath: false }) - await writeFile(join(root, 'relative.txt'), 'relative') - expect(text(await call(ctx, owner, { command: 'view', path: 'relative.txt' }))) - .toContain("Here's the content of") - }) - it('delegates read-before-edit decisions to fs-policy', async () => { const { ctx, root, owner } = await setup({}, { fsPolicy: true }) const existing = join(root, 'existing.txt') @@ -444,15 +466,17 @@ describe('tool-str-replace-editor', () => { expect(ownerless.error).toMatchObject({ info: { code: 'FS_SANDBOX_DENIED' } }) }) - it('can preserve tabs outside the edited region', async () => { - const { ctx, root, owner } = await setup({ expandTabsOnMutation: false }) + 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: 'old', - new_str: 'new', + old_str: '\told', + new_str: '\tnew', }) await call(ctx, owner, { command: 'str_replace', @@ -487,9 +511,10 @@ describe('tool-str-replace-editor', () => { const { ctx, root, owner } = await setup() const path = join(root, 'backend-error.txt') await writeFile(path, 'old\n') - ctx.fs.writeText = async () => { + const failWrite = async (): Promise => { throw new Error('backend write failed') } + ctx.fs.writeText = failWrite const replace = await call(ctx, owner, { command: 'str_replace', diff --git a/packages/pty/tool-bash-persistent/README.i18n.yaml b/packages/pty/tool-bash-persistent/README.i18n.yaml index 2f15d109c1..aec30c2b4a 100644 --- a/packages/pty/tool-bash-persistent/README.i18n.yaml +++ b/packages/pty/tool-bash-persistent/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/pty/tool-bash-persistent/README.md -README.md: 04c714d5489dbae8572e9339a4387a148450a0e9 -README.zh.md: adfb38b10409174d9558b963f5a2359cf819f04b +README.md: cfb3acf41803f5b06ecb3f56ee8ffc29ce9147b1 +README.zh.md: a525485933344a8f9bc218456c099efdef572902 diff --git a/packages/pty/tool-bash-persistent/README.md b/packages/pty/tool-bash-persistent/README.md index 04c714d548..cfb3acf418 100644 --- a/packages/pty/tool-bash-persistent/README.md +++ b/packages/pty/tool-bash-persistent/README.md @@ -10,7 +10,7 @@ Model-facing `bash(command)` backed by one owner-scoped `ctx.pty` shell. The pac |---|---:|---| | `backendType` | `shell` | Registered PTY backend used for each Agent shell. | | `timeoutMs` | `300000` | Wall-clock limit for one command; timeout closes the shell. | -| `maxOutputChars` | `16000` | Prefix characters retained before the clipping notice. | +| `maxOutputChars` | `16000` | Maximum retained command-output characters; fixed diagnostics are added afterward. | | `description` | Persistent-shell description | Model-facing environment contract. | ## Model Experience @@ -33,11 +33,11 @@ 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 and the shell prompt. 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 tells the model that the next call starts fresh. +Commands share one shell per Agent, so cwd, exported variables, activated environments, functions, and background jobs persist across calls. Results exclude private completion markers and the shell prompt. 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. #### Token effect -Data-dependent and bounded by `maxOutputChars` plus the fixed clipping notice. +Data-dependent. `maxOutputChars` bounds retained command output; fixed clipping, lost-prefix, status, timeout, and reset diagnostics can extend the result. #### KV Cache effect @@ -46,5 +46,5 @@ Append-only tool results follow the reusable request prefix. ## Known Limitations and Deferred Work - The tool requires an owning Agent and a real PTY backend. -- Explicit `exit`, timeout, or cancellation discards shell state; the next call starts a fresh shell. +- Explicit `exit` and timeout discard shell state. Cancellation also resets and discards the result, even when a complete status marker is already observable; the next call starts a fresh shell. - Environment facts such as network access and package mirrors belong in the configured `description`, not this package's default. diff --git a/packages/pty/tool-bash-persistent/README.zh.md b/packages/pty/tool-bash-persistent/README.zh.md index adfb38b104..a525485933 100644 --- a/packages/pty/tool-bash-persistent/README.zh.md +++ b/packages/pty/tool-bash-persistent/README.zh.md @@ -10,7 +10,7 @@ |---|---:|---| | `backendType` | `shell` | 每个 Agent shell 使用的已注册 PTY 后端。 | | `timeoutMs` | `300000` | 单条命令的墙钟时间上限;超时会关闭 shell。 | -| `maxOutputChars` | `16000` | 截断提示前保留的前缀字符数。 | +| `maxOutputChars` | `16000` | 命令输出最多保留的字符数;固定诊断会在此后追加。 | | `description` | 持久 shell 描述 | 面向模型的环境契约。 | ## 模型体验 @@ -33,11 +33,11 @@ #### 模型所见 -每个 Agent 的命令共享一个 shell,因此 cwd、导出的环境变量、已激活环境、函数和后台任务会跨调用保留。结果不包含私有完成标记和 shell 提示符。长输出保留仍可读取的最早前缀并追加截断提示;若 PTY 已丢弃真正的开头,结果会明确说明,而不是把尾部伪装成完整输出。超时返回有界的部分输出、关闭状态不确定的 shell,并告知模型下次调用从新 shell 开始。 +每个 Agent 的命令共享一个 shell,因此 cwd、导出的环境变量、已激活环境、函数和后台任务会跨调用保留。结果不包含私有完成标记和 shell 提示符。经封装的命令以非零状态结束时,结果会追加 `[exit code: N]`;若 shell 在报告该状态前退出,则改为追加 `[shell exited: code N]`、`[shell killed by signal: SIG]`,或在后端既未提供退出码也未提供信号时追加 `[shell exited]`;随后重置 shell,并告知模型下次调用从新 shell 开始。长输出保留仍可读取的最早前缀并追加截断提示;若 PTY 已丢弃真正的开头,结果会明确说明,而不是把尾部伪装成完整输出。超时返回有界的部分输出、关闭状态不确定的 shell,并报告该重置。 #### Token 影响 -随数据变化,并受 `maxOutputChars` 与固定截断提示约束。 +随数据变化。`maxOutputChars` 限制保留的命令输出;固定的截断、前缀丢失、状态、超时与重置诊断可能使结果更长。 #### KV Cache 影响 @@ -46,5 +46,5 @@ ## 已知限制与延后工作 - 工具需要拥有它的 Agent 和真实 PTY 后端。 -- 显式 `exit`、超时或取消会丢弃 shell 状态;下次调用创建新 shell。 +- 显式 `exit` 与超时会丢弃 shell 状态。取消同样会重置 shell 并丢弃结果,即使已经能观察到完整状态标记也是如此;下次调用创建新 shell。 - 网络访问、软件包镜像等环境事实应写入配置的 `description`,而非包默认描述。 diff --git a/packages/pty/tool-bash-persistent/src/index.ts b/packages/pty/tool-bash-persistent/src/index.ts index 136dcbaabd..24cc998cf2 100644 --- a/packages/pty/tool-bash-persistent/src/index.ts +++ b/packages/pty/tool-bash-persistent/src/index.ts @@ -11,6 +11,7 @@ import type { PtyReadResult, PtySendResult, PtySessionId } from '@deepseek-ai/ds import { deadline, timeoutOf } from '@deepseek-ai/dsh-timeout' import { defineTool } from '@deepseek-ai/dsh-tools' +// TODO: Replace the file-search advice; arbitrary command output need not come from a searchable file. 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.' @@ -43,6 +44,7 @@ interface RetainedOutput { interface CapturedOutput { text: string incomplete: boolean + exitCode?: number } interface PersistentShells { @@ -91,14 +93,17 @@ function stripPrompt(text: string): string { function commandOutput( snapshot: RetainedOutput, marker: CommandMarkers, -): CapturedOutput { +): CapturedOutput | undefined { const text = snapshot.text const end = text.lastIndexOf(marker.end) + const status = /^(\d+)\r?\n/.exec(text.slice(end + marker.end.length))?.[1] + if (status === undefined) return undefined const startMarker = text.lastIndexOf(marker.start, end) const start = startMarker < 0 ? 0 : startMarker + marker.start.length return { text: stripPrompt(text.slice(start, end).replace(/^\r?\n/, '')), incomplete: startMarker < 0, + exitCode: Number(status), } } @@ -165,9 +170,31 @@ function retainedScrollback( function renderCaptured(output: CapturedOutput, maxOutputChars: number): string { const rendered = maybeTruncate(output.text, maxOutputChars, output.incomplete) - return output.incomplete && output.text.length > 0 + 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}]` + : undefined + return appendStatusMarker(withPrefix, marker) +} + +function appendStatusMarker(content: string, marker: string | undefined): string { + if (marker === undefined) return content + return content.length === 0 ? marker : `${content}\n${marker}` +} + +function renderShellExitStatus( + content: string, + exitCode: number | null, + signal: NodeJS.Signals | null, +): string { + const marker = signal !== null + ? `[shell killed by signal: ${signal}]` + : exitCode !== null + ? `[shell exited: code ${exitCode}]` + : '[shell exited]' + return appendStatusMarker(content, marker) } function persistentShells(ctx: Context, config: ResolvedConfig): PersistentShells { @@ -286,27 +313,32 @@ async function executeCommand( ) await shells.reset(owner, 'persistent bash command timed out') 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, SHELL_RESET_MESSAGE, ].join('\n') } + if (commandDeadline.signal.aborted) { + await shells.reset(owner, 'persistent bash command aborted') + commandDeadline.signal.throwIfAborted() + } if (latest.text.includes(marker.end)) { const complete = commandOutput(retainedScrollback(ctx, owner, id, latest), marker) - return renderCaptured(complete, config.maxOutputChars) + if (complete !== undefined) return renderCaptured(complete, config.maxOutputChars) } if (result.sessionStatus.kind === 'exited') { const snapshot = retainedScrollback(ctx, owner, id, latest) await shells.reset(owner, 'persistent bash shell exited') return [ - renderCaptured(partialOutput(snapshot, marker, fallback, fallbackTruncated), config.maxOutputChars), + renderShellExitStatus( + renderCaptured(partialOutput(snapshot, marker, fallback, fallbackTruncated), config.maxOutputChars), + result.sessionStatus.exitCode, + result.sessionStatus.signal, + ), SHELL_RESET_MESSAGE, ].filter(part => part.length > 0).join('\n') } - if (commandDeadline.signal.aborted) { - await shells.reset(owner, 'persistent bash command aborted') - commandDeadline.signal.throwIfAborted() - } if (promptCompleted(result)) { const snapshot = retainedScrollback(ctx, owner, id, latest) return renderCaptured( diff --git a/packages/pty/tool-bash-persistent/src/invariant.ts b/packages/pty/tool-bash-persistent/src/invariant.ts index f6b5acfbc7..5e276d4c45 100644 --- a/packages/pty/tool-bash-persistent/src/invariant.ts +++ b/packages/pty/tool-bash-persistent/src/invariant.ts @@ -15,8 +15,9 @@ export const name = 'tool-bash-persistent-invariant' export const inject = ['invariants'] /** - * No runtime invariant: the tool adapter owns no independent durable state; - * PTY ownership and filesystem mutation relations stay with their services. + * No runtime invariant: the adapter's private owner-to-shell cache has no + * observable event or data relation. Lifecycle tests prove its cleanup without + * adding a public surface solely for an invariant. */ const install: InvariantInstaller = () => {} diff --git a/packages/pty/tool-bash-persistent/tests/tools.spec.ts b/packages/pty/tool-bash-persistent/tests/tools.spec.ts index 811d733f2f..4b5401a256 100644 --- a/packages/pty/tool-bash-persistent/tests/tools.spec.ts +++ b/packages/pty/tool-bash-persistent/tests/tools.spec.ts @@ -78,9 +78,15 @@ type StubMode = | 'empty-read' | 'stalled-read' | 'exit' + | 'signal-exit' + | 'unknown-exit' | 'wait-for-abort' + | 'end-on-abort' | 'idle-then-normal' | 'large' + | 'nonzero' + | 'torn-status' + | 'finish-torn-status' | 'end-only' | 'init-exit' | 'init-timeout' @@ -117,11 +123,16 @@ class StubPtySession implements PtyBackendSession { return this.operation(Promise.resolve(this.result(this.motd, 'stdin_read'))) } if (this.mode === 'send-error') throw new Error('stub send failed') - if (this.mode === 'wait-for-abort') { + if (this.mode === 'wait-for-abort' || this.mode === 'end-on-abort') { const done = new Promise>((resolve) => { request.signal?.addEventListener('abort', () => { - this.scrollback += 'partial output' - resolve(this.result('partial output', 'stdin_read')) + const start = /__DSH_PERSISTENT_BASH_START_[^_]+(?:-[^_]+)*__/.exec(request.text)?.[0] + const end = /__DSH_PERSISTENT_BASH_END_[^:]+:/.exec(request.text)?.[0] + const output = this.mode === 'end-on-abort' + ? `${start ?? ''}\ninterrupted\n${end ?? ''}130\n${this.motd}` + : 'partial output' + this.scrollback += output + resolve(this.result(output, 'stdin_read')) }, { once: true }) }) return this.operation(done) @@ -152,18 +163,36 @@ class StubPtySession implements PtyBackendSession { this.pendingText = '' const start = /__DSH_PERSISTENT_BASH_START_[^_]+(?:-[^_]+)*__/.exec(sent)?.[0] const end = /__DSH_PERSISTENT_BASH_END_[^:]+:/.exec(sent)?.[0] + if (this.mode === 'torn-status') { + const output = `${start ?? ''}\nhello from stub\n${end ?? ''}` + this.scrollback += output + this.mode = 'finish-torn-status' + return this.operation(Promise.resolve(this.result(output, 'inferred_idle'))) + } + if (this.mode === 'finish-torn-status') { + const output = `7\n${this.motd}` + this.scrollback += output + return this.operation(Promise.resolve(this.result(output, 'stdin_read'))) + } if (this.mode === 'end-only') { const output = `recovered output\n${end ?? ''}0\n${this.motd}` this.scrollback += output return this.operation(Promise.resolve(this.result(output, 'stdin_read'))) } - const commandOutput = this.mode === 'large' ? 'x'.repeat(100) : 'hello from stub' - const output = `${start ?? ''}\n${commandOutput}\n${end ?? ''}0\n${this.motd}` + const commandOutput = this.mode === 'large' + ? 'x'.repeat(100) + : this.mode === 'nonzero' ? '' : 'hello from stub' + const exitCode = this.mode === 'nonzero' ? 7 : 0 + const output = `${start ?? ''}\n${commandOutput}\n${end ?? ''}${exitCode}\n${this.motd}` this.scrollback += output - if (this.mode === 'exit') { + if (this.mode === 'exit' || this.mode === 'signal-exit' || this.mode === 'unknown-exit') { const exitedOutput = `${start ?? ''}\nhello from stub\n` this.scrollback = this.scrollback.slice(0, -output.length) + exitedOutput - this.statusValue = { kind: 'exited', exitCode: 0, signal: null } + this.statusValue = this.mode === 'signal-exit' + ? { kind: 'exited', exitCode: null, signal: 'SIGTERM' } + : this.mode === 'exit' + ? { kind: 'exited', exitCode: 9, signal: null } + : { kind: 'exited', exitCode: null, signal: null } return this.operation(Promise.resolve(this.result(exitedOutput, 'session_exit'))) } return this.operation(Promise.resolve(this.result(output, 'stdin_read'))) @@ -247,7 +276,7 @@ async function setup( describe('tool-bash-persistent', () => { it('registers a configurable schema and reuses one owner shell', async () => { - const { ctx, owner, stub } = await setup({ + const { ctx, owner, stub, fiber } = await setup({ backendType: 'stub', description: 'deployment-specific persistent shell', }) @@ -269,6 +298,10 @@ describe('tool-bash-persistent', () => { const ownerWithoutCwd = agent(ctx, undefined) expect(text(await call(ctx, ownerWithoutCwd, 'pwd'))).toBe('hello from stub') expect(stub.sessions).toHaveLength(2) + + await fiber.dispose() + expect(ctx.tools.schemas()).toEqual([]) + expect(ctx.tools.get('bash')).toBeUndefined() }) it('handles inferred idle, prompt fallback, shell exit, clipping, and cleanup', async () => { @@ -303,19 +336,48 @@ describe('tool-bash-persistent', () => { session.mode = 'large' expect(text(await call(ctx, owner, 'large'))).toContain('') + session.mode = 'nonzero' + expect(text(await call(ctx, owner, 'false'))).toBe('[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).toContain('next bash call starts from the workspace') expect(session.closed).toContain('persistent bash shell exited') await call(ctx, owner, 'new shell') expect(stub.sessions).toHaveLength(2) + const replacement = stub.sessions[1]! + replacement.mode = 'signal-exit' + expect(text(await call(ctx, owner, 'kill shell'))) + .toContain('[shell killed by signal: SIGTERM]') + + await call(ctx, owner, 'another shell') + expect(stub.sessions).toHaveLength(3) const externallyClosed = ctx.pty.list(owner)[0]?.sessionId expect(externallyClosed).toBeDefined() await ctx.pty.kill(owner, externallyClosed!, 'external cleanup') await fiber.dispose() - expect(stub.sessions[1]?.closed).toEqual(['external cleanup']) + expect(stub.sessions[2]?.closed).toEqual(['external cleanup']) + }) + + it('waits for status digits after a torn completion marker', async () => { + const { ctx, owner, stub } = await setup({ backendType: 'stub', maxOutputChars: 1_000 }) + await call(ctx, owner, 'warm up') + 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]') + }) + + it('reports a shell exit when the backend has no code or signal', async () => { + const { ctx, owner, stub } = await setup({ backendType: 'stub' }) + await call(ctx, owner, 'warm up') + stub.sessions[0]!.mode = 'unknown-exit' + + expect(text(await call(ctx, owner, 'exit without status'))).toContain('[shell exited]') }) it('marks a short missing-prefix result and tolerates exhausted scrollback pages', async () => { @@ -372,22 +434,25 @@ describe('tool-bash-persistent', () => { expect(stub.sessions[0]?.closed).toContain('persistent bash command timed out') }) - it('cancels in-flight work, resets the shell, and releases a queued call', async () => { - const { ctx, owner, stub } = await setup({ backendType: 'stub', timeoutMs: 5_000 }) - await call(ctx, owner, 'warm up') - stub.sessions[0]!.mode = 'wait-for-abort' - const controller = new AbortController() - const cancelled = call(ctx, owner, 'hang', controller.signal) - const queued = call(ctx, owner, 'after cancellation') - setTimeout(() => { - controller.abort(new Error('caller stopped')) - }, 5) + it.each(['wait-for-abort', 'end-on-abort'] as const)( + 'cancels %s work, resets the shell, and releases a queued call', + async (mode) => { + const { ctx, owner, stub } = await setup({ backendType: 'stub', timeoutMs: 5_000 }) + await call(ctx, owner, 'warm up') + stub.sessions[0]!.mode = mode + const controller = new AbortController() + const cancelled = call(ctx, owner, 'hang', controller.signal) + const queued = call(ctx, owner, 'after cancellation') + setTimeout(() => { + controller.abort(new Error('caller stopped')) + }, 5) - expect((await cancelled).isError).toBe(true) - expect(text(await queued)).toBe('hello from stub') - expect(stub.sessions[0]?.closed).toContain('persistent bash command aborted') - expect(stub.sessions).toHaveLength(2) - }) + expect((await cancelled).isError).toBe(true) + expect(text(await queued)).toBe('hello from stub') + expect(stub.sessions[0]?.closed).toContain('persistent bash command aborted') + expect(stub.sessions).toHaveLength(2) + }, + ) it.each(['init-exit', 'init-timeout'] as const)( 'fails initialization and closes the unusable shell for %s', diff --git a/patches/node-pty@1.1.0.patch b/patches/node-pty@1.1.0.patch index f0de7b9054..56892a3d58 100644 --- a/patches/node-pty@1.1.0.patch +++ b/patches/node-pty@1.1.0.patch @@ -2,7 +2,7 @@ diff --git a/lib/unixTerminal.js b/lib/unixTerminal.js index 1ec12f796a822c78fba9ad7f6448c3987e325c23..5cd6b7d635f4752be5a6c5ff9cf9edf988cf94c5 100644 --- a/lib/unixTerminal.js +++ b/lib/unixTerminal.js -@@ -26,10 +26,22 @@ var terminal_1 = require("./terminal"); +@@ -26,10 +26,23 @@ var terminal_1 = require("./terminal"); var utils_1 = require("./utils"); var native = utils_1.loadNativeModule('pty'); var pty = native.module; @@ -10,6 +10,7 @@ index 1ec12f796a822c78fba9ad7f6448c3987e325c23..5cd6b7d635f4752be5a6c5ff9cf9edf9 -helperPath = path.resolve(__dirname, helperPath); -helperPath = helperPath.replace('app.asar', 'app.asar.unpacked'); -helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked'); ++// A current external embedded-runtime consumer supplies a non-sibling helper. +var helperPath = process.env.DSH_NODE_PTY_SPAWN_HELPER; +if (helperPath) { + helperPath = path.resolve(helperPath); @@ -33,7 +34,7 @@ diff --git a/src/unixTerminal.ts b/src/unixTerminal.ts index 98733dc0cd752b554bd94e45904ca341ad141bba..fa234291206617ae5a6d8605abf9771220392d17 100644 --- a/src/unixTerminal.ts +++ b/src/unixTerminal.ts -@@ -14,10 +14,20 @@ import { assign, loadNativeModule } from './utils'; +@@ -14,10 +14,21 @@ import { assign, loadNativeModule } from './utils'; const native = loadNativeModule('pty'); const pty: IUnixNative = native.module; @@ -41,6 +42,7 @@ index 98733dc0cd752b554bd94e45904ca341ad141bba..fa234291206617ae5a6d8605abf97712 -helperPath = path.resolve(__dirname, helperPath); -helperPath = helperPath.replace('app.asar', 'app.asar.unpacked'); -helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked'); ++// A current external embedded-runtime consumer supplies a non-sibling helper. +let helperPath = process.env.DSH_NODE_PTY_SPAWN_HELPER; +if (helperPath) { + helperPath = path.resolve(helperPath); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 73092a292c..82af864f54 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,7 @@ settings: patchedDependencies: '@earendil-works/pi-tui@0.80.7': 6c30c5386c0159131e1361023cddf31377f5728962524841964373312c1ed946 - node-pty@1.1.0: 4a1568bc9ef77084629054d0736430818818155abcd0dce581ef8c782e974c15 + node-pty@1.1.0: 7a0c04f1f49d798a9ffe2f7f414c01064a44ca2489772d0c3e1235ab336755e6 importers: @@ -656,7 +656,7 @@ importers: devDependencies: node-pty: specifier: 1.1.0 - version: 1.1.0(patch_hash=4a1568bc9ef77084629054d0736430818818155abcd0dce581ef8c782e974c15) + version: 1.1.0(patch_hash=7a0c04f1f49d798a9ffe2f7f414c01064a44ca2489772d0c3e1235ab336755e6) packages/acp/acp: dependencies: @@ -3402,7 +3402,7 @@ importers: dependencies: node-pty: specifier: ^1.1.0 - version: 1.1.0(patch_hash=4a1568bc9ef77084629054d0736430818818155abcd0dce581ef8c782e974c15) + version: 1.1.0(patch_hash=7a0c04f1f49d798a9ffe2f7f414c01064a44ca2489772d0c3e1235ab336755e6) schemastery: specifier: ^3.18.0 version: 3.18.0 @@ -15287,7 +15287,7 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 - node-pty@1.1.0(patch_hash=4a1568bc9ef77084629054d0736430818818155abcd0dce581ef8c782e974c15): + node-pty@1.1.0(patch_hash=7a0c04f1f49d798a9ffe2f7f414c01064a44ca2489772d0c3e1235ab336755e6): dependencies: node-addon-api: 7.1.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8aad3a1f3d..eb7a7c6322 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -40,7 +40,7 @@ allowBuilds: # JSONL durability calls MoveFileExW with write-through publication on Windows. koffi: true # The Python runtime deploy includes the reviewed workspace postinstall that - # places node-pty's spawn helper beside the compiled PTY backend. + # restores the executable bit on node-pty's macOS spawn helper. '@deepseek-ai/dsh-pty-local@file:packages/pty/pty-local': true # The Landlock launcher family is our own sibling-repo release, consumed diff --git a/python/README.i18n.yaml b/python/README.i18n.yaml index f0d6c67967..59f3e836cf 100644 --- a/python/README.i18n.yaml +++ b/python/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write python/README.md -README.md: aee682e25fc33287c49131d0f5b92b136ed16bae -README.zh.md: 4404114fcdab78468991769a4657370f85997a88 +README.md: dfd9d909122f9245a19fe91d8a394156795b13ae +README.zh.md: 151b2cdab4f28384a20d16c86d398f669fbdc818 diff --git a/python/README.md b/python/README.md index aee682e25f..dfd9d90912 100644 --- a/python/README.md +++ b/python/README.md @@ -22,7 +22,7 @@ pnpm exec tsx scripts/build-exe-for-python-sdk.ts --skip-build # lib/ artifac pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets=node24-linux-x64,node24-linux-arm64,node24-macos-arm64 ``` -Products land in `dist-exe/` and are synced into this package as `sdk-runtime/src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-pkg--` plus the matching `-spawn-helper` required by `node-pty` (platform: `linux`/`macos`; arch: `x64`/`arm64`) — after a local build the SDK finds the runtime with no further setup. The `build-exe-for-python-sdk` CI workflow (manual dispatch, or the `build-exe` PR label) exercises the same products. A full three-target run retains four release wheels; a subset dispatch retains the SDK wheel and selected platform wheels. Which plugins the exe bundles and how the carriers are organized: [sdk-runtime README](sdk-runtime/README.md); the build also refreshes the dev-only node carrier (see "against the Node source" below). +Products land in `dist-exe/` and are synced into this package as `sdk-runtime/src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-pkg--` (platform: `linux`/`macos`; arch: `x64`/`arm64`); macOS builds also sync the matching `-spawn-helper` required by `node-pty`. After a local build the SDK finds the runtime with no further setup. The `build-exe-for-python-sdk` CI workflow (manual dispatch, or the `build-exe` PR label) exercises the same products. A full three-target run retains four release wheels; a subset dispatch retains the SDK wheel and selected platform wheels. Which plugins the exe bundles and how the carriers are organized: [sdk-runtime README](sdk-runtime/README.md); the build also refreshes the dev-only node carrier (see "against the Node source" below). ## Validating the SDK against the executable diff --git a/python/README.zh.md b/python/README.zh.md index 4404114fcd..151b2cdab4 100644 --- a/python/README.zh.md +++ b/python/README.zh.md @@ -22,7 +22,7 @@ pnpm exec tsx scripts/build-exe-for-python-sdk.ts --skip-build # lib/ artifac pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets=node24-linux-x64,node24-linux-arm64,node24-macos-arm64 ``` -产物落入 `dist-exe/`,并同步进本包的 `sdk-runtime/src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-pkg--` 及 `node-pty` 所需的同名 `-spawn-helper` 伴随文件(platform:`linux`/`macos`;arch:`x64`/`arm64`),本地构建完成后 SDK 不需要额外设置就能找到运行时。`build-exe-for-python-sdk` CI 工作流(手动触发,或给 PR 打 `build-exe` 标签)会测试同样的产物。完整构建三个目标时保留 4 个发布用 wheel 包;手动选择部分目标时保留 SDK wheel 与所选平台的 wheel。exe 内置哪些插件、载体如何组织,见 [sdk-runtime README](sdk-runtime/README.md);构建还会顺带刷新仅供开发使用的 `node` 载体(见下文「对着 Node 源码运行」)。 +产物落入 `dist-exe/`,并同步进本包的 `sdk-runtime/src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-pkg--`(platform:`linux`/`macos`;arch:`x64`/`arm64`);macOS 构建还会同步 `node-pty` 所需的同名 `-spawn-helper` 伴随文件。本地构建完成后 SDK 不需要额外设置就能找到运行时。`build-exe-for-python-sdk` CI 工作流(手动触发,或给 PR 打 `build-exe` 标签)会测试同样的产物。完整构建三个目标时保留 4 个发布用 wheel 包;手动选择部分目标时保留 SDK wheel 与所选平台的 wheel。exe 内置哪些插件、载体如何组织,见 [sdk-runtime README](sdk-runtime/README.md);构建还会顺带刷新仅供开发使用的 `node` 载体(见下文「对着 Node 源码运行」)。 ## 用可执行文件验证 SDK diff --git a/python/sdk-runtime/README.i18n.yaml b/python/sdk-runtime/README.i18n.yaml index 4b104ec211..f39bfa8f13 100644 --- a/python/sdk-runtime/README.i18n.yaml +++ b/python/sdk-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 python/sdk-runtime/README.md -README.md: ee3791eddf26b526316d4f3952793a03cc48841e -README.zh.md: 59d40ee56688cb377902ff126b7fa77606c7ad8b +README.md: 07bb3c574b3cd49f1dc74f0e9d9bd1bb7ca9b216 +README.zh.md: 9eb03352505f7cad3e6bf6f253ed6564fdb06ca0 diff --git a/python/sdk-runtime/README.md b/python/sdk-runtime/README.md index ee3791eddf..07bb3c574b 100644 --- a/python/sdk-runtime/README.md +++ b/python/sdk-runtime/README.md @@ -8,19 +8,19 @@ Runtime carrier package for the Python SDK (dist `deepseek-harness-runtime-bin`, Two carriers coexist under `src/deepseek_harness_runtime/runtime/`, both injected by the repo's `scripts/build-exe-for-python-sdk.ts` build and both gitignored: -- **exe (production)** — a single-file Node executable `dsh-jsonrpc-agent-pkg--` plus its native `-spawn-helper` sibling (platform: `linux`/`macos`; arch: `x64`/`arm64`). The helper is required by `node-pty`; both files are built and validated as one runtime product. No Node installation is needed on the target machine. This is the only carrier that ships in wheel distributions; this package does not publish sdists. +- **exe (production)** — a single-file Node executable `dsh-jsonrpc-agent-pkg--` (platform: `linux`/`macos`; arch: `x64`/`arm64`). macOS builds also ship the native `-spawn-helper` sibling that `node-pty` uses there. No Node installation is needed on the target machine. This is the only carrier that ships in wheel distributions; this package does not publish sdists. - **node (dev-only)** — the full deploy closure under `runtime/node/` (`package.json` + `node_modules/`), executed as `node runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js` on a system Node >= 22.19. It is the current checkout's source build, meant for repo-local development and verification only; it is never selected automatically and is excluded from distributions. Both carriers hold the same content, defined once: the [package.json](package.json) at this package's root is the deploy root of the single-exe pipeline — a pure dependency manifest (no code of its own) whose dependency closure IS both the plugin set compiled into the exe and the tree materialized into `runtime/node/`. Adding a plugin to the distribution means adding one dependency line there and rebuilding. A missing exe raises `FileNotFoundError` naming both acquisition routes: build via `scripts/build-exe-for-python-sdk.ts` in a deepseek-harness checkout, or install the matching platform runtime wheel produced by the `build-exe-for-python-sdk` CI workflow. A missing dev-only node carrier names its sole route, the build script. The workflow retains wheels rather than standalone executable archives. Acquisition strategy is deliberately separate from the lookup interface, so an on-demand download can replace it later without touching callers. -Each wheel contains exactly one runtime executable and its matching native spawn helper. A missing sidecar makes the runtime installation incomplete and is a hard startup error, even for a selected Cordis composition that does not use PTY tools; old exe-only wheels are intentionally unsupported. The fixed tags are `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, and `py3-none-macosx_11_0_arm64`; the build hook rejects `py3-none-any`, absent or multiple runtime files, non-executable files, and unsupported platform tags. The repository root `package.json` supplies the shared version for this package and the SDK, and a `python-vX.Y.Z` release tag must match it. +Each wheel contains exactly one runtime executable. The macOS wheel also contains its matching native spawn helper; a missing sidecar makes that installation incomplete and is a hard startup error, even for a selected Cordis composition that does not use PTY tools. Linux wheels contain no spawn helper because `node-pty` uses the staged `pty.node` addon directly. The fixed tags are `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, and `py3-none-macosx_11_0_arm64`; the build hook rejects `py3-none-any`, absent or multiple runtime files, non-executable files, and unsupported platform tags. The repository root `package.json` supplies the shared version for this package and the SDK, and a `python-vX.Y.Z` release tag must match it. ## Resolution API - `resolve_bundled_launch_args(mode=None) -> tuple[str, ...]` — the argv tuple that launches the bundled runtime: `(exe_path,)` in exe mode, `(node_path, bin_js_path)` in node mode. Mode selection: explicit argument > `DSH_RUNTIME_MODE` env var (`exe` | `node`) > automatic. Automatic resolution finds the production exe ONLY — the dev-only node carrier must be opted into explicitly so a production deployment can never silently ride on a source build. -- `bundled_runtime_path() -> Path` — the platform exe path (exe carrier only; it validates that the required sibling `-spawn-helper` is also installed). The node carrier has no single-path equivalent and launches via the argv tuple above. +- `bundled_runtime_path() -> Path` — the platform exe path (exe carrier only; on macOS it validates that the required sibling `-spawn-helper` is also installed). The node carrier has no single-path equivalent and launches via the argv tuple above. - `bundled_default_config_path() -> Path` — the checked-in default config (see below). - `bundled_package_dir() -> Path` — the installed package data root. diff --git a/python/sdk-runtime/README.zh.md b/python/sdk-runtime/README.zh.md index 59d40ee566..9eb0335250 100644 --- a/python/sdk-runtime/README.zh.md +++ b/python/sdk-runtime/README.zh.md @@ -8,19 +8,19 @@ Python SDK 的运行时载体包(分发名 `deepseek-harness-runtime-bin`, 两种载体并存于 `src/deepseek_harness_runtime/runtime/` 之下,均由仓库的 `scripts/build-exe-for-python-sdk.ts` 构建注入,且均被 git 忽略: -- **exe(生产)**——单文件 Node 可执行程序 `dsh-jsonrpc-agent-pkg--` 及其原生 `-spawn-helper` 伴随文件(platform:`linux`/`macos`;arch:`x64`/`arm64`)。`node-pty` 需要该 helper;构建与校验会把两者视作同一个运行时产物。目标机器无需安装 Node。这是唯一随 wheel 包分发的载体;本包不发布 sdist。 +- **exe(生产)**——单文件 Node 可执行程序 `dsh-jsonrpc-agent-pkg--`(platform:`linux`/`macos`;arch:`x64`/`arm64`)。macOS 构建还会随附 `node-pty` 在该平台使用的原生 `-spawn-helper` 伴随文件。目标机器无需安装 Node。这是唯一随 wheel 包分发的载体;本包不发布 sdist。 - **`node`(仅限开发)**——`runtime/node/` 下的完整部署闭包(`package.json` + `node_modules/`),在系统 Node >= 22.19 上以 `node runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js` 执行。它是当前检出的源码构建,仅用于仓库本地的开发与验证;不会被自动选中,也不进入分发物。 两种载体承载相同的内容,且只定义一次:本包根目录的 [package.json](package.json) 是 single-exe 流水线的部署根目录——一份零代码的纯依赖 manifest,其依赖闭包既是编译进 exe 的插件集,也是物化到 `runtime/node/` 的文件树。往分发物里加插件,就是在那里加一行依赖再重新构建。 exe 缺失时抛出 `FileNotFoundError`,并写明两种获取途径:在 deepseek-harness 检出中经 `scripts/build-exe-for-python-sdk.ts` 构建,或安装 `build-exe-for-python-sdk` CI 工作流生成的对应平台运行时 wheel 包。仅限开发的 `node` 载体缺失时只提示构建脚本这一条途径。该工作流只保留 wheel 包,不保留独立 exe 归档。获取策略与查找接口刻意分离,之后可以换成按需下载而不改动任何调用方。 -每个 wheel 包只包含一个运行时可执行文件及其匹配的原生 spawn helper。缺少伴随文件意味着运行时安装不完整,并会在启动时硬失败,即使所选 Cordis 组合不使用 PTY 工具也是如此;旧的仅 exe wheel 有意不再兼容。固定标签为 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64` 与 `py3-none-macosx_11_0_arm64`;构建钩子会拒绝 `py3-none-any`、运行时文件缺失或重复、文件不可执行以及不支持的平台标签。仓库根目录的 `package.json` 为本包和 SDK 提供共同版本,`python-vX.Y.Z` 发布标签必须与其匹配。 +每个 wheel 包只包含一个运行时可执行文件。macOS wheel 包还包含与其匹配的原生 spawn helper;缺少伴随文件意味着该安装不完整,并会在启动时硬失败,即使所选 Cordis 组合不使用 PTY 工具也是如此。Linux wheel 包不包含 spawn helper,因为 `node-pty` 直接使用暂存的 `pty.node` 原生插件。固定标签为 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64` 与 `py3-none-macosx_11_0_arm64`;构建钩子会拒绝 `py3-none-any`、运行时文件缺失或重复、文件不可执行以及不支持的平台标签。仓库根目录的 `package.json` 为本包和 SDK 提供共同版本,`python-vX.Y.Z` 发布标签必须与其匹配。 ## 解析 API - `resolve_bundled_launch_args(mode=None) -> tuple[str, ...]`——启动内置运行时的 argv 元组:exe 模式下为 `(exe_path,)`,`node` 模式下为 `(node_path, bin_js_path)`。模式选择:显式参数 > `DSH_RUNTIME_MODE` 环境变量(`exe` | `node`)> 自动。自动解析只找生产 exe——仅限开发的 `node` 载体必须显式选用,从而生产部署绝不会悄悄跑在源码构建上。 -- `bundled_runtime_path() -> Path`——平台 exe 路径(仅 exe 载体,并会校验必要的 `-spawn-helper` 伴随文件也已安装)。`node` 载体没有单一路径的等价物,经由上面的 argv 元组启动。 +- `bundled_runtime_path() -> Path`——平台 exe 路径(仅 exe 载体,并会在 macOS 上校验必要的 `-spawn-helper` 伴随文件也已安装)。`node` 载体没有单一路径的等价物,经由上面的 argv 元组启动。 - `bundled_default_config_path() -> Path`——检入的默认配置(见下文)。 - `bundled_package_dir() -> Path`——已安装包的数据根目录。 diff --git a/python/sdk-runtime/hatch_build.py b/python/sdk-runtime/hatch_build.py index 108e77cf2c..b0f9a79550 100644 --- a/python/sdk-runtime/hatch_build.py +++ b/python/sdk-runtime/hatch_build.py @@ -13,7 +13,6 @@ _PLATFORMS = { "linux-arm64": ("manylinux_2_28_aarch64", "dsh-jsonrpc-agent-pkg-linux-arm64"), "macos-arm64": ("macosx_11_0_arm64", "dsh-jsonrpc-agent-pkg-macos-arm64"), } -_SPAWN_HELPER_SUFFIX = "-spawn-helper" def _host_platform_tag() -> str: @@ -48,23 +47,17 @@ class RuntimeBuildHook(BuildHookInterface): expected_executable = matches[0][1] runtime_dir = Path(self.root) / "src" / "deepseek_harness_runtime" / "runtime" runtime_files = sorted(runtime_dir.glob("dsh-jsonrpc-agent-pkg-*") if runtime_dir.is_dir() else []) - executables = [path for path in runtime_files if not path.name.endswith(_SPAWN_HELPER_SUFFIX)] - helpers = [path for path in runtime_files if path.name.endswith(_SPAWN_HELPER_SUFFIX)] - if [path.name for path in executables] != [expected_executable]: - found = ", ".join(path.name for path in executables) or "none" + expected_files = [expected_executable] + if "-macos-" in expected_executable: + expected_files.append(f"{expected_executable}-spawn-helper") + found_files = [path.name for path in runtime_files] + if found_files != expected_files: raise RuntimeError( - f"runtime wheel {platform_tag} must contain only {expected_executable}; found {found}" + f"runtime wheel {platform_tag} payload must be {expected_files}; found {found_files}" ) - expected_helper = f"{expected_executable}{_SPAWN_HELPER_SUFFIX}" - if [path.name for path in helpers] != [expected_helper]: - found = ", ".join(path.name for path in helpers) or "none" - raise RuntimeError( - f"runtime wheel {platform_tag} must contain only {expected_helper}; found {found}" - ) - for executable in [executables[0], helpers[0]]: + for executable in runtime_files: if executable.stat().st_mode & stat.S_IXUSR == 0: raise RuntimeError(f"runtime executable is not executable: {executable}") - build_data["pure_python"] = False build_data["infer_tag"] = False build_data["tag"] = f"py3-none-{platform_tag}" diff --git a/python/sdk-runtime/src/deepseek_harness_runtime/__init__.py b/python/sdk-runtime/src/deepseek_harness_runtime/__init__.py index 9228281ab2..a3aa53ae80 100644 --- a/python/sdk-runtime/src/deepseek_harness_runtime/__init__.py +++ b/python/sdk-runtime/src/deepseek_harness_runtime/__init__.py @@ -5,8 +5,8 @@ Two runtime carriers coexist under ``runtime/``, both injected by the repo's - **exe (production)**: single-file Node executables named ``dsh-jsonrpc-agent-pkg--`` (platform in {linux, macos}, arch in - {x64, arm64}) plus a sibling ``-spawn-helper`` used by ``node-pty``; the - target machine needs no Node installation. + {x64, arm64}); macOS also uses a sibling ``-spawn-helper``. The target machine + needs no Node installation. - **node (dev-only)**: the full deploy closure under ``runtime/node/`` (``package.json`` + ``node_modules/``), executed as ``node runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js`` on a @@ -28,7 +28,6 @@ import sys from pathlib import Path PACKAGE_METADATA_FILENAME = "deepseek-harness-runtime.json" -SPAWN_HELPER_SUFFIX = "-spawn-helper" RUNTIME_MODE_ENV_VAR = "DSH_RUNTIME_MODE" @@ -71,11 +70,11 @@ def bundled_default_config_path() -> Path: def bundled_runtime_path() -> Path: """Absolute path of the bundled single-file runtime executable for the current platform. - Raises FileNotFoundError when the platform is unsupported or the executable - has not been placed into this package; the message names the acquisition - routes (acquisition strategy is deliberately separate from this lookup - interface, so an on-demand download can replace it without touching - callers). + Raises FileNotFoundError when the platform is unsupported, the executable + has not been placed into this package, or the required macOS spawn helper is + missing; the message names the acquisition routes (acquisition strategy is + deliberately separate from this lookup interface, so an on-demand download + can replace it without touching callers). """ tag = _current_platform_tag() path = bundled_package_dir() / "runtime" / f"dsh-jsonrpc-agent-pkg-{tag}" @@ -84,12 +83,13 @@ def bundled_runtime_path() -> Path: f"deepseek-harness-runtime-bin is missing the runtime executable at {path}. " + _EXE_ACQUISITION_HINT ) - helper = Path(f"{path}{SPAWN_HELPER_SUFFIX}") - if not helper.is_file(): - raise FileNotFoundError( - f"deepseek-harness-runtime-bin is missing the node-pty spawn helper at {helper}. " - + _EXE_ACQUISITION_HINT - ) + if tag.startswith("macos-"): + helper = Path(f"{path}-spawn-helper") + if not helper.is_file(): + raise FileNotFoundError( + f"deepseek-harness-runtime-bin is missing the node-pty spawn helper at {helper}. " + + _EXE_ACQUISITION_HINT + ) return path @@ -152,7 +152,6 @@ def _node_launch_args() -> tuple[str, str]: __all__ = [ "PACKAGE_METADATA_FILENAME", "RUNTIME_MODE_ENV_VAR", - "SPAWN_HELPER_SUFFIX", "bundled_default_config_path", "bundled_package_dir", "bundled_runtime_path", diff --git a/python/sdk/tests/test_release_version.py b/python/sdk/tests/test_release_version.py index 7b7c8254b1..7e5f660070 100644 --- a/python/sdk/tests/test_release_version.py +++ b/python/sdk/tests/test_release_version.py @@ -4,7 +4,6 @@ from __future__ import annotations import json import runpy -import stat from pathlib import Path from types import SimpleNamespace @@ -40,38 +39,22 @@ def test_repository_version_rejects_non_stable_versions(tmp_path: Path) -> None: build_python_release.repository_version(tmp_path) -def test_stage_runtime_copies_executable_and_spawn_helper(tmp_path: Path) -> None: - executable = tmp_path / "dsh-jsonrpc-agent-pkg-macos-arm64" +@pytest.mark.parametrize(("target", "with_helper"), [("linux-x64", False), ("macos-arm64", True)]) +def test_stage_runtime_copies_platform_payload( + tmp_path: Path, target: str, with_helper: bool +) -> None: + executable = tmp_path / f"dsh-jsonrpc-agent-pkg-{target}" executable.write_bytes(b"runtime") executable.chmod(0o755) - spawn_helper = Path(f"{executable}-spawn-helper") - spawn_helper.write_bytes(b"helper") - spawn_helper.chmod(0o751) + expected = {executable.name: b"runtime"} + if with_helper: + spawn_helper = Path(f"{executable}-spawn-helper") + spawn_helper.write_bytes(b"helper") + spawn_helper.chmod(0o755) + expected[spawn_helper.name] = b"helper" destination = tmp_path / "staging" - build_python_release.stage_runtime( - destination, - "1.2.3", - executable, - executable.name, - ) + build_python_release.stage_runtime(destination, "1.2.3", executable, executable.name) runtime_dir = destination / "src" / "deepseek_harness_runtime" / "runtime" - assert (runtime_dir / executable.name).read_bytes() == b"runtime" - copied_helper = runtime_dir / spawn_helper.name - assert copied_helper.read_bytes() == b"helper" - assert copied_helper.stat().st_mode & stat.S_IXUSR - - -def test_stage_runtime_rejects_missing_spawn_helper(tmp_path: Path) -> None: - executable = tmp_path / "dsh-jsonrpc-agent-pkg-linux-x64" - executable.write_bytes(b"runtime") - executable.chmod(0o755) - - with pytest.raises(FileNotFoundError, match="spawn helper"): - build_python_release.stage_runtime( - tmp_path / "staging", - "1.2.3", - executable, - executable.name, - ) + assert {path.name: path.read_bytes() for path in runtime_dir.glob("dsh-jsonrpc-agent-pkg-*")} == expected diff --git a/python/sdk/tests/test_runtime_resolution.py b/python/sdk/tests/test_runtime_resolution.py index 400394ae4e..778203f4d1 100644 --- a/python/sdk/tests/test_runtime_resolution.py +++ b/python/sdk/tests/test_runtime_resolution.py @@ -2,6 +2,9 @@ from __future__ import annotations +from pathlib import Path + +import deepseek_harness_runtime as runtime import pytest from deepseek_harness_runtime import ( @@ -39,3 +42,20 @@ def test_explicit_mode_wins_over_env_mode(monkeypatch: pytest.MonkeyPatch) -> No except FileNotFoundError: return # explicit 'exe' was honored; only the artifact is missing assert args[0].endswith(("-x64", "-arm64")) + + +def test_runtime_requires_spawn_helper_only_on_macos( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + runtime_dir = tmp_path / "runtime" + runtime_dir.mkdir() + linux = runtime_dir / "dsh-jsonrpc-agent-pkg-linux-x64" + linux.touch() + (runtime_dir / "dsh-jsonrpc-agent-pkg-macos-arm64").touch() + monkeypatch.setattr(runtime, "bundled_package_dir", lambda: tmp_path) + + monkeypatch.setattr(runtime, "_current_platform_tag", lambda: "macos-arm64") + with pytest.raises(FileNotFoundError, match="node-pty spawn helper"): + runtime.bundled_runtime_path() + monkeypatch.setattr(runtime, "_current_platform_tag", lambda: "linux-x64") + assert runtime.bundled_runtime_path() == linux diff --git a/scripts/build-exe-for-python-sdk.ts b/scripts/build-exe-for-python-sdk.ts index 38400009ac..d2cdbeec4d 100644 --- a/scripts/build-exe-for-python-sdk.ts +++ b/scripts/build-exe-for-python-sdk.ts @@ -7,9 +7,9 @@ */ import { spawn } from 'node:child_process' -import { existsSync, mkdirSync, statSync } from 'node:fs' -import { chmod, copyFile, readFile, rm, writeFile } from 'node:fs/promises' -import { basename, join, resolve, sep } from 'node:path' +import { existsSync, statSync } from 'node:fs' +import { chmod, copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises' +import { basename, dirname, join, resolve, sep } from 'node:path' import { parseArgs } from 'node:util' const root = resolve(import.meta.dirname, '..') @@ -19,7 +19,6 @@ const DEPLOY_ROOT_PACKAGE = 'dsh-jsonrpc-agent-pkg' /** The app entry inside the deployed closure. */ const ENTRY_BIN = 'node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js' const OUTPUT_BASENAME = 'dsh-jsonrpc-agent-pkg' -const SPAWN_HELPER_SUFFIX = '-spawn-helper' /** Default Node major; SEA mode requires at least Node 22. */ const DEFAULT_NODE_RANGE = 'node24' /** Pinned for reproducible builds. */ @@ -53,11 +52,6 @@ const ARCHES = ['x64', 'arm64'] as const type Platform = (typeof PLATFORMS)[number] type Arch = (typeof ARCHES)[number] -interface RuntimeProduct { - executable: string - spawnHelper: string -} - function isPlatform(value: string): value is Platform { return (PLATFORMS as readonly string[]).includes(value) } @@ -295,12 +289,12 @@ class SingleExeBuild { /** * Package one target; SEA mode accepts one target per invocation. * @param target - the pkg target triple to build. - * @returns the canonical product path `/dsh-jsonrpc-agent-pkg--`. + * @returns the executable path and, on macOS, its helper path. */ - async pack(target: Target): Promise { + async pack(target: Target): Promise { const product = join(this.outDir, `${OUTPUT_BASENAME}-${target.platform}-${target.arch}`) - const spawnHelper = `${product}${SPAWN_HELPER_SUFFIX}` - if (!this.cli.dryRun) mkdirSync(this.outDir, { recursive: true }) + await this.prepareNativePty(target) + if (!this.cli.dryRun) await mkdir(this.outDir, { recursive: true }) await this.run(`pkg ${target.spec}`, pnpmBin(), [ 'dlx', PKG_SPEC, @@ -314,85 +308,80 @@ class SingleExeBuild { if (!this.cli.dryRun && !existsSync(product)) { throw new Error(`build-exe-for-python-sdk: product ${product} is missing after the pkg run; inspect ${this.outDir}.`) } + if (target.platform !== 'macos') return [product] + const spawnHelper = `${product}-spawn-helper` + const source = join(this.staging, 'node_modules', 'node-pty', 'prebuilds', `darwin-${target.arch}`, 'spawn-helper') if (this.cli.dryRun) { - console.log(`build-exe-for-python-sdk: [dry-run] copy target node-pty spawn-helper to ${spawnHelper}`) + console.log(`build-exe-for-python-sdk: [dry-run] cp ${source} ${spawnHelper}`) } else { - const source = this.resolveSpawnHelper(target) await copyFile(source, spawnHelper) - await chmod(spawnHelper, statSync(source).mode & 0o777) + await chmod(spawnHelper, 0o755) } - return { executable: product, spawnHelper } + return [product, spawnHelper] } /** - * Resolve the node-pty helper that matches a pkg target. - * @param target - the pkg target whose helper must be shipped. - * @returns a physical executable outside pkg's virtual snapshot. + * Put the target node-pty addon in the staged closure. Linux npm installs + * build it from source, but legacy deploy omits that side-effect directory. + * @param target - the pkg target whose native addon is being staged. */ - private resolveSpawnHelper(target: Target): string { - const nodePtyRoot = join(this.staging, 'node_modules', 'node-pty') - const nativePlatform = target.platform === 'macos' ? 'darwin' : 'linux' - const candidates = [ - join(nodePtyRoot, 'prebuilds', `${nativePlatform}-${target.arch}`, 'spawn-helper'), - ] - const hostPlatform = process.platform === 'darwin' ? 'macos' : process.platform - const hostArch = process.arch === 'x64' || process.arch === 'arm64' ? process.arch : undefined - if (target.platform === hostPlatform && target.arch === hostArch) { - candidates.push(join(nodePtyRoot, 'build', 'Release', 'spawn-helper')) + private async prepareNativePty(target: Target): Promise { + const stagedBuild = join(this.staging, 'node_modules', 'node-pty', 'build') + if (this.cli.dryRun) console.log(`build-exe-for-python-sdk: [dry-run] rm -rf ${stagedBuild}`) + else await rm(stagedBuild, { recursive: true, force: true }) + if (target.platform !== 'linux') return + const source = join(root, 'packages', 'pty', 'pty-local', 'node_modules', 'node-pty', 'build', 'Release', 'pty.node') + const destination = join(stagedBuild, 'Release', 'pty.node') + if (this.cli.dryRun) { + console.log(`build-exe-for-python-sdk: [dry-run] cp ${source} ${destination}`) + return } - const helper = candidates.find(candidate => existsSync(candidate)) - if (helper === undefined) { + const host = Target.host() + if (target.platform !== host.platform || target.arch !== host.arch) { throw new Error( - `build-exe-for-python-sdk: node-pty spawn-helper for ${target.platform}-${target.arch} is missing; ` - + `checked ${candidates.join(', ')}. Build each runtime on its target platform and architecture.`, + 'build-exe-for-python-sdk: build the Linux runtime on its target architecture; ' + + `target ${target.platform}-${target.arch} does not match host ${host.platform}-${host.arch}.`, ) } - if (statSync(helper).mode & 0o111) return helper - throw new Error(`build-exe-for-python-sdk: node-pty spawn-helper is not executable: ${helper}`) + await mkdir(dirname(destination), { recursive: true }) + await copyFile(source, destination) } /** * Print each product path and, outside dry-run mode, its size. * @param products - the product paths returned by {@link pack}. */ - printProducts(products: RuntimeProduct[]): void { + printProducts(products: string[]): void { console.log(this.cli.dryRun ? 'build-exe-for-python-sdk: [dry-run] would produce:' : 'build-exe-for-python-sdk: products:') - for (const product of products) { + for (const path of products) { if (this.cli.dryRun) { - console.log(` ${product.executable}`) - console.log(` ${product.spawnHelper}`) + console.log(` ${path}`) continue } - for (const path of [product.executable, product.spawnHelper]) { - const megabytes = statSync(path).size / (1024 * 1024) - console.log(` ${path} (${megabytes.toFixed(1)} MB)`) - } + const megabytes = statSync(path).size / (1024 * 1024) + console.log(` ${path} (${megabytes.toFixed(1)} MB)`) } } /** - * Copy each executable into the Python runtime package. The deployed node + * Copy each product into the Python runtime package. The deployed node * carrier is already in place, and `dist-exe/` retains upload copies. * @param products - the product paths returned by {@link pack}. */ - async syncToPythonRuntime(products: RuntimeProduct[]): Promise { + async syncToPythonRuntime(products: string[]): Promise { const destDir = resolve(root, PYTHON_RUNTIME_DIR) if (this.cli.dryRun) { - for (const product of products) { - for (const path of [product.executable, product.spawnHelper]) { - console.log(`build-exe-for-python-sdk: [dry-run] cp ${path} ${join(destDir, basename(path))}`) - } + for (const path of products) { + console.log(`build-exe-for-python-sdk: [dry-run] cp ${path} ${join(destDir, basename(path))}`) } return } - mkdirSync(destDir, { recursive: true }) - for (const product of products) { - for (const path of [product.executable, product.spawnHelper]) { - const destination = join(destDir, basename(path)) - await copyFile(path, destination) - await chmod(destination, statSync(path).mode & 0o777) - console.log(`build-exe-for-python-sdk: synced ${destination}`) - } + await mkdir(destDir, { recursive: true }) + for (const path of products) { + const destination = join(destDir, basename(path)) + await copyFile(path, destination) + await chmod(destination, statSync(path).mode & 0o777) + console.log(`build-exe-for-python-sdk: synced ${destination}`) } } @@ -441,8 +430,8 @@ async function main(): Promise { await pipeline.build() await pipeline.deployStaging() await pipeline.injectPkgConfig() - const products: RuntimeProduct[] = [] - for (const target of cli.targets) products.push(await pipeline.pack(target)) + const products: string[] = [] + for (const target of cli.targets) products.push(...await pipeline.pack(target)) pipeline.printProducts(products) await pipeline.syncToPythonRuntime(products) } diff --git a/scripts/build-python-release.py b/scripts/build-python-release.py index 915968b30d..ec049cdd4f 100644 --- a/scripts/build-python-release.py +++ b/scripts/build-python-release.py @@ -22,7 +22,10 @@ PLATFORMS = { "linux-arm64": ("manylinux_2_28_aarch64", "dsh-jsonrpc-agent-pkg-linux-arm64"), "macos-arm64": ("macosx_11_0_arm64", "dsh-jsonrpc-agent-pkg-macos-arm64"), } -SPAWN_HELPER_SUFFIX = "-spawn-helper" + + +def runtime_suffixes(executable_name: str) -> tuple[str, ...]: + return ("", "-spawn-helper") if "-macos-" in executable_name else ("",) def main() -> None: @@ -133,25 +136,12 @@ def stage_sdk(destination: Path, version: str) -> None: def stage_runtime(destination: Path, version: str, executable: Path, executable_name: str) -> None: - if not executable.is_file(): - raise FileNotFoundError(f"runtime executable does not exist: {executable}") - if executable.stat().st_mode & stat.S_IXUSR == 0: - raise PermissionError(f"runtime executable is not executable: {executable}") - spawn_helper = Path(f"{executable}{SPAWN_HELPER_SUFFIX}") - if not spawn_helper.is_file(): - raise FileNotFoundError(f"runtime spawn helper does not exist: {spawn_helper}") - if spawn_helper.stat().st_mode & stat.S_IXUSR == 0: - raise PermissionError(f"runtime spawn helper is not executable: {spawn_helper}") copy_package(ROOT / "python" / "sdk-runtime", destination) rewrite_version(destination / "pyproject.toml", version) runtime_dir = destination / "src" / "deepseek_harness_runtime" / "runtime" runtime_dir.mkdir(parents=True, exist_ok=True) - destination_executable = runtime_dir / executable_name - shutil.copyfile(executable, destination_executable) - destination_executable.chmod(executable.stat().st_mode & 0o777) - destination_helper = runtime_dir / f"{executable_name}{SPAWN_HELPER_SUFFIX}" - shutil.copyfile(spawn_helper, destination_helper) - destination_helper.chmod(spawn_helper.stat().st_mode & 0o777) + for suffix in runtime_suffixes(executable_name): + shutil.copy2(Path(f"{executable}{suffix}"), runtime_dir / f"{executable_name}{suffix}") def verify_wheel( @@ -173,19 +163,16 @@ def verify_wheel( runtime_files = [ name for name in archive.namelist() if "/runtime/dsh-jsonrpc-agent-pkg-" in name ] - helpers = [name for name in runtime_files if name.endswith(SPAWN_HELPER_SUFFIX)] - executables = [name for name in runtime_files if not name.endswith(SPAWN_HELPER_SUFFIX)] if package == "runtime": assert platform is not None - if len(executables) != 1 or not executables[0].endswith(f"/runtime/{platform[1]}"): - raise RuntimeError(f"{wheel} must contain exactly {platform[1]}, found {executables}") - expected_helper = f"{platform[1]}{SPAWN_HELPER_SUFFIX}" - if len(helpers) != 1 or not helpers[0].endswith(f"/runtime/{expected_helper}"): - raise RuntimeError(f"{wheel} must contain exactly {expected_helper}, found {helpers}") - for executable in [executables[0], helpers[0]]: - mode = archive.getinfo(executable).external_attr >> 16 + expected_files = [f"{platform[1]}{suffix}" for suffix in runtime_suffixes(platform[1])] + found_files = sorted(Path(name).name for name in runtime_files) + if found_files != expected_files: + raise RuntimeError(f"{wheel} runtime payload must be {expected_files}, found {found_files}") + for runtime_file in runtime_files: + mode = archive.getinfo(runtime_file).external_attr >> 16 if mode & stat.S_IXUSR == 0: - raise RuntimeError(f"{wheel} runtime executable lost its executable bit: {executable}") + raise RuntimeError(f"{wheel} runtime executable lost its executable bit: {runtime_file}") elif runtime_files: raise RuntimeError(f"SDK wheel unexpectedly contains runtime executables: {runtime_files}") if package == "sdk": diff --git a/scripts/smoke-python-runtime.py b/scripts/smoke-python-runtime.py index 346818e5fa..3ce800932f 100644 --- a/scripts/smoke-python-runtime.py +++ b/scripts/smoke-python-runtime.py @@ -72,6 +72,9 @@ CUSTOM_CORDIS = """\ name: '@deepseek-ai/dsh-agent-spine-demo' config: workspaceContext: false + skills: + enabled: false + toolBash: false tools: mode: both - id: sessions @@ -79,10 +82,6 @@ CUSTOM_CORDIS = """\ config: root: !!js process.env.DSH_SESSION_ROOT compression: 'none' -- id: bash - name: '@deepseek-ai/dsh-bash-local' - config: - cwd: !!js process.env.DSH_CWD - id: code-runtime name: '@deepseek-ai/dsh-code-runtime-worker' - id: subagents @@ -874,6 +873,8 @@ def normalize_snapshot_value( normalized["createdAt"] = 0 if "seq" in normalized and "time" in normalized: normalized["time"] = 0 + if isinstance(normalized.get("id"), str) and normalized.get("role") in ("assistant", "user"): + normalized["id"] = "{{messageId}}" scrub_snapshot_header(normalized) return normalized diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/result.json b/scripts/snapshots/python-sdk-single-exe/advanced/result.json index 07393e7f25..79daa0570c 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/result.json +++ b/scripts/snapshots/python-sdk-single-exe/advanced/result.json @@ -30,7 +30,9 @@ ], "source": { "kind": "user" - } + }, + "role": "user", + "id": "{{messageId}}" }, "surfaceOp": "append" }, @@ -70,20 +72,15 @@ }, "system": "{{system}}", "tools": [ - "bash", "cordis_inspect", "cordis_mount", "cordis_unmount", "run_code", - "skill", "subagent", "task_kill", "task_list", "task_output", "workflow" - ], - "messagePrefix": [ - "{{messagePrefix}}" ] }, "reason": "initial" @@ -176,17 +173,22 @@ "data": { "turn": 1, "step": 1, - "content": [ - { - "type": "tool-call", - "id": "advanced-mount", - "name": "cordis_mount", - "arguments": "{\"code\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}" - } - ], - "provenance": { - "provider": "deepseek", - "model": "smoke-model" + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "advanced-mount", + "name": "cordis_mount", + "arguments": "{\"code\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}" + } + ], + "source": { + "kind": "model", + "provider": "deepseek", + "model": "smoke-model" + }, + "id": "{{messageId}}" }, "usage": { "inputTokens": 3, @@ -221,14 +223,27 @@ "data": { "turn": 1, "step": 1, - "callId": "advanced-mount", - "content": [ - { - "type": "text", - "text": "Temporary Plugin dyn-1 is running (plugin \"\"; available until unmounted or DSH restarts)." - } - ], - "isError": false + "message": { + "source": { + "kind": "tool", + "callId": "advanced-mount" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "advanced-mount", + "content": [ + { + "type": "text", + "text": "Temporary Plugin dyn-1 is running (plugin \"\"; available until unmounted or DSH restarts)." + } + ], + "isError": false + } + ], + "role": "user", + "id": "{{messageId}}" + } }, "sourceEventSeqs": [ 11 @@ -266,21 +281,16 @@ }, "system": "{{system}}", "tools": [ - "bash", "cordis_inspect", "cordis_mount", "cordis_unmount", "run_code", - "skill", "snapshot_double", "subagent", "task_kill", "task_list", "task_output", "workflow" - ], - "messagePrefix": [ - "{{messagePrefix}}" ] }, "reason": "change" @@ -373,17 +383,22 @@ "data": { "turn": 1, "step": 2, - "content": [ - { - "type": "tool-call", - "id": "advanced-code", - "name": "run_code", - "arguments": "{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}" - } - ], - "provenance": { - "provider": "deepseek", - "model": "smoke-model" + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "advanced-code", + "name": "run_code", + "arguments": "{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}" + } + ], + "source": { + "kind": "model", + "provider": "deepseek", + "model": "smoke-model" + }, + "id": "{{messageId}}" }, "usage": { "inputTokens": 3, @@ -451,14 +466,27 @@ "data": { "turn": 1, "step": 2, - "callId": "advanced-code", - "content": [ - { - "type": "text", - "text": "42" - } - ], - "isError": false + "message": { + "source": { + "kind": "tool", + "callId": "advanced-code" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "advanced-code", + "content": [ + { + "type": "text", + "text": "42" + } + ], + "isError": false + } + ], + "role": "user", + "id": "{{messageId}}" + } }, "sourceEventSeqs": [ 22 @@ -570,17 +598,22 @@ "data": { "turn": 1, "step": 3, - "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.\"}" - } - ], - "provenance": { - "provider": "deepseek", - "model": "smoke-model" + "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", + "model": "smoke-model" + }, + "id": "{{messageId}}" }, "usage": { "inputTokens": 3, @@ -615,14 +648,27 @@ "data": { "turn": 1, "step": 3, - "callId": "advanced-direct-child", - "content": [ - { - "type": "text", - "text": "DIRECT_CHILD_OK" - } - ], - "isError": false + "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": [ 34 @@ -734,17 +780,22 @@ "data": { "turn": 1, "step": 4, - "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\"}}" - } - ], - "provenance": { - "provider": "deepseek", - "model": "smoke-model" + "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", + "model": "smoke-model" + }, + "id": "{{messageId}}" }, "usage": { "inputTokens": 3, @@ -779,14 +830,27 @@ "data": { "turn": 1, "step": 4, - "callId": "advanced-workflow", - "content": [ - { - "type": "text", - "text": "workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}" - } - ], - "isError": false + "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 @@ -898,17 +962,22 @@ "data": { "turn": 1, "step": 5, - "content": [ - { - "type": "tool-call", - "id": "advanced-unmount", - "name": "cordis_unmount", - "arguments": "{\"id\": \"dyn-1\"}" - } - ], - "provenance": { - "provider": "deepseek", - "model": "smoke-model" + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "advanced-unmount", + "name": "cordis_unmount", + "arguments": "{\"id\": \"dyn-1\"}" + } + ], + "source": { + "kind": "model", + "provider": "deepseek", + "model": "smoke-model" + }, + "id": "{{messageId}}" }, "usage": { "inputTokens": 3, @@ -943,14 +1012,27 @@ "data": { "turn": 1, "step": 5, - "callId": "advanced-unmount", - "content": [ - { - "type": "text", - "text": "Temporary Plugin dyn-1 was unmounted and removed." - } - ], - "isError": false + "message": { + "source": { + "kind": "tool", + "callId": "advanced-unmount" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "advanced-unmount", + "content": [ + { + "type": "text", + "text": "Temporary Plugin dyn-1 was unmounted and removed." + } + ], + "isError": false + } + ], + "role": "user", + "id": "{{messageId}}" + } }, "sourceEventSeqs": [ 54 @@ -988,20 +1070,15 @@ }, "system": "{{system}}", "tools": [ - "bash", "cordis_inspect", "cordis_mount", "cordis_unmount", "run_code", - "skill", "subagent", "task_kill", "task_list", "task_output", "workflow" - ], - "messagePrefix": [ - "{{messagePrefix}}" ] }, "reason": "change" @@ -1090,15 +1167,20 @@ "data": { "turn": 1, "step": 6, - "content": [ - { - "type": "text", - "text": "ADVANCED_EXECUTABLE_OK" - } - ], - "provenance": { - "provider": "deepseek", - "model": "smoke-model" + "message": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "ADVANCED_EXECUTABLE_OK" + } + ], + "source": { + "kind": "model", + "provider": "deepseek", + "model": "smoke-model" + }, + "id": "{{messageId}}" }, "usage": { "inputTokens": 3, @@ -1173,7 +1255,9 @@ ], "source": { "kind": "user" - } + }, + "role": "user", + "id": "{{messageId}}" }, "surfaceOp": "append" } @@ -1231,20 +1315,15 @@ }, "system": "{{system}}", "tools": [ - "bash", "cordis_inspect", "cordis_mount", "cordis_unmount", "run_code", - "skill", "subagent", "task_kill", "task_list", "task_output", "workflow" - ], - "messagePrefix": [ - "{{messagePrefix}}" ] }, "reason": "initial" @@ -1373,17 +1452,22 @@ "data": { "turn": 1, "step": 1, - "content": [ - { - "type": "tool-call", - "id": "advanced-mount", - "name": "cordis_mount", - "arguments": "{\"code\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}" - } - ], - "provenance": { - "provider": "deepseek", - "model": "smoke-model" + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "advanced-mount", + "name": "cordis_mount", + "arguments": "{\"code\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}" + } + ], + "source": { + "kind": "model", + "provider": "deepseek", + "model": "smoke-model" + }, + "id": "{{messageId}}" }, "usage": { "inputTokens": 3, @@ -1430,14 +1514,27 @@ "data": { "turn": 1, "step": 1, - "callId": "advanced-mount", - "content": [ - { - "type": "text", - "text": "Temporary Plugin dyn-1 is running (plugin \"\"; available until unmounted or DSH restarts)." - } - ], - "isError": false + "message": { + "source": { + "kind": "tool", + "callId": "advanced-mount" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "advanced-mount", + "content": [ + { + "type": "text", + "text": "Temporary Plugin dyn-1 is running (plugin \"\"; available until unmounted or DSH restarts)." + } + ], + "isError": false + } + ], + "role": "user", + "id": "{{messageId}}" + } }, "sourceEventSeqs": [ 11 @@ -1493,21 +1590,16 @@ }, "system": "{{system}}", "tools": [ - "bash", "cordis_inspect", "cordis_mount", "cordis_unmount", "run_code", - "skill", "snapshot_double", "subagent", "task_kill", "task_list", "task_output", "workflow" - ], - "messagePrefix": [ - "{{messagePrefix}}" ] }, "reason": "change" @@ -1636,17 +1728,22 @@ "data": { "turn": 1, "step": 2, - "content": [ - { - "type": "tool-call", - "id": "advanced-code", - "name": "run_code", - "arguments": "{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}" - } - ], - "provenance": { - "provider": "deepseek", - "model": "smoke-model" + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "advanced-code", + "name": "run_code", + "arguments": "{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}" + } + ], + "source": { + "kind": "model", + "provider": "deepseek", + "model": "smoke-model" + }, + "id": "{{messageId}}" }, "usage": { "inputTokens": 3, @@ -1738,14 +1835,27 @@ "data": { "turn": 1, "step": 2, - "callId": "advanced-code", - "content": [ - { - "type": "text", - "text": "42" - } - ], - "isError": false + "message": { + "source": { + "kind": "tool", + "callId": "advanced-code" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "advanced-code", + "content": [ + { + "type": "text", + "text": "42" + } + ], + "isError": false + } + ], + "role": "user", + "id": "{{messageId}}" + } }, "sourceEventSeqs": [ 22 @@ -1905,17 +2015,22 @@ "data": { "turn": 1, "step": 3, - "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.\"}" - } - ], - "provenance": { - "provider": "deepseek", - "model": "smoke-model" + "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", + "model": "smoke-model" + }, + "id": "{{messageId}}" }, "usage": { "inputTokens": 3, @@ -1995,7 +2110,9 @@ ], "source": { "kind": "user" - } + }, + "role": "user", + "id": "{{messageId}}" }, "surfaceOp": "append" } @@ -2053,21 +2170,16 @@ }, "system": "{{system}}", "tools": [ - "bash", "cordis_inspect", "cordis_mount", "cordis_unmount", "run_code", - "skill", "snapshot_double", "subagent", "task_kill", "task_list", "task_output", "workflow" - ], - "messagePrefix": [ - "{{messagePrefix}}" ] }, "reason": "initial" @@ -2192,15 +2304,20 @@ "data": { "turn": 1, "step": 1, - "content": [ - { - "type": "text", - "text": "DIRECT_CHILD_OK" - } - ], - "provenance": { - "provider": "deepseek", - "model": "smoke-model" + "message": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "DIRECT_CHILD_OK" + } + ], + "source": { + "kind": "model", + "provider": "deepseek", + "model": "smoke-model" + }, + "id": "{{messageId}}" }, "usage": { "inputTokens": 3, @@ -2278,14 +2395,27 @@ "data": { "turn": 1, "step": 3, - "callId": "advanced-direct-child", - "content": [ - { - "type": "text", - "text": "DIRECT_CHILD_OK" - } - ], - "isError": false + "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": [ 34 @@ -2445,17 +2575,22 @@ "data": { "turn": 1, "step": 4, - "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\"}}" - } - ], - "provenance": { - "provider": "deepseek", - "model": "smoke-model" + "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", + "model": "smoke-model" + }, + "id": "{{messageId}}" }, "usage": { "inputTokens": 3, @@ -2535,7 +2670,9 @@ ], "source": { "kind": "user" - } + }, + "role": "user", + "id": "{{messageId}}" }, "surfaceOp": "append" } @@ -2593,21 +2730,16 @@ }, "system": "{{system}}", "tools": [ - "bash", "cordis_inspect", "cordis_mount", "cordis_unmount", "run_code", - "skill", "snapshot_double", "subagent", "task_kill", "task_list", "task_output", "workflow" - ], - "messagePrefix": [ - "{{messagePrefix}}" ] }, "reason": "initial" @@ -2732,15 +2864,20 @@ "data": { "turn": 1, "step": 1, - "content": [ - { - "type": "text", - "text": "WORKFLOW_CHILD_OK" - } - ], - "provenance": { - "provider": "deepseek", - "model": "smoke-model" + "message": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "WORKFLOW_CHILD_OK" + } + ], + "source": { + "kind": "model", + "provider": "deepseek", + "model": "smoke-model" + }, + "id": "{{messageId}}" }, "usage": { "inputTokens": 3, @@ -2818,14 +2955,27 @@ "data": { "turn": 1, "step": 4, - "callId": "advanced-workflow", - "content": [ - { - "type": "text", - "text": "workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}" - } - ], - "isError": false + "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 @@ -2985,17 +3135,22 @@ "data": { "turn": 1, "step": 5, - "content": [ - { - "type": "tool-call", - "id": "advanced-unmount", - "name": "cordis_unmount", - "arguments": "{\"id\": \"dyn-1\"}" - } - ], - "provenance": { - "provider": "deepseek", - "model": "smoke-model" + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "advanced-unmount", + "name": "cordis_unmount", + "arguments": "{\"id\": \"dyn-1\"}" + } + ], + "source": { + "kind": "model", + "provider": "deepseek", + "model": "smoke-model" + }, + "id": "{{messageId}}" }, "usage": { "inputTokens": 3, @@ -3042,14 +3197,27 @@ "data": { "turn": 1, "step": 5, - "callId": "advanced-unmount", - "content": [ - { - "type": "text", - "text": "Temporary Plugin dyn-1 was unmounted and removed." - } - ], - "isError": false + "message": { + "source": { + "kind": "tool", + "callId": "advanced-unmount" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "advanced-unmount", + "content": [ + { + "type": "text", + "text": "Temporary Plugin dyn-1 was unmounted and removed." + } + ], + "isError": false + } + ], + "role": "user", + "id": "{{messageId}}" + } }, "sourceEventSeqs": [ 54 @@ -3105,20 +3273,15 @@ }, "system": "{{system}}", "tools": [ - "bash", "cordis_inspect", "cordis_mount", "cordis_unmount", "run_code", - "skill", "subagent", "task_kill", "task_list", "task_output", "workflow" - ], - "messagePrefix": [ - "{{messagePrefix}}" ] }, "reason": "change" @@ -3243,15 +3406,20 @@ "data": { "turn": 1, "step": 6, - "content": [ - { - "type": "text", - "text": "ADVANCED_EXECUTABLE_OK" - } - ], - "provenance": { - "provider": "deepseek", - "model": "smoke-model" + "message": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "ADVANCED_EXECUTABLE_OK" + } + ], + "source": { + "kind": "model", + "provider": "deepseek", + "model": "smoke-model" + }, + "id": "{{messageId}}" }, "usage": { "inputTokens": 3, diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.1.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.1.jsonl index 05998f8980..2929f8664c 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.1.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.1.jsonl @@ -1,14 +1,14 @@ {"type":"session","version":0,"id":"{{child-1}}","createdAt":0,"cwd":"{{cwd}}","parentSession":"{{parent}}","delegationDepth":1} {"type":"turn/start","seq":0,"time":0,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} -{"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} {"type":"session/title","seq":2,"time":0,"data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[1],"source":{"kind":"fallback"}}} {"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}} -{"type":"request/header","seq":4,"time":0,"data":{"header":{"config":{"provider":"deepseek","model":"smoke-model","reasoningEffort":"high"},"system":"{{system}}","tools":["bash","cordis_inspect","cordis_mount","cordis_unmount","run_code","skill","snapshot_double","subagent","task_kill","task_list","task_output","workflow"],"messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}} +{"type":"request/header","seq":4,"time":0,"data":{"header":{"config":{"provider":"deepseek","model":"smoke-model","reasoningEffort":"high"},"system":"{{system}}","tools":["cordis_inspect","cordis_mount","cordis_unmount","run_code","snapshot_double","subagent","task_kill","task_list","task_output","workflow"]},"reason":"initial"}} {"type":"assistant/chunk","seq":5,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","seq":6,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"DIRECT_CHILD_OK"}}} {"type":"assistant/chunk","seq":7,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DIRECT_CHILD_OK"}}}} {"type":"assistant/chunk","seq":8,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","seq":9,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":10,"time":0,"data":{"turn":1,"step":1,"content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"provenance":{"provider":"deepseek","model":"smoke-model"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[5,6,7,8,9],"surfaceOp":"append"} +{"type":"assistant/message","seq":10,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[5,6,7,8,9],"surfaceOp":"append"} {"type":"step/end","seq":11,"time":0,"data":{"turn":1,"step":1}} {"type":"turn/end","seq":12,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.2.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.2.jsonl index 778c200078..a5da33d006 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.2.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.2.jsonl @@ -1,14 +1,14 @@ {"type":"session","version":0,"id":"{{child-2}}","createdAt":0,"cwd":"{{cwd}}","parentSession":"{{parent}}","delegationDepth":1} {"type":"turn/start","seq":0,"time":0,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} -{"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} {"type":"session/title","seq":2,"time":0,"data":{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[1],"source":{"kind":"fallback"}}} {"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}} -{"type":"request/header","seq":4,"time":0,"data":{"header":{"config":{"provider":"deepseek","model":"smoke-model","reasoningEffort":"high"},"system":"{{system}}","tools":["bash","cordis_inspect","cordis_mount","cordis_unmount","run_code","skill","snapshot_double","subagent","task_kill","task_list","task_output","workflow"],"messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}} +{"type":"request/header","seq":4,"time":0,"data":{"header":{"config":{"provider":"deepseek","model":"smoke-model","reasoningEffort":"high"},"system":"{{system}}","tools":["cordis_inspect","cordis_mount","cordis_unmount","run_code","snapshot_double","subagent","task_kill","task_list","task_output","workflow"]},"reason":"initial"}} {"type":"assistant/chunk","seq":5,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","seq":6,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"WORKFLOW_CHILD_OK"}}} {"type":"assistant/chunk","seq":7,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"WORKFLOW_CHILD_OK"}}}} {"type":"assistant/chunk","seq":8,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","seq":9,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":10,"time":0,"data":{"turn":1,"step":1,"content":[{"type":"text","text":"WORKFLOW_CHILD_OK"}],"provenance":{"provider":"deepseek","model":"smoke-model"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[5,6,7,8,9],"surfaceOp":"append"} +{"type":"assistant/message","seq":10,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"WORKFLOW_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[5,6,7,8,9],"surfaceOp":"append"} {"type":"step/end","seq":11,"time":0,"data":{"turn":1,"step":1}} {"type":"turn/end","seq":12,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.jsonl index 1078fe4985..1f2f890b3c 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.jsonl @@ -1,30 +1,30 @@ {"type":"session","version":0,"id":"{{parent}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0} {"type":"turn/start","seq":0,"time":0,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} -{"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"Run the advanced packaged-runtime snapshot scenario."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"Run the advanced packaged-runtime snapshot scenario."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} {"type":"session/title","seq":2,"time":0,"data":{"title":"Run the advanced packaged-runtime snapsh","messageSeqs":[1],"source":{"kind":"fallback"}}} {"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}} -{"type":"request/header","seq":4,"time":0,"data":{"header":{"config":{"provider":"deepseek","model":"smoke-model","reasoningEffort":"high"},"system":"{{system}}","tools":["bash","cordis_inspect","cordis_mount","cordis_unmount","run_code","skill","subagent","task_kill","task_list","task_output","workflow"],"messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}} +{"type":"request/header","seq":4,"time":0,"data":{"header":{"config":{"provider":"deepseek","model":"smoke-model","reasoningEffort":"high"},"system":"{{system}}","tools":["cordis_inspect","cordis_mount","cordis_unmount","run_code","subagent","task_kill","task_list","task_output","workflow"]},"reason":"initial"}} {"type":"assistant/chunk","seq":5,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","seq":6,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-mount","name":"cordis_mount","argumentsDelta":"{\"code\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}"}}} {"type":"assistant/chunk","seq":7,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-mount","name":"cordis_mount","arguments":"{\"code\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}"}}}} {"type":"assistant/chunk","seq":8,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","seq":9,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","seq":10,"time":0,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"advanced-mount","name":"cordis_mount","arguments":"{\"code\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}"}],"provenance":{"provider":"deepseek","model":"smoke-model"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[5,6,7,8,9],"surfaceOp":"append"} +{"type":"assistant/message","seq":10,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-mount","name":"cordis_mount","arguments":"{\"code\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}"}],"source":{"kind":"model","provider":"deepseek","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[5,6,7,8,9],"surfaceOp":"append"} {"type":"tool/call","seq":11,"time":0,"data":{"turn":1,"step":1,"callId":"advanced-mount","name":"cordis_mount","arguments":"{\"code\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}"}} -{"type":"tool/result","seq":12,"time":0,"data":{"turn":1,"step":1,"callId":"advanced-mount","content":[{"type":"text","text":"Temporary Plugin dyn-1 is running (plugin \"\"; available until unmounted or DSH restarts)."}],"isError":false},"sourceEventSeqs":[11],"surfaceOp":"append"} +{"type":"tool/result","seq":12,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-mount"},"content":[{"type":"tool-result","toolCallId":"advanced-mount","content":[{"type":"text","text":"Temporary Plugin dyn-1 is running (plugin \"\"; available until unmounted or DSH restarts)."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[11],"surfaceOp":"append"} {"type":"step/end","seq":13,"time":0,"data":{"turn":1,"step":1}} {"type":"step/start","seq":14,"time":0,"data":{"turn":1,"step":2}} -{"type":"request/header","seq":15,"time":0,"data":{"header":{"config":{"provider":"deepseek","model":"smoke-model","reasoningEffort":"high"},"system":"{{system}}","tools":["bash","cordis_inspect","cordis_mount","cordis_unmount","run_code","skill","snapshot_double","subagent","task_kill","task_list","task_output","workflow"],"messagePrefix":["{{messagePrefix}}"]},"reason":"change"}} +{"type":"request/header","seq":15,"time":0,"data":{"header":{"config":{"provider":"deepseek","model":"smoke-model","reasoningEffort":"high"},"system":"{{system}}","tools":["cordis_inspect","cordis_mount","cordis_unmount","run_code","snapshot_double","subagent","task_kill","task_list","task_output","workflow"]},"reason":"change"}} {"type":"assistant/chunk","seq":16,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","seq":17,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-code","name":"run_code","argumentsDelta":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}} {"type":"assistant/chunk","seq":18,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}}} {"type":"assistant/chunk","seq":19,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","seq":20,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","seq":21,"time":0,"data":{"turn":1,"step":2,"content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}],"provenance":{"provider":"deepseek","model":"smoke-model"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[16,17,18,19,20],"surfaceOp":"append"} +{"type":"assistant/message","seq":21,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}],"source":{"kind":"model","provider":"deepseek","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[16,17,18,19,20],"surfaceOp":"append"} {"type":"tool/call","seq":22,"time":0,"data":{"turn":1,"step":2,"callId":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}} {"type":"tool/code-dispatch-start","seq":23,"time":0,"data":{"parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"snapshot_double","arguments":{"value":21}}} {"type":"tool/code-dispatch","seq":24,"time":0,"data":{"parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"snapshot_double","arguments":{"value":21},"isError":false,"content":[{"type":"text","text":"42"}]}} -{"type":"tool/result","seq":25,"time":0,"data":{"turn":1,"step":2,"callId":"advanced-code","content":[{"type":"text","text":"42"}],"isError":false},"sourceEventSeqs":[22],"surfaceOp":"append"} +{"type":"tool/result","seq":25,"time":0,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-code"},"content":[{"type":"tool-result","toolCallId":"advanced-code","content":[{"type":"text","text":"42"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[22],"surfaceOp":"append"} {"type":"step/end","seq":26,"time":0,"data":{"turn":1,"step":2}} {"type":"step/start","seq":27,"time":0,"data":{"turn":1,"step":3}} {"type":"assistant/chunk","seq":28,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} @@ -32,9 +32,9 @@ {"type":"assistant/chunk","seq":30,"time":0,"data":{"turn":1,"step":3,"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":"assistant/chunk","seq":31,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","seq":32,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","seq":33,"time":0,"data":{"turn":1,"step":3,"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.\"}"}],"provenance":{"provider":"deepseek","model":"smoke-model"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[28,29,30,31,32],"surfaceOp":"append"} +{"type":"assistant/message","seq":33,"time":0,"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.\"}"}],"source":{"kind":"model","provider":"deepseek","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[28,29,30,31,32],"surfaceOp":"append"} {"type":"tool/call","seq":34,"time":0,"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.\"}"}} -{"type":"tool/result","seq":35,"time":0,"data":{"turn":1,"step":3,"callId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false},"sourceEventSeqs":[34],"surfaceOp":"append"} +{"type":"tool/result","seq":35,"time":0,"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":"{{messageId}}"}},"sourceEventSeqs":[34],"surfaceOp":"append"} {"type":"step/end","seq":36,"time":0,"data":{"turn":1,"step":3}} {"type":"step/start","seq":37,"time":0,"data":{"turn":1,"step":4}} {"type":"assistant/chunk","seq":38,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} @@ -42,9 +42,9 @@ {"type":"assistant/chunk","seq":40,"time":0,"data":{"turn":1,"step":4,"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":"assistant/chunk","seq":41,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","seq":42,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","seq":43,"time":0,"data":{"turn":1,"step":4,"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\"}}"}],"provenance":{"provider":"deepseek","model":"smoke-model"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[38,39,40,41,42],"surfaceOp":"append"} +{"type":"assistant/message","seq":43,"time":0,"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-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}],"source":{"kind":"model","provider":"deepseek","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[38,39,40,41,42],"surfaceOp":"append"} {"type":"tool/call","seq":44,"time":0,"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-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}} -{"type":"tool/result","seq":45,"time":0,"data":{"turn":1,"step":4,"callId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false},"sourceEventSeqs":[44],"surfaceOp":"append"} +{"type":"tool/result","seq":45,"time":0,"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-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","seq":46,"time":0,"data":{"turn":1,"step":4}} {"type":"step/start","seq":47,"time":0,"data":{"turn":1,"step":5}} {"type":"assistant/chunk","seq":48,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} @@ -52,17 +52,17 @@ {"type":"assistant/chunk","seq":50,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-unmount","name":"cordis_unmount","arguments":"{\"id\": \"dyn-1\"}"}}}} {"type":"assistant/chunk","seq":51,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","seq":52,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","seq":53,"time":0,"data":{"turn":1,"step":5,"content":[{"type":"tool-call","id":"advanced-unmount","name":"cordis_unmount","arguments":"{\"id\": \"dyn-1\"}"}],"provenance":{"provider":"deepseek","model":"smoke-model"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[48,49,50,51,52],"surfaceOp":"append"} +{"type":"assistant/message","seq":53,"time":0,"data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-unmount","name":"cordis_unmount","arguments":"{\"id\": \"dyn-1\"}"}],"source":{"kind":"model","provider":"deepseek","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[48,49,50,51,52],"surfaceOp":"append"} {"type":"tool/call","seq":54,"time":0,"data":{"turn":1,"step":5,"callId":"advanced-unmount","name":"cordis_unmount","arguments":"{\"id\": \"dyn-1\"}"}} -{"type":"tool/result","seq":55,"time":0,"data":{"turn":1,"step":5,"callId":"advanced-unmount","content":[{"type":"text","text":"Temporary Plugin dyn-1 was unmounted and removed."}],"isError":false},"sourceEventSeqs":[54],"surfaceOp":"append"} +{"type":"tool/result","seq":55,"time":0,"data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-unmount"},"content":[{"type":"tool-result","toolCallId":"advanced-unmount","content":[{"type":"text","text":"Temporary Plugin dyn-1 was unmounted and removed."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[54],"surfaceOp":"append"} {"type":"step/end","seq":56,"time":0,"data":{"turn":1,"step":5}} {"type":"step/start","seq":57,"time":0,"data":{"turn":1,"step":6}} -{"type":"request/header","seq":58,"time":0,"data":{"header":{"config":{"provider":"deepseek","model":"smoke-model","reasoningEffort":"high"},"system":"{{system}}","tools":["bash","cordis_inspect","cordis_mount","cordis_unmount","run_code","skill","subagent","task_kill","task_list","task_output","workflow"],"messagePrefix":["{{messagePrefix}}"]},"reason":"change"}} +{"type":"request/header","seq":58,"time":0,"data":{"header":{"config":{"provider":"deepseek","model":"smoke-model","reasoningEffort":"high"},"system":"{{system}}","tools":["cordis_inspect","cordis_mount","cordis_unmount","run_code","subagent","task_kill","task_list","task_output","workflow"]},"reason":"change"}} {"type":"assistant/chunk","seq":59,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","seq":60,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"text-delta","index":0,"text":"ADVANCED_EXECUTABLE_OK"}}} {"type":"assistant/chunk","seq":61,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}}}} {"type":"assistant/chunk","seq":62,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","seq":63,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":64,"time":0,"data":{"turn":1,"step":6,"content":[{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}],"provenance":{"provider":"deepseek","model":"smoke-model"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[59,60,61,62,63],"surfaceOp":"append"} +{"type":"assistant/message","seq":64,"time":0,"data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}],"source":{"kind":"model","provider":"deepseek","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[59,60,61,62,63],"surfaceOp":"append"} {"type":"step/end","seq":65,"time":0,"data":{"turn":1,"step":6}} {"type":"turn/end","seq":66,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}