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 be7aedb4bd..64c9153ca6 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md -2026-07-10-single-file-executable-sdk-runtime-distribution.md: e5438fc01fdd634cd4d8535dfa37a1de356da04d -2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 218133f077bed7584b6a26fea5f939b64d4fd670 +2026-07-10-single-file-executable-sdk-runtime-distribution.md: e46dbbc119e2078e44632d81b333c8be5ab9d6d7 +2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: d7a1e3f445ea1c1f03df2b349a4391534a5502c3 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 e5438fc01f..e46dbbc119 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 @@ -23,40 +23,40 @@ The exe is packaged with the **`--sea` (enhanced SEA) mode** of [@yao-pkg/pkg](h Terminology reminder: pkg's `/snapshot` VFS has nothing to do with this repo's testing-system "snapshot" (ACP replay expected outputs, `$DSH_SNAPSHOT`); this document says "VFS" for the former. -### The serving interface is a plugin: the two packages sdk/server + sdk/python-runtime +### The serving interface is a plugin inside the dsh application -The deterministic protocol implementation (`server.ts` / `transport.ts`) lands as two packages on the existing `acp/acp` + `examples/acp-demo` pattern — the serving surface is itself a plugin: +The deterministic serving surface is a plugin selected by the packaged `dsh` application: - [`packages/sdk/server`](../../../../packages/sdk/server/README.md) (`@deepseek-ai/dsh-sdk-jsonrpc-server`): the pure protocol plugin; on apply it mounts `HarnessSdkJsonRpcServer` plus a line-delimited JSON-RPC transport on the process stdio, with disposal through `ctx.effect()`. Whether to serve is decided by `cordis.yml`; a yml that does not mount it is a legitimate process that does not serve. Protocol-level exit belongs to the plugin (after answering and flushing the `shutdown` response it disposes the root runtime so persistence drains, then `exit(0)`; an HMR-style unload only stops the service without exiting the process). -- [`packages/sdk/python-runtime`](../../../../packages/sdk/python-runtime/README.md) (`@deepseek-ai/dsh-sdk-python-runtime`): a private packaged entry — `installFailLoud` + `loadEnv` + config discovery + `boot()` from [`dsh-app-boot`](../../../../packages/boot/app-boot/src/index.ts), done once boot completes; the server is brought up by the `dsh-sdk-jsonrpc-server` entry in the yml. Its only dependency is app-boot. Process-level exit belongs to the packaged entry (stdin EOF/SIGTERM → dispose then 0, SIGINT → 130). +- [`apps/cli`](../../../../apps/cli/README.md) (`@deepseek-ai/dsh`): the packaged application entry; its `sdk` profile mounts `dsh-sdk-jsonrpc-server`, and the CLI owns environment layering, profile composition, stdin/signal shutdown, and process exit. -Config discovery has two channels and fails loudly when both are missing: the `DSH_CORDIS_CONFIG` environment variable first (the SDK client convention), then an argv positional argument; no default path and no built-in fallback whatsoever — "the plugins actually booted are decided by an external cordis.yml" is a hard semantic. +The Python client supplies an explicit Harness home and selects the `sdk` profile plus ordered patch files. A missing home, profile, bundle, or server row fails loudly; there is no external complete-config fallback. The [Python profile-runtime decision](2026-08-23-python-sdk-dsh-profile-runtime.md) owns this application surface. ### Plugin resolution: the VFS holds a real package tree, the closure manifest IS the deploy root Inside the exe's VFS sits a **real package tree in build-artifact form** (each package's `lib/` plus a real `node_modules`). The packaged JSON-RPC entry supplies its installed harness base to app-boot's root Include: relative plugin specifiers resolve from the external configuration directory, while bare package names resolve from the VFS, so a configuration inside another Node project cannot shadow the packaged plugin set. The ordinary development bin leaves bare packages configuration-owned. Bare specifiers in the packaged entry resolve upward along `node_modules` from the entry's position inside the VFS and land inside the VFS naturally. The closed set needs no allowlist code — the set is whatever the VFS has installed, and importing a name outside the set fails. -The deploy root is [`python/sdk-runtime/package.json`](../../../../python/sdk-runtime/package.json) (`dsh-sdk-python-runtime-closure`, a pnpm workspace member and a zero-code pure dependency manifest) — the unified source of truth for "which plugins the exe ships" and "what the Python runtime distributes". Adding a plugin to the exe = adding one dependency line to the manifest and repackaging. [`scripts/verify-runtime-closure.ts`](../../../../scripts/verify-runtime-closure.ts) reads every shipped `packages/preset/agent-presets/presets/*/agent.cordis.yml`, evaluates `disabled` conditions that compare `process.platform` for every target in `python/sdk-runtime/platforms.json`, and requires each active workspace plugin at the runtime root through an explicit `workspace:` dependency. It also traverses every workspace package covered by that manifest and requires every non-optional workspace peer, reporting the complete preset or referencing-package → missing-dependency chain; unknown platform conditions remain active so a plugin cannot be omitted by an unsupported expression. `pnpm run hygiene`, CI static, and the single-exe build run it before packaging. Deploy also packs by each package's `files`, so the shared chunks tsdown splits out must be covered by `files`. +The deploy root is [`python/sdk-runtime/package.json`](../../../../python/sdk-runtime/package.json) (`dsh-python-runtime-closure`, a pnpm workspace member and a zero-code pure dependency manifest) — the unified source of truth for "which plugins the exe ships" and "what the Python runtime distributes". Adding a plugin to the exe = adding one dependency line to the manifest and repackaging. [`scripts/verify-runtime-closure.ts`](../../../../scripts/verify-runtime-closure.ts) reads every shipped `packages/preset/agent-presets/presets/*/agent.cordis.yml`, evaluates `disabled` conditions that compare `process.platform` for every target in `python/sdk-runtime/platforms.json`, and requires each active workspace plugin at the runtime root through an explicit `workspace:` dependency. It also traverses every workspace package covered by that manifest and requires every non-optional workspace peer, reporting the complete preset or referencing-package → missing-dependency chain; unknown platform conditions remain active so a plugin cannot be omitted by an unsupported expression. `pnpm run hygiene`, CI static, and the single-exe build run it before packaging. Deploy also packs by each package's `files`, so the shared chunks tsdown splits out must be covered by `files`. The deploy root includes `@deepseek-ai/dsh-mcp-client` as an explicitly supported custom-configuration plugin even though no shipped preset mounts it. An external config can therefore connect to user-supplied stdio and Streamable HTTP MCP servers and register their tools; the distribution does not carry those servers or extend the bridge to MCP Resources and Prompts. The executable and installed-wheel smokes start a temporary stdio server, discover its tool, and complete one model-requested call. ### 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-sdk-python-runtime-closure 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/` → restore any direct workspace package that legacy deploy hoisted back under the source manifest's `node_modules`, omitting its package-local dependency tree and rejecting any remaining manifest gap → replace every staged dependency symlink with its target bytes, remove package-manager `.bin` links, and fail if any symlink remains → inject the pkg configuration (`bin` points at `node_modules/@deepseek-ai/dsh-sdk-python-runtime/lib/packaged-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; CI rebuilds that addon inside the matching manylinux 2.28 container before packaging, and the builder copies it from the root install into the staged closure because legacy deploy omits that side-effect directory. Every target copies its native `@vscode/ripgrep` binary beside the executable as the required `-rg` sidecar; pkg runtimes select that sidecar through `process.pkg`, while ordinary Node execution uses `@vscode/ripgrep` directly. macOS uses its target prebuild and also emits the required `-spawn-helper`. 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 gives pkg a stable single-instance layout that the explicit materialization pass makes symlink-free; disabling automatic peer installation prevents undeclared peers from expanding the closure; link-workspace-packages selects direct workspace dependencies. [`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) overrides the transitive `@deepseek-ai/cosmokit` and `@deepseek-ai/schemastery` semver requests to the pinned vendor sources so legacy deploy never resolves those unpublished names from a registry. +[`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-python-runtime-closure 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/` → restore direct workspace packages omitted by legacy deploy and reject any remaining manifest gap → replace staged dependency symlinks with their target bytes, remove package-manager `.bin` links, and fail if any symlink remains → inject pkg configuration whose bin is `node_modules/@deepseek-ai/dsh/lib/bin.js` and whose assets cover dynamic profile, bundle, frontend, preset, native-library, and configuration reads → stage the target `node-pty` addon → invoke `pkg --sea` once per target → write `deepseek-harness-sdk-runtime--` under `dist-exe/` and copy it into the runtime directory. Linux CI rebuilds `pty.node` inside the matching manylinux 2.28 container because legacy deploy omits that install side effect. Every target copies its native `@vscode/ripgrep` binary beside the executable as the required `-rg` sidecar; pkg runtimes select that sidecar through `process.pkg`, while ordinary Node execution uses `@vscode/ripgrep` directly. macOS uses its target prebuild and also emits the required `-spawn-helper`. All four deploy flags are grounded in measurement: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted gives pkg a stable single-instance layout that the explicit materialization pass makes symlink-free; disabling automatic peer installation prevents undeclared peers from expanding the closure; link-workspace-packages selects direct workspace dependencies. [`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) overrides the transitive `@deepseek-ai/cosmokit` and `@deepseek-ai/schemastery` semver requests to the pinned vendor sources so legacy deploy never resolves those unpublished names from a registry. CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml) is called for all three targets by the [installed-wheel Python runtime pull-request validation](../testing/2026-08-23-installed-python-wheel-black-box-ci.md) and the [public publication workflow](../process/2026-08-11-python-publication-workflow.md); `workflow_dispatch` and the `build-exe` label can still select a subset. Native builds run on linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64, with `~/.pkg-cache` cached, and pkg handles macOS ad-hoc signing. Each leg installs the release-shaped SDK and runtime wheels into a clean venv outside the checkout, proves their package and executable provenance, then drives the complete keyless scenario set through the public SDK and direct NDJSON JSON-RPC. Trusted pull requests additionally run a real DeepSeek two-turn tool smoke on every target; fork and Dependabot heads receive no key. Linux inspects the executable and native addon's GLIBC requirements and runs an additional manylinux 2.28 smoke, while macOS verifies that the executable's deployment target fits the wheel tag. 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 `python-v` 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 the checked-in default `runtime/cordis.yml`, the build-injected platform exe with its required `-rg` sidecar and optional macOS 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-sdk-python-runtime/lib/packaged-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` is the client and `python/sdk-runtime` is the runtime carrier package. The runtime package's data directory holds the build-injected platform executable with its required `-rg` sidecar and optional macOS helper, plus the build-injected `runtime/node/` closure tree for repository development. `resolve_bundled_launch_args()` selects the executable by default; explicit `DSH_RUNTIME_MODE=node` runs `runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js` on system Node 22.19 or newer. The node carrier never enters wheel distributions, and neither carrier uses a checked-in complete `cordis.yml`. [`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) reads the authoritative `X.Y.Z` or prerelease version from the repository root `package.json`, converts prereleases to their PEP 440 spelling, and stages both packages at that wheel version, with `deepseek-harness-sdk` depending exactly on the matching `deepseek-harness-runtime-bin`. An optional `python-v` 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. Staging also carries the repository license into both wheels and the third-party notices into the bundled runtime wheel. The SDK is a `py3-none-any` wheel; each wheel-only runtime package contains one exe and its architecture-matched `-rg` sidecar, and the macOS wheel also contains its architecture-matched spawn helper. Runtime wheels use one of `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, or the conservative `py3-none-macosx_14_0_arm64` tag for the Node 24 executable's macOS 13.5 deployment target; the Hatch hook rejects sdists, universal tags, mixed-platform payloads, missing or extra sidecars, 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-sdk-jsonrpc-server` serving entry, with `!!js` environment-variable fallbacks) via `DSH_CORDIS_CONFIG`. +The Python client launches the packaged `dsh` command with the selected profile (`sdk` by default), ordered patch files, and an explicit Harness home. The profile owns JSON-RPC serving and application composition; missing homes, profiles, bundles, patches, and server rows fail without an external complete-config fallback. ### Naming lineage -`@deepseek-ai/dsh-sdk-python-runtime` (the private carrier) → `dsh-sdk-python-runtime-closure` (the deploy manifest; no scope prefix, so it is not a dsh release package) → `dsh-jsonrpc-agent-pkg--` (the exe artifacts). The wire `serverInfo.name` stays `deepseek-harness-sdk-runtime` (a protocol-stable value); the Python distribution names are `deepseek-harness-sdk` / `deepseek-harness-runtime-bin`, while the import modules remain `deepseek_harness` / `deepseek_harness_runtime`. +`dsh-python-runtime-closure` is the private deploy manifest and `deepseek-harness-sdk-runtime--` is the executable family. The wire `serverInfo.name` is `deepseek-harness-sdk-runtime`; the Python distribution names are `deepseek-harness-sdk` / `deepseek-harness-runtime-bin`, while the import modules are `deepseek_harness` / `deepseek_harness_runtime`. ## Disposition of worker-style plugins 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 218133f077..d7a1e3f445 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 @@ -23,40 +23,40 @@ exe 使用 [@yao-pkg/pkg](https://github.com/yao-pkg/pkg)(vercel/pkg 归档后 术语提醒:pkg 的 `/snapshot` VFS 与本仓库测试体系的「快照」(ACP(Agent Client Protocol)回放预期输出、`$DSH_SNAPSHOT`)无关,本文用「VFS」指前者。 -### 对外服务接口也是插件:sdk/server + sdk/python-runtime 两个包 +### 对外服务接口是 dsh 应用中的插件 -确定性协议实现(`server.ts` / `transport.ts`)按 `acp/acp` + `examples/acp-demo` 的既有模式落为两包——对外服务接口本身也是插件: +确定性服务接口由打包后的 `dsh` 应用选择为插件: - [`packages/sdk/server`](../../../../packages/sdk/server/README.zh.md)(`@deepseek-ai/dsh-sdk-jsonrpc-server`):纯协议插件;执行 `apply` 时,在进程 stdio 上挂载 `HarnessSdkJsonRpcServer` 与按行分隔的 JSON-RPC 传输层,资源释放走 `ctx.effect()`。是否提供服务由 `cordis.yml` 决定;未挂载该插件的配置会启动一个不提供此服务的合法进程。协议级退出归插件所有(应答并确保 `shutdown` 响应发送完毕后,对根运行时执行 dispose(资源释放),让待处理的持久化操作完成,再调用 `exit(0)`;HMR(热模块替换)式卸载只停止服务,不退出进程)。 -- [`packages/sdk/python-runtime`](../../../../packages/sdk/python-runtime/README.zh.md)(`@deepseek-ai/dsh-sdk-python-runtime`):私有打包入口——`installFailLoud` + `loadEnv` + 配置发现 + [`dsh-app-boot`](../../../../packages/boot/app-boot/src/index.ts) 的 `boot()`;`boot()` 完成后入口即完成,服务器由 `cordis.yml` 中的 `dsh-sdk-jsonrpc-server` 条目启动。它只依赖 `app-boot`。进程级退出归打包入口所有(stdin EOF/SIGTERM → dispose 后返回 0,SIGINT → 130)。 +- [`apps/cli`](../../../../apps/cli/README.zh.md)(`@deepseek-ai/dsh`):打包后的应用入口;其 `sdk` profile 挂载 `dsh-sdk-jsonrpc-server`,CLI 负责环境分层、profile 组合、stdin/signal 关闭与进程退出。 -配置发现有两个通道,均缺失时立即报错:优先使用 `DSH_CORDIS_CONFIG` 环境变量(SDK 客户端约定),其次使用 argv 位置参数;没有默认路径或内置回退——「实际启动的插件由外部 `cordis.yml` 决定」是硬语义。 +Python 客户端提供显式 Harness home,并选择 `sdk` profile 与有序 patch 文件。缺失 home、profile、bundle 或 server 配置项都会明确失败;不存在外部完整配置回退。[Python profile 运行时决策](2026-08-23-python-sdk-dsh-profile-runtime.zh.md)负责该应用接口。 ### 插件解析:VFS 装载真实包树,闭包 manifest(元数据清单)就是部署根目录 exe 的 VFS 内是**构建产物形态的真实包树**(各包的 `lib/` + 真实 `node_modules`)。打包专用 JSON-RPC 入口会向 app-boot 的根 Include 提供自身已安装 harness 的基准位置:相对插件说明符从外部配置目录解析,裸包名则从 VFS 解析,因此位于另一个 Node 项目内的配置无法遮蔽已打包的插件集合。普通开发 bin 仍由配置项目提供裸包。打包入口中的裸包名从该入口在 VFS 内的位置沿 `node_modules` 向上解析,自然落在 VFS 内。封闭集不需要白名单代码——VFS 中安装了什么,集合中就有什么;`import()` 集合外的名称会失败。 -部署根目录是 [`python/sdk-runtime/package.json`](../../../../python/sdk-runtime/package.json)(`dsh-sdk-python-runtime-closure`,pnpm 工作区成员、零代码纯依赖 manifest),也是「exe 安装哪些插件」与「Python 运行时分发什么」的统一真源。向 exe 添加插件,就是在 manifest 中增加一行依赖后重新打包。[`scripts/verify-runtime-closure.ts`](../../../../scripts/verify-runtime-closure.ts) 读取每个已发布的 `packages/preset/agent-presets/presets/*/agent.cordis.yml`,针对 `python/sdk-runtime/platforms.json` 中的每个目标解析比较 `process.platform` 的 `disabled` 条件,并要求该目标启用的每个工作区插件都通过显式的 `workspace:` 依赖列在运行时根目录。它还遍历该 manifest 覆盖的全部工作区包,要求每个非可选的工作区对等依赖(peer dependency)都显式列出,并报告“preset 或引用包 → 缺失依赖”的完整链路;无法识别的平台条件会保持启用,避免因不支持的表达式遗漏插件。`pnpm run hygiene`、CI 静态检查与 single-exe 构建都会在打包前运行该门禁。部署还会依据各包的 `files` 字段打包,因此 tsdown 拆出的共享分片必须被 `files` 覆盖。 +部署根目录是 [`python/sdk-runtime/package.json`](../../../../python/sdk-runtime/package.json)(`dsh-python-runtime-closure`,pnpm 工作区成员、零代码纯依赖 manifest),也是「exe 安装哪些插件」与「Python 运行时分发什么」的统一真源。向 exe 添加插件,就是在 manifest 中增加一行依赖后重新打包。[`scripts/verify-runtime-closure.ts`](../../../../scripts/verify-runtime-closure.ts) 读取每个已发布的 `packages/preset/agent-presets/presets/*/agent.cordis.yml`,针对 `python/sdk-runtime/platforms.json` 中的每个目标解析比较 `process.platform` 的 `disabled` 条件,并要求该目标启用的每个工作区插件都通过显式的 `workspace:` 依赖列在运行时根目录。它还遍历该 manifest 覆盖的全部工作区包,要求每个非可选的工作区对等依赖(peer dependency)都显式列出,并报告“preset 或引用包 → 缺失依赖”的完整链路;无法识别的平台条件会保持启用,避免因不支持的表达式遗漏插件。`pnpm run hygiene`、CI 静态检查与 single-exe 构建都会在打包前运行该门禁。部署还会依据各包的 `files` 字段打包,因此 tsdown 拆出的共享分片必须被 `files` 覆盖。 部署根目录显式包含 `@deepseek-ai/dsh-mcp-client`,将其作为自定义配置可用的插件,即使随附 preset 均未挂载该插件。外部配置因此可以连接由用户提供的 stdio 与 Streamable HTTP MCP server 并注册其工具;分发物不包含这些 server,也不将桥接范围扩展到 MCP Resources 和 Prompts。可执行程序与已安装 wheel 包的冒烟测试会启动临时 stdio server,发现其工具,并完成一次由模型请求的调用。 ### 构建流水线与产物 -[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-sdk-python-runtime-closure 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/` → 恢复被 legacy deploy 提升回源 manifest 的 `node_modules` 下的任何直接工作区包,同时省略其包内依赖树,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的每个符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 注入 pkg 配置(`bin` 指向闭包内的 `node_modules/@deepseek-ai/dsh-sdk-python-runtime/lib/packaged-bin.js`;`assets` 使用全量 glob,因为动态 `import()` 对 pkg 静态分析不可见,必须显式打入全部内容)→ 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 可执行文件 `dsh-jsonrpc-agent-pkg--` 写入 `dist-exe/`,并拷回运行时目录。Linux 安装会从源码构建 `pty.node`;CI 会在打包前进入匹配架构的 manylinux 2.28 容器重新构建该 addon,而 `--legacy` 部署会省略这一副作用目录,因此构建器会把它从根安装目录复制到暂存闭包。每个目标都会把对应的原生 `@vscode/ripgrep` 二进制复制到可执行文件旁,作为必需的 `-rg` 伴随文件;pkg 运行时通过 `process.pkg` 选择该伴随文件,普通 Node 执行则直接使用 `@vscode/ripgrep`。macOS 使用对应目标的预构建产物,并额外生成所需的 `-spawn-helper`。CI 将这些产物作为测试中间输入,只保留对应平台的 wheel 包。四个部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 为 pkg 提供稳定的单实例布局,再由显式物化步骤消除符号链接;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 legacy deploy 不会从注册表解析这些未发布名称。 +[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-python-runtime-closure 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/` → 恢复 legacy deploy 遗漏的直接工作区包,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 注入 pkg 配置,其中 bin 为 `node_modules/@deepseek-ai/dsh/lib/bin.js`,assets 覆盖动态读取的 profile、bundle、前端、preset、原生库与配置文件 → 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 将 `deepseek-harness-sdk-runtime--` 写入 `dist-exe/` 并拷回运行时目录。Linux CI 会在匹配的 manylinux 2.28 容器中重新构建 `pty.node`,因为 legacy deploy 会遗漏这一安装副作用。每个目标都会把对应的原生 `@vscode/ripgrep` 二进制复制到可执行文件旁,作为必需的 `-rg` 伴随文件;pkg 运行时通过 `process.pkg` 选择该伴随文件,普通 Node 执行则直接使用 `@vscode/ripgrep`。macOS 使用对应目标的预构建产物,并额外生成所需的 `-spawn-helper`。四个部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 为 pkg 提供稳定的单实例布局,再由显式物化步骤消除符号链接;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 legacy deploy 不会从注册表解析这些未发布名称。 CI 使用 [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml):[安装后 wheel Python 运行时拉取请求验证](../testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)与[公开发布工作流](../process/2026-08-11-python-publication-workflow.zh.md)都会调用它构建全部三个目标;`workflow_dispatch` 与 `build-exe` 标签仍可选择部分目标。linux-x64、linux-arm64(`ubuntu-24.04-arm`)和 macos-arm64 三个平台分别进行原生构建,并缓存 `~/.pkg-cache`;macOS 的 ad-hoc 签名由 pkg 处理。每个平台都把发布形态的 SDK wheel 包与运行时 wheel 包安装到 checkout 外的干净 venv,证明包与可执行文件来源,再通过公开 SDK 与直接 NDJSON JSON-RPC 运行完整 keyless 场景。可信拉取请求还会在每个目标上运行真实 DeepSeek 双轮工具冒烟测试;fork 与 Dependabot head 不会获得密钥。Linux 会检查可执行文件和原生 addon 各自的 GLIBC 依赖,并额外运行 manylinux 2.28 冒烟测试;macOS 则验证可执行文件的部署目标符合 wheel 包标签。完整构建三个目标时保留 4 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 3 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-v` 标签流水线,构建一个 SDK wheel 包和 3 个原生运行时 wheel 包,再由单个串行任务校验并将这 4 个文件发布到项目的 PyPI 注册表。Windows 不在目标范围内。 ### Python SDK 分发:双载体,exe 用于生产,`node` 用于开发 -Python SDK 位于 [`python/`](../../../../python/README.zh.md):`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含检入的默认 `runtime/cordis.yml`、构建注入的平台 exe 及其必需的 `-rg` 伴随文件和可选的 macOS helper,以及构建注入的 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 的自动解析**只查找 exe**;`node` 载体仅在显式设置 `DSH_RUNTIME_MODE=node` 时启用(运行 `runtime/node/node_modules/@deepseek-ai/dsh-sdk-python-runtime/lib/packaged-bin.js`,需要系统 Node ≥22.19),定位为本仓库成员的开发验证通道,不随 wheel 包分发。 +Python SDK 位于 [`python/`](../../../../python/README.zh.md):`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含构建注入的平台可执行文件及其必需的 `-rg` 伴随文件和可选的 macOS helper,以及供仓库开发使用的构建注入 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 默认选择可执行文件;显式设置 `DSH_RUNTIME_MODE=node` 会在系统 Node 22.19 或更高版本上运行 `runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js`。node 载体从不进入 wheel 分发,两种载体都不使用检入的完整 `cordis.yml`。 [`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) 从仓库根目录的 `package.json` 读取权威的 `X.Y.Z` 或预发布版本,把预发布版本转换为 PEP 440 写法,并以该 wheel 包版本暂存两个包,让 `deepseek-harness-sdk` 精确依赖匹配版本的 `deepseek-harness-runtime-bin`。可选的 `python-v` 发布标签只是一项一致性断言,与仓库版本不同时会被拒绝;源码 `pyproject.toml` 中的开发占位版本从不决定发布版本。暂存过程还会把仓库许可证放入两个 wheel 包,并把第三方声明放入内置运行时 wheel 包。SDK 是 `py3-none-any` wheel 包;每个只提供 wheel 包的运行时包都包含一个 exe 及其架构匹配的 `-rg` 伴随文件,macOS wheel 包还包含与其架构匹配的 spawn helper。运行时 wheel 包使用 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64`,或针对 Node 24 可执行文件 macOS 13.5 部署目标而保守选择的 `py3-none-macosx_14_0_arm64` 标签;Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、伴随文件缺失或多余,以及不支持的平台。 -exe「必须显式配置」的硬语义不变;零配置体验由包装层恢复:调用方没有提供 `cordis`、没有显式指定运行时,且环境中没有 `DSH_CORDIS_CONFIG` 时,客户端将检入的默认 `cordis.yml`(`agent-core` + 预载的 `llm-deepseek` + JSONL 持久化 + `bash-local` + `dsh-sdk-jsonrpc-server` 对外服务条目,并通过 `!!js` 使用环境变量兜底)显式注入 `DSH_CORDIS_CONFIG`。 +Python 客户端使用所选 profile(默认 `sdk`)、有序 patch 文件和显式 Harness home 启动打包后的 `dsh` 命令。Profile 负责 JSON-RPC 服务和应用组合;缺失 home、profile、bundle、patch 或 server 配置项都会失败,不存在外部完整配置回退。 ### 命名血统 -`@deepseek-ai/dsh-sdk-python-runtime`(私有载体)→ `dsh-sdk-python-runtime-closure`(部署 manifest;没有作用域前缀,因此不属于 dsh 发布包)→ `dsh-jsonrpc-agent-pkg--`(exe 产物)。协议字段 `serverInfo.name` 保持为 `deepseek-harness-sdk-runtime`(协议稳定值);Python 分发包名为 `deepseek-harness-sdk` / `deepseek-harness-runtime-bin`,导入模块名仍为 `deepseek_harness` / `deepseek_harness_runtime`。 +`dsh-python-runtime-closure` 是私有部署 manifest,`deepseek-harness-sdk-runtime--` 是可执行文件族。协议字段 `serverInfo.name` 是 `deepseek-harness-sdk-runtime`;Python 分发包名是 `deepseek-harness-sdk` / `deepseek-harness-runtime-bin`,导入模块名是 `deepseek_harness` / `deepseek_harness_runtime`。 ## 工作线程插件 diff --git a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml index f43fae952f..a70ebb7db2 100644 --- a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md -2026-08-05-profile-plugin-bundles.md: c9f685eecddcd4ea8d8580becef3c32a9693a329 -2026-08-05-profile-plugin-bundles.zh.md: 3f2db5ea7653fde356f6ae8f21898f1fdada3926 +2026-08-05-profile-plugin-bundles.md: 493568691dac3a54185f11cbbf8162bf6b6355b1 +2026-08-05-profile-plugin-bundles.zh.md: adfa95b6f8d0fdd6fe3c0ebbc7a62d935ebb1987 diff --git a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md index c9f685eecd..493568691d 100644 --- a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md +++ b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md @@ -12,7 +12,7 @@ The `dsh` launcher hardcoded its compositions: `base.cordis.yml` + `web.cordis.y Everything becomes a **profile**: a directory `$DSH_HOME/profiles/` with a `package.json` (pnpm-managed out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list) and a user `cordis.patch.yml`. A **bundle** is an npm package declaring `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; the two manifest kinds live under distinct `dsh.profile` / `dsh.bundle` keys so a package.json states which role it plays. The tree composes over an empty root by applying each bundle's patch in `dsh.profile.bundles` order, then the user layer and `--patch` overlays — one `applyEntryPatches` call shared by boot and `--dump-config`. App invocation values later moved from launcher-derived patches to startup services in the [app-owned command-line decision](2026-08-06-app-owned-command-line.md). -The default Profile templates use `@deepseek-ai/dsh-base` (shared core rows), `@deepseek-ai/dsh-web-app` (browser Host rows and Web runtime glue), and `@deepseek-ai/dsh-headless` (a direct one-shot runner over base, without web-app). Generic `dsh --profile ` hands its remaining arguments to that profile's command-line startup row: Web owns its flag family, while headless owns its task positional. Patch overlays use launcher-owned `--patch`. `dsh plugin --profile ` is a thin pnpm forwarder that initializes the profile and reconciles `dsh.profile.bundles` with installed bundle declarations; a package without a bundle declaration remains a plain dependency. [Headless as a direct core entry point](2026-08-09-headless-direct-core-entry-point.md) owns the headless composition contract. +The default Profile templates use `@deepseek-ai/dsh-base` as the shared core for `web`, `headless`, `sdk`, and `acp`, with one mode bundle above it. The [standalone `sdk-minimal` profile](2026-08-24-standalone-sdk-minimal-profile.md) instead lists one bundle that owns its complete explicit tree. Generic `dsh --profile ` hands its remaining arguments to that profile's command-line startup row: Web owns its flag family, headless owns its task positional, and the protocol profiles accept no app options. Patch overlays use launcher-owned `--patch`. `dsh plugin --profile ` is a thin pnpm forwarder that initializes the profile and reconciles `dsh.profile.bundles` with installed bundle declarations; a package without a bundle declaration remains a plain dependency. [Headless as a direct core entry point](2026-08-09-headless-direct-core-entry-point.md) owns the headless composition contract. Resolution is two-anchored by construction: `dsh.profile.bundles` names resolve from the dsh installation first, then the profile directory — so in-box bundles always come from the same installation as the running `dsh` and pnpm never manages them — while bare plugin names in patch rows resolve through the profile directory's Node parent-walk into the maintained flat fallback `$DSH_HOME/profiles/node_modules` (one symlink per package the installation's app and bundles depend on, healed on every launch). @@ -27,7 +27,7 @@ Two supporting refactors: the webserver's built-in static dist serving became th ## Consequences -- New composition surfaces (a TUI, provider packs) ship as ordinary npm packages installable per profile; the repository no longer needs a row for every deployment shape. +- New composition surfaces (a TUI, provider packs) ship as ordinary npm packages installable per profile, without a repository row for every deployment shape. - `apps/cli` shrank to argv parsing, profile machinery consumption, and the pnpm forwarder; `AppCLIEntry` and the per-surface boot paths are gone. - The keyless web e2e scaffold boots the same bundle layers over the same empty-root shape as production, including the profiles module fallback, so composition drift between test and product fails loudly. -- Backends reject nothing old on disk (pre-release stance): `$DSH_HOME/config.yaml` is simply no longer read. +- Under the pre-release stance, backends carry no compatibility behavior for old on-disk configuration; `$DSH_HOME/config.yaml` is ignored. diff --git a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.zh.md b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.zh.md index 3f2db5ea76..adfa95b6f8 100644 --- a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.zh.md @@ -12,7 +12,7 @@ Status: implemented 一切都变成 **profile**:即目录 `$DSH_HOME/profiles/`,其中包含一个 `package.json`(pnpm 管理的树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表)和一份用户 `cordis.patch.yml`。**组合包**(bundle)是声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;两种 manifest 分别位于互不相同的 `dsh.profile` / `dsh.bundle` 键下,因此一份 package.json 能说明自己扮演哪种角色。配置树在空的根之上组合:按 `dsh.profile.bundles` 顺序应用每个组合包的 patch,然后是用户层与 `--patch` overlay——启动与 `--dump-config` 共享同一条 `applyEntryPatches` 路径。随后,[应用持有命令行的决策](2026-08-06-app-owned-command-line.zh.md)又把调用期取值从启动器派生的 patch 迁移到了启动服务。 -默认 Profile 模板使用的组合包是 `@deepseek-ai/dsh-base`(共享核心配置行)、`@deepseek-ai/dsh-web-app`(浏览器 Host 配置行与 Web 运行时粘合层)和 `@deepseek-ai/dsh-headless`(直接叠加在 base 上且不含 web-app 的一次性 runner)。通用的 `dsh --profile ` 把剩余参数交给该 profile 的命令行启动行:Web 持有自己的 flag 家族,headless 则持有任务位置参数。patch overlay 使用启动器持有的 `--patch`。`dsh plugin --profile ` 是一层薄薄的 pnpm 转发器,负责初始化 profile,并依据已安装包的组合包声明调和 `dsh.profile.bundles`;没有组合包声明的包保持为普通依赖。[Headless 作为直接 core 入口](2026-08-09-headless-direct-core-entry-point.zh.md)负责 headless 组合约定。 +默认 Profile 模板为 `web`、`headless`、`sdk` 与 `acp` 使用 `@deepseek-ai/dsh-base` 作为共享核心,并在其上叠加一个模式组合包。[独立 `sdk-minimal` profile](2026-08-24-standalone-sdk-minimal-profile.zh.md)则只列出一个拥有完整显式配置树的组合包。通用的 `dsh --profile ` 把剩余参数交给该 profile 的命令行启动行:Web 持有自己的 flag 家族,headless 持有任务位置参数,协议 profile 不接受应用选项。patch overlay 使用启动器持有的 `--patch`。`dsh plugin --profile ` 是一层薄薄的 pnpm 转发器,负责初始化 profile,并依据已安装包的组合包声明调和 `dsh.profile.bundles`;没有组合包声明的包保持为普通依赖。[Headless 作为直接 core 入口](2026-08-09-headless-direct-core-entry-point.zh.md)负责 headless 组合约定。 解析在构造上就是双锚点的:`dsh.profile.bundles` 中的名称先从 dsh 安装目录解析,再从 profile 目录解析——因此内置组合包始终来自与运行中 `dsh` 相同的安装,pnpm 从不管理它们——而 patch 行中的裸插件名称经 profile 目录的 Node 父目录逐级查找,落到受维护的扁平回退目录 `$DSH_HOME/profiles/node_modules`(安装目录的应用与各组合包所依赖的每个包各一个符号链接,每次启动时修复)。 @@ -27,7 +27,7 @@ Status: implemented ## Consequences -- 新的组合表层(TUI、提供方扩展包)以普通 npm 包形式交付,可按 profile 安装;仓库不再需要为每种部署形态各留一行。 +- 新的组合表层(TUI、提供方扩展包)以普通 npm 包形式交付,可按 profile 安装,无需在仓库中为每种部署形态各留一行。 - `apps/cli` 收缩为 argv 解析、profile 机制的消费方和 pnpm 转发器;`AppCLIEntry` 与各表层专属的启动路径全部移除。 - 无密钥 web e2e 脚手架以与生产相同的空根形态启动相同的组合包层,包括 profiles 模块回退,因此测试与产品之间的组合漂移会响亮失败。 -- 后端不拒绝磁盘上的任何旧格式(发布前姿态):`$DSH_HOME/config.yaml` 只是不再被读取。 +- 按发布前姿态,后端不携带旧磁盘配置的兼容行为;`$DSH_HOME/config.yaml` 会被忽略。 diff --git a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.i18n.yaml index 7c2052c0a3..7342dd6abc 100644 --- a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md -2026-08-10-fork-children-stay-one-shot.md: 44b947a3e0580263f1973aaf24534b7b2f01c0b6 -2026-08-10-fork-children-stay-one-shot.zh.md: acb12c54fa37d4462cac1b1035bc74d5a96ea719 +2026-08-10-fork-children-stay-one-shot.md: b2d9a77d7cc9969e517a4f5d6973aa2aee1134f5 +2026-08-10-fork-children-stay-one-shot.zh.md: b5dc1a7fda4e2b4152baf63e9c49b89bcebdeef0 diff --git a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md index 44b947a3e0..b2d9a77d7c 100644 --- a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md +++ b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md @@ -1,4 +1,4 @@ -# Agent Note: Forked children stay one-shot +# Agent Note: Cache-preserving forked children stay one-shot Status: implemented @@ -12,7 +12,7 @@ The child-scoped `report` return channel is now the largest such addition, and s ## Decision -Every shipped composition binds the fork delegation tool to `backgroundMode: one-shot`: [the base bundle](../../../../packages/bundle/base/cordis.patch.yml), [the ACP example](../../../../examples/acp-agent/cordis.yml), and [the headless example](../../../../examples/headless-agent/cordis.yml). The base bundle leaves `run_in_background` available, because it mounts a task service; the two examples set `enableRunInBackground: false`, because they mount none and a one-shot background start would otherwise fail at call time on a missing `tasks` service. +The cache-preserving compositions bind the fork delegation tool to `backgroundMode: one-shot`: [the base bundle](../../../../packages/bundle/base/cordis.patch.yml), [the ACP example](../../../../examples/acp-agent/cordis.yml), and [the headless example](../../../../examples/headless-agent/cordis.yml). The base bundle leaves `run_in_background` available, because it mounts a task service; the two examples set `enableRunInBackground: false`, because they mount none and a one-shot background start would otherwise fail at call time on a missing `tasks` service. The standard, code, and Cordis CLI presets instead bind fork to `continuable`; their child-scoped `report` additions invalidate the inherited prefix and accept the recomputation cost described here. One-shot children — foreground and background alike — are created through `SubagentRuntime.start()`, which never enters the continuable activation-setup registry, so neither `report` nor its prompt section is installed. A forked one-shot child's system prompt and tool schemas therefore equal its parent's, apart from the `persona` and `toolFilter` deltas a deployment opts into per delegation tool. @@ -20,9 +20,9 @@ One-shot children — foreground and background alike — are created through `S ### The restriction is composition, not code -`ForkInProcessProvider.prepareContinuable` stays implemented and `ctx.subagents.startContinuable()` still accepts `fork`; only the shipped `cordis.yml` rows changed. `tool-subagent` knows both the provider's `inheritsParentContext` and its own `backgroundMode` at mount, so a load-time rejection of the pair was available and is deliberately not added: the pair is not wrong in general. It is wrong only while a child-scope delta precedes inherited history, and the package that creates that delta — [`dsh-tool-subagent-report`](../../../../packages/subagent/tool-subagent-report/README.md) — is separately installable and, by its own design, invisible to `tool-subagent`. A deployment that omits the report package can run continuable forked children with the prefix intact. Encoding one roster's consequence as a delegation-tool invariant would make the tool assert something it cannot observe. +`ForkInProcessProvider.prepareContinuable` stays implemented and `ctx.subagents.startContinuable()` accepts `fork`; composition chooses whether the fork tool is one-shot or continuable. `tool-subagent` knows both the provider's `inheritsParentContext` and its own `backgroundMode` at mount, so a load-time rejection of the pair is available and deliberately absent: the pair is not wrong in general. It is costly only while a child-scope delta precedes inherited history, and the package that creates that delta — [`dsh-tool-subagent-report`](../../../../packages/subagent/tool-subagent-report/README.md) — is separately installable and, by its own design, invisible to `tool-subagent`. A deployment that omits the report package can run continuable forked children with the prefix intact. Encoding one roster's consequence as a delegation-tool invariant would make the tool assert something it cannot observe. -The reintroduction condition is recorded as a `TODO(fork-continuable-prefix-reuse)` marker on `prepareContinuable` itself, the one method the shipped compositions do not call, and tracked as issue #2124: continuable fork reopens when a child's system prompt and tool schemas can match its parent's byte for byte. +The cache-preserving condition is recorded as a `TODO(fork-continuable-prefix-reuse)` marker on `prepareContinuable` and tracked as issue #2124: continuable fork preserves its inherited prefix when the child's system prompt and tool schemas can match the parent's byte for byte. ## Alternatives considered @@ -30,7 +30,7 @@ The reintroduction condition is recorded as a `TODO(fork-continuable-prefix-reus **Stop mounting the fork provider at all.** This was the broader form of the restriction. Rejected because foreground fork *is* the prefix-reusing case and is untouched by the report channel, so a full ban gives up the capability without buying anything the one-shot binding does not already buy — and would leave no shipped composition exercising session seeding. -**Ship continuable forked children and accept the loss.** Rejected because the loss is total rather than marginal: reuse breaks ahead of the inherited history, so the child pays full prefill on a transcript it duplicated for the sole purpose of not paying it. A deployment that wants a long-lived child with no inherited context already has `spawn`. +**Use continuable forked children in cache-preserving compositions and accept the loss.** Rejected for the base bundle and ACP/headless examples because the loss is total rather than marginal: reuse breaks ahead of the inherited history, so the child pays full prefill on a transcript it duplicated for the sole purpose of not paying it. The CLI presets make the other tradeoff and retain continuable fork. A deployment that wants a long-lived child with no inherited context already has `spawn`. **Make `report` visible to every Agent.** A global registration would restore byte-identical prefixes by giving parent and child the same schema and section. Rejected because roots, one-shot children, remote children, and agentless callers would advertise a tool with no derivable recipient, and execution-time rejection would make schema visibility disagree with authority — the scope-local decision the [report tool Agent Note](../feature/2026-07-30-continuable-subagent-report-tool.md) already settled. @@ -38,12 +38,12 @@ The reintroduction condition is recorded as a `TODO(fork-continuable-prefix-reus ## Consequences -- No shipped composition creates a continuable forked child; `subagent_fork` returns a result to its caller's turn, and `send_message` addresses only spawned children. -- A forked child's request prefix stays byte-identical to its parent's unless the deployment configures `persona` or `toolFilter` on the fork delegation tool, so the token cost of seeding buys provider-side reuse again. -- The fork provider's continuable path has no production caller and no assembled-composition coverage. It keeps its package-level tests, and the seam still accepts it, so a bundle or `--patch` overlay can reintroduce it with no code change and no warning. +- The base bundle and ACP/headless examples create only one-shot forked children; their `subagent_fork` returns a result to the caller's turn, and `send_message` addresses only spawned children there. The three CLI presets create continuable forked children. +- A one-shot forked child's request prefix stays byte-identical to its parent's unless the deployment configures `persona`, `toolFilter`, or a different LLM route on the fork delegation tool, so the token cost of seeding can buy provider-side reuse. Continuable fork adds `report` before the inherited history and forfeits that reuse. +- The fork provider's continuable path has CLI production callers and package-level tests. The same seam accepts one-shot composition, so a bundle or `--patch` overlay can choose either lifecycle without a code change or warning. - `subagent_fork`'s model-visible schema changes: the continuable background wording is replaced by the one-shot task wording in the base bundle, and disappears entirely from the two examples. The affected keyless snapshot tool-schema sidecars are re-recorded in the same change. -- The report obligation's reach narrows to spawned children in shipped deployments. Its default `next-step` scheduling, authority model, and coverage remain independent of fork composition. +- The report obligation reaches spawned children in every continuable composition and forked children in the CLI presets. Its default `next-step` scheduling, authority model, and coverage remain independent of fork composition. ### Accepted risks -The constraint lives in three configuration files and a code comment, not in a gate. A future bundle row or profile patch can set `backgroundMode: continuable` on a fork tool and silently reintroduce the prefix loss; nothing fails loud. That is the accepted cost of not encoding one roster's consequence into `tool-subagent`. +The one-shot constraint lives in three configuration files and a code comment, not in a gate; the CLI preset rows already choose `backgroundMode: continuable` and incur the prefix loss. Any bundle or profile patch can make either choice without a warning. That is the accepted cost of not encoding one roster's consequence into `tool-subagent`. diff --git a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md index acb12c54fa..b5dc1a7fda 100644 --- a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md @@ -1,4 +1,4 @@ -# Agent Note: fork 出的 child 保持 one-shot +# Agent Note: 保留缓存的 fork child 保持 one-shot Status: implemented @@ -12,7 +12,7 @@ fork 与 spawn 的唯一区别是 child 的 Session 会以 parent 已完成轮 ## 决策 -所有随附组合都把 fork 委派工具绑定为 `backgroundMode: one-shot`:[base 组合包](../../../../packages/bundle/base/cordis.patch.yml)、[ACP 示例](../../../../examples/acp-agent/cordis.yml)与[headless 示例](../../../../examples/headless-agent/cordis.yml)。base 组合包保留 `run_in_background`,因为它挂载了 task 服务;两个示例设置 `enableRunInBackground: false`,因为它们都不挂载 task 服务,否则一次 one-shot 后台启动会在调用时因缺少 `tasks` 服务而失败。 +保留缓存的组合会把 fork 委派工具绑定为 `backgroundMode: one-shot`:[base 组合包](../../../../packages/bundle/base/cordis.patch.yml)、[ACP 示例](../../../../examples/acp-agent/cordis.yml)与[headless 示例](../../../../examples/headless-agent/cordis.yml)。base 组合包保留 `run_in_background`,因为它挂载了 task 服务;两个示例设置 `enableRunInBackground: false`,因为它们都不挂载 task 服务,否则一次 one-shot 后台启动会在调用时因缺少 `tasks` 服务而失败。standard、code 与 Cordis CLI preset 则把 fork 绑定为 `continuable`;其子级作用域的 `report` 增量会使继承前缀失效,并接受这里说明的重算成本。 one-shot child——前台与后台皆然——经由 `SubagentRuntime.start()` 创建,该路径从不进入可继续的 activation setup 注册表,因此 `report` 与它的提示词 section 都不会被安装。于是一个 fork 出的 one-shot child 的系统提示词与工具 schema 与其 parent 相同,只差部署逐个委派工具主动选择的 `persona` 与 `toolFilter` 增量。 @@ -20,9 +20,9 @@ one-shot child——前台与后台皆然——经由 `SubagentRuntime.start()` ### 该限制在于组合,不在于代码 -`ForkInProcessProvider.prepareContinuable` 仍然实现完好,`ctx.subagents.startContinuable()` 也仍接受 `fork`;改动的只有随附的 `cordis.yml` 行。`tool-subagent` 在挂载时同时知道提供方的 `inheritsParentContext` 与自身的 `backgroundMode`,因此一个加载期拒绝该组合的检查是可行的,而这里刻意不加:该组合并非普遍错误。它只在某个 child 作用域增量位于继承历史之前时才是错的,而产生该增量的包——[`dsh-tool-subagent-report`](../../../../packages/subagent/tool-subagent-report/README.zh.md)——是独立安装的,并且按其自身设计对 `tool-subagent` 不可见。一个不安装 report 包的部署可以在前缀完好的前提下运行可继续的 fork child。把某一份插件清单的后果写成委派工具的不变量,会让该工具断言它无法观察到的事实。 +`ForkInProcessProvider.prepareContinuable` 仍然实现完好,`ctx.subagents.startContinuable()` 也接受 `fork`;组合会选择 fork 工具采用 one-shot 还是 continuable。`tool-subagent` 在挂载时同时知道提供方的 `inheritsParentContext` 与自身的 `backgroundMode`,因此一个加载期拒绝该组合的检查是可行的,而这里刻意不加:该组合并非普遍错误。只有在某个 child 作用域增量位于继承历史之前时,它才会产生高昂成本,而产生该增量的包——[`dsh-tool-subagent-report`](../../../../packages/subagent/tool-subagent-report/README.zh.md)——是独立安装的,并且按其自身设计对 `tool-subagent` 不可见。一个不安装 report 包的部署可以在前缀完好的前提下运行可继续的 fork child。把某一份插件清单的后果写成委派工具的不变量,会让该工具断言它无法观察到的事实。 -重新开放的条件记录为 `prepareContinuable` 方法上的 `TODO(fork-continuable-prefix-reuse)` 标记——随附组合不调用这个方法——并由 issue #2124 跟踪:当 child 的系统提示词与工具 schema 能与其 parent 逐字节一致时,可继续 fork 即可重新开放。 +保留缓存的条件记录为 `prepareContinuable` 方法上的 `TODO(fork-continuable-prefix-reuse)` 标记,并由 issue #2124 跟踪:当 child 的系统提示词与工具 schema 能与其 parent 逐字节一致时,可继续 fork 就能保留继承前缀。 ## 备选方案 @@ -30,7 +30,7 @@ one-shot child——前台与后台皆然——经由 `SubagentRuntime.start()` **干脆不挂载 fork 提供方。** 这是该限制更彻底的形式。否决的原因是前台 fork *正是*复用前缀的那种情形,且不受 report 通道影响,因此全面禁用会在不换来任何 one-shot 绑定尚未换来的东西的同时放弃该能力——并且随附组合将没有任何一个演练 session 初始内容。 -**照常随附可继续的 fork child 并接受这份损失。** 否决的原因是这份损失是全额而非边际的:复用在继承历史之前就已中断,于是 child 为一份自己复制过来、目的恰恰是不必付费的 transcript 付了全额预填充。想要一个没有继承上下文的长期 child 的部署,本来就有 `spawn`。 +**在保留缓存的组合中随附可继续的 fork child 并接受这份损失。** base 组合包与 ACP/headless 示例不采用,因为这份损失是全额而非边际的:复用在继承历史之前就已中断,于是 child 为一份自己复制过来、目的恰恰是不必付费的 transcript 付了全额预填充。CLI preset 选择了另一项取舍并保留可继续 fork。想要一个没有继承上下文的长期 child 的部署,本来就有 `spawn`。 **让 `report` 对每个 Agent 可见。** 全局注册会通过让 parent 与 child 拥有相同的 schema 与 section 来恢复逐字节相同的前缀。否决的原因是根 agent、one-shot child、远端 child 与无 agent 调用方都会宣告一件推导不出收件方的工具,而执行期拒绝会让 schema 可见性与权限彼此矛盾——这正是[report 工具 Agent Note](../feature/2026-07-30-continuable-subagent-report-tool.zh.md)已经定下的作用域局部决策。 @@ -38,12 +38,12 @@ one-shot child——前台与后台皆然——经由 `SubagentRuntime.start()` ## 后果 -- 没有任何随附组合会创建可继续的 fork child;`subagent_fork` 把结果返回给调用方的轮次,而 `send_message` 只寻址 spawn 出的 child。 -- 除非部署在 fork 委派工具上配置了 `persona` 或 `toolFilter`,fork child 的请求前缀与其 parent 逐字节相同,因此初始内容的 token 成本重新换来了提供方侧的复用。 -- fork 提供方的可继续路径没有生产调用方,也没有整体组装层面的覆盖。它保留自己的包内测试,seam 也仍然接受它,因此某个组合包或 `--patch` 覆盖层可以无需改动代码、也不会有任何警告地把它重新引入。 +- base 组合包与 ACP/headless 示例只创建 one-shot fork child;其中的 `subagent_fork` 会把结果返回给调用方的轮次,`send_message` 也只寻址 spawn 出的 child。三个 CLI preset 会创建可继续的 fork child。 +- 除非部署在 fork 委派工具上配置了 `persona`、`toolFilter` 或不同的 LLM 路由,one-shot fork child 的请求前缀会与其 parent 逐字节相同,因此初始内容的 token 成本可以换来提供方侧的复用。可继续 fork 会在继承历史之前增加 `report`,从而失去该复用。 +- fork 提供方的可继续路径有 CLI 生产调用方与包内测试。同一条 seam 也接受 one-shot 组合,因此某个组合包或 `--patch` 覆盖层可以无需改动代码、也不会有任何警告地选择任一生命周期。 - `subagent_fork` 面向模型的 schema 发生变化:base 组合包中可继续的后台措辞被 one-shot 的 task 措辞取代,在两个示例中则完全消失。受影响的无密钥快照工具 schema 伴随文件在同一次改动中重新记录。 -- 在随附部署中,report 义务的覆盖范围收窄到 spawn 出的 child。它的 `next-step` 默认调度、权限模型与覆盖仍独立于 fork 组合。 +- 在每个可继续组合中,report 义务都会覆盖 spawn 出的 child;在 CLI preset 中,它也覆盖 fork 出的 child。它的 `next-step` 默认调度、权限模型与覆盖仍独立于 fork 组合。 ### 已接受的风险 -该限制存在于三个配置文件与一处代码注释中,而不在门禁里。未来某个组合包行或 profile 补丁可以在 fork 工具上设置 `backgroundMode: continuable`,从而悄然重新引入前缀损失;没有任何东西会失败得很响亮。这就是不把某一份插件清单的后果写入 `tool-subagent` 所接受的代价。 +one-shot 限制存在于三个配置文件与一处代码注释中,而不在门禁里;CLI preset 行已经选择 `backgroundMode: continuable` 并承担前缀损失。任何组合包或 profile 补丁都能选择任一方式,且不会收到警告。这就是不把某一份插件清单的后果写入 `tool-subagent` 所接受的代价。 diff --git a/.agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.i18n.yaml index 6f7a67dd4d..d3d02c9f70 100644 --- a/.agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md -2026-08-11-repository-naming-contract-and-rename-ledger.md: 2de88df5f1a5037d32daa9a8ee9cd1edfc60528a -2026-08-11-repository-naming-contract-and-rename-ledger.zh.md: 0cbbbee561358614da0b20e49c610fbee1d4e537 +2026-08-11-repository-naming-contract-and-rename-ledger.md: 403a7288bb3af0eff09b229e7d399b94492ae6c0 +2026-08-11-repository-naming-contract-and-rename-ledger.zh.md: 644e1c0bebfc43414e9e67def8d6c42acddf4c72 diff --git a/.agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md b/.agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md index 2de88df5f1..403a7288bb 100644 --- a/.agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md +++ b/.agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md @@ -274,11 +274,11 @@ Keep MCP, Todo, and the Plan Mode package, key, events, and tool names. This dec | `E2BSandboxService` | `E2BRuntime` | The class creates, reuses, and disposes the E2B execution environment used by filesystem and subprocess adapters. It is broader than one sandbox handle and narrower than a generic owner. Keep `@deepseek-ai/dsh-e2b`, `ctx.e2b`, and the `e2b/` group. | | `@deepseek-ai/dsh-frontend-static` | `@deepseek-ai/dsh-host-frontend-static` | The package is the Host plugin that serves the frontend assets. The prefix distinguishes it from frontend application code. | | `PluginInventoryService` | `PluginInventoryGateway` | The class is a Remote-only adapter from the live Loader tree to the `pluginInventory/list` RPC. It owns no same-process service, cache, history, or mutation path. `Gateway` states the role that exists. | -| `@deepseek-ai/dsh-jsonrpc-demo`, `@deepseek-ai/dsh-sdk-jsonrpc-demo` | `@deepseek-ai/dsh-sdk-python-runtime` | The private package carries only the temporarily separate Python SDK runtime; the [single dsh launcher decision](2026-08-22-single-dsh-application-launcher.md) owns its application-boundary change. | -| `packages/examples/jsonrpc-demo/` | `packages/sdk/python-runtime/` | The carrier is production packaging infrastructure for the Python SDK, not a demo bundle. | -| `examples/jsonrpc-agent/` | `examples/python-sdk-agent/` | The direct-config runnable example belongs specifically to the Python SDK exception. | +| `@deepseek-ai/dsh-jsonrpc-demo`, `@deepseek-ai/dsh-sdk-jsonrpc-demo`, `@deepseek-ai/dsh-sdk-python-runtime` | removed | The Python runtime packages the existing `@deepseek-ai/dsh` CLI and its `sdk` profile; a private application package would recreate a second launcher. | +| `packages/examples/jsonrpc-demo/`, `packages/sdk/python-runtime/` | removed | The Python runtime wheel's closure manifest owns packaging without a separate application package. | +| `examples/jsonrpc-agent/` | `examples/python-sdk-agent/` | The example demonstrates Python use of the `sdk` profile and ordered patches. | | `@deepseek-ai/dsh-acp-demo` | `@deepseek-ai/dsh-acp-app` | The package is the ACP profile's application bundle, not a standalone demo bin. | -| Deploy-root manifest `dsh-jsonrpc-agent-pkg` | `dsh-sdk-python-runtime-closure` | The manifest defines the private Python runtime dependency closure. The Python-visible executable basename remains fixed until its documented profile migration. | +| Deploy-root manifests `dsh-jsonrpc-agent-pkg`, `dsh-sdk-python-runtime-closure` | `dsh-python-runtime-closure` | The zero-code manifest defines the Python runtime wheel's complete `dsh` dependency closure without naming a separate SDK application. | | `@deepseek-ai/dsh-frontend` | `@deepseek-ai/dsh-web-frontend` | The application is the web frontend. Keep its physical `apps/web/` folder. | Keep atomic-write, brand, native-command, timeout utility, directory-picker, `dsh-base`, `dsh-web-app`, `dsh-sdk-app`, `dsh-acp-app`, app boot, CLI names, and the `headless` package, bundle, and example identity. `headless` is the intended product essence and may later support more than one-shot execution. diff --git a/.agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.zh.md b/.agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.zh.md index 0cbbbee561..644e1c0beb 100644 --- a/.agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.zh.md @@ -274,11 +274,11 @@ PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui` | `E2BSandboxService` | `E2BRuntime` | 该类创建、复用和释放文件系统与子进程适配器所使用的 E2B 执行环境。它比单个沙箱句柄的职责更广,又比通用所有者更具体。保留 `@deepseek-ai/dsh-e2b`、`ctx.e2b` 和 `e2b/` 组。 | | `@deepseek-ai/dsh-frontend-static` | `@deepseek-ai/dsh-host-frontend-static` | 该包是提供前端资源的 Host 插件。此前缀可将它与前端应用代码区分开。 | | `PluginInventoryService` | `PluginInventoryGateway` | 该类只负责把实时 Loader 树适配到 `pluginInventory/list` RPC。它不拥有同进程服务、缓存、历史或修改路径。`Gateway` 准确说明现有角色。 | -| `@deepseek-ai/dsh-jsonrpc-demo`、`@deepseek-ai/dsh-sdk-jsonrpc-demo` | `@deepseek-ai/dsh-sdk-python-runtime` | 该私有包只承载暂时独立的 Python SDK 运行时;其应用边界变更由[单一 dsh 启动器决策](2026-08-22-single-dsh-application-launcher.zh.md)负责。 | -| `packages/examples/jsonrpc-demo/` | `packages/sdk/python-runtime/` | 该载体是 Python SDK 的生产打包基础设施,不是演示组合包。 | -| `examples/jsonrpc-agent/` | `examples/python-sdk-agent/` | 直读配置的可运行示例专属于 Python SDK 例外。 | +| `@deepseek-ai/dsh-jsonrpc-demo`、`@deepseek-ai/dsh-sdk-jsonrpc-demo`、`@deepseek-ai/dsh-sdk-python-runtime` | 已删除 | Python 运行时打包现有 `@deepseek-ai/dsh` CLI 与其 `sdk` profile;私有应用包会重新产生第二个启动器。 | +| `packages/examples/jsonrpc-demo/`、`packages/sdk/python-runtime/` | 已删除 | Python 运行时 wheel 的闭包 manifest 负责打包,无需独立应用包。 | +| `examples/jsonrpc-agent/` | `examples/python-sdk-agent/` | 该示例演示 Python 使用 `sdk` profile 与有序 patch。 | | `@deepseek-ai/dsh-acp-demo` | `@deepseek-ai/dsh-acp-app` | 该包是 ACP profile 的应用组合包,不是独立 demo bin。 | -| 部署根 manifest `dsh-jsonrpc-agent-pkg` | `dsh-sdk-python-runtime-closure` | 该 manifest 定义私有 Python 运行时依赖闭包。面向 Python 的可执行文件基本名称保持不变,直至完成已记录的 profile 迁移。 | +| 部署根 manifest `dsh-jsonrpc-agent-pkg`、`dsh-sdk-python-runtime-closure` | `dsh-python-runtime-closure` | 该零代码 manifest 定义 Python 运行时 wheel 的完整 `dsh` 依赖闭包,不再命名独立 SDK 应用。 | | `@deepseek-ai/dsh-frontend` | `@deepseek-ai/dsh-web-frontend` | 该应用是 Web 前端。保留其物理目录 `apps/web/`。 | 保留 atomic-write、brand、native-command、timeout 实用工具、目录选择器、`dsh-base`、`dsh-web-app`、`dsh-sdk-app`、`dsh-acp-app`、应用启动、CLI(命令行界面)名称,以及 `headless` 包、组合包和示例身份。`headless` 是预期的产品本质,未来也可以支持不止一次性执行。 diff --git a/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.i18n.yaml index 1e9ba16226..61028c878a 100644 --- a/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md -2026-08-22-single-dsh-application-launcher.md: 69188e806d9192d230d1f1b52daf27a3b30481be -2026-08-22-single-dsh-application-launcher.zh.md: a6bb1909b019ea0d386bd2f4a1bb8f5199ef1974 +2026-08-22-single-dsh-application-launcher.md: 48a45cb2454b5532a78474203b6d88aef3dd0697 +2026-08-22-single-dsh-application-launcher.zh.md: dbf2fb3a5bdc16208b0435482d8d0851bd7c44d7 diff --git a/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md b/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md index 69188e806d..48a45cb245 100644 --- a/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md +++ b/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md @@ -8,19 +8,19 @@ English | [中文](2026-08-22-single-dsh-application-launcher.zh.md) DeepSeek Harness application processes need one owner for composition, plugin resolution, environment discovery, shutdown, and user customization. A dedicated app bin with a complete `cordis.yml` creates a second lifecycle beside profile launch: plugins installed into a profile do not reach it, behavior drifts from `dsh-base`, and SDK callers learn arbitrary process argv instead of the product's composition model. -The Python SDK distributes a native executable and three platform wheels whose embedded direct-config runtime cannot change launch architecture without rebuilding and validating the complete VFS closure. That distribution needs an explicit temporary exception, not a second general Node application pattern. +The Python SDK distributes a native executable and three platform wheels. Its packaged process must use the same profile launcher while preserving the closed VFS dependency tree, native sidecars, and installed-wheel evidence. ## Decision ### Launch scope -Every supported Node application starts through the `dsh` CLI and one named profile. The shipped application commands are `dsh web`, `dsh --profile headless`, `dsh --profile sdk`, and `dsh --profile acp`; `dsh web` is the deliberate convenience alias for `--profile web`, not another application entry. +Every supported Node application starts through the `dsh` CLI and one named profile. The shipped application commands are `dsh web`, `dsh --profile headless`, `dsh --profile sdk`, `dsh --profile sdk-minimal`, and `dsh --profile acp`; `dsh web` is the deliberate convenience alias for `--profile web`, not another application entry. Vendor CLIs, build-only and test-only executables, direct in-process plugin mounting, and the private browser WebWorker preview are outside the application-launch inventory. A package app bin or root demo that launches a package entry is not an accepted extension point. ### Profile applications -`@deepseek-ai/dsh-sdk-app` and `@deepseek-ai/dsh-acp-app` compose the protocol applications over `@deepseek-ai/dsh-base`. The SDK bundle adds the JSON-RPC server plus app-owned help and stdio lifetime; the ACP bundle adds the automation-only ACP server plus the same application responsibilities. Both adopt the base model, tools, persistence, settings, credentials, policy, and environment behavior. +`@deepseek-ai/dsh-sdk-app` and `@deepseek-ai/dsh-acp-app` compose the full protocol applications over `@deepseek-ai/dsh-base`. The SDK bundle adds the JSON-RPC server plus app-owned help and stdio lifetime; the ACP bundle adds the automation-only ACP server plus the same application responsibilities. Both adopt the base model, tools, persistence, settings, credentials, policy, and environment behavior. The [standalone sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.md) reuses SDK startup and JSON-RPC serving but deliberately owns a complete explicit tree without `dsh-base`. Profile manifests own patch reload: @@ -29,11 +29,12 @@ Profile manifests own patch reload: | `web` | `live` | | `headless` | `startup` | | `sdk` | `startup` | +| `sdk-minimal` | `startup` | | `acp` | `startup` | Custom profiles default to `live`. A startup profile still applies its bundle, profile, home-level, and invocation `--patch` layers, but it does not watch them after boot. `dsh-base` inserts the module-HMR row disabled; a profile with a tested source-module reload lifecycle must enable it explicitly. None of the shipped profiles enable server module HMR: `patchReload: live` uses the launcher's config-only watcher while the startup profiles install no watcher. SDK and ACP cannot safely replace their server, agents, persistence, or tool registry inside one owned stdio connection. -The shipped protocol profiles reserve stdout for protocol frames, expose help without starting transport, and route stdin EOF and signals through bounded root disposal. ACP remains automation-only. The SDK JSON-RPC methods, notification fields, and `initialize.serverInfo.name` remain stable. Model-visible tool and persistence defaults come from `dsh-base`, and runnable snapshots own those assembled application outputs. +The shipped protocol profiles reserve stdout for protocol frames, expose help without starting transport, and route stdin EOF and signals through bounded root disposal. ACP remains automation-only. The SDK JSON-RPC methods, notification fields, and `initialize.serverInfo.name` remain stable. Full-profile model-visible tool and persistence defaults come from `dsh-base`; `sdk-minimal` owns its explicit defaults. Runnable snapshots own the assembled application outputs. ### TypeScript SDK customization @@ -43,25 +44,21 @@ SDK users customize plugins through profiles. `dsh plugin --profile ...` Direct SDK use follows normal Harness-home resolution: explicit `dshHome`, inherited `DSH_HOME`, then `~/.dsh`. `subagent-dsh-sdk` instead requires an explicit absolute home, so a nested runtime cannot discover a person's profiles, installed plugins, credentials, or sessions through the operating-system home. DSH-specific ACP child examples also pass an isolated home; the ACP backend itself remains generic for non-DSH agents. -### Python exception and names +### Python runtime -The Python SDK's direct-config application lives in the private `packages/sdk/python-runtime` package named `@deepseek-ai/dsh-sdk-python-runtime`. Its only packaged executable entry is `lib/packaged-bin.js`, consumed by the private `dsh-sdk-python-runtime-closure` deploy root. It has no public npm bin. The runnable direct Python example is `examples/python-sdk-agent`. +The Python runtime wheel packages the ordinary `@deepseek-ai/dsh` CLI from `node_modules/@deepseek-ai/dsh/lib/bin.js` through the private `dsh-python-runtime-closure` deploy manifest. The Python client selects `dsh --profile sdk` by default, ordered patch files, and an explicit Harness home; the runnable Python example selects `sdk-minimal`. The installed `dsh` console command exposes the same profile grammar and the separately packaged `web` application. -Python-observable behavior remains fixed: Python API, SDK wire, default `cordis.yml`, environment variables, wheel distribution names, packaged executable names, sidecar names, explicit runtime options, zero-config behavior, and supported platforms. The stable SDK family remains `@deepseek-ai/dsh-sdk-client`, `@deepseek-ai/dsh-sdk-protocol`, `@deepseek-ai/dsh-sdk-jsonrpc-server`, and wire identity `deepseek-harness-sdk-runtime`; `@deepseek-ai/dsh-acp` remains the ACP protocol plugin. There is no compatibility package, forwarding executable, fallback parser, or SDK/ACP launcher alias. +The executable family is `deepseek-harness-sdk-runtime--`. The SDK wire, wheel and import distribution names, sidecar names, and wire identity `deepseek-harness-sdk-runtime` remain stable. The SDK package family is `@deepseek-ai/dsh-sdk-client`, `@deepseek-ai/dsh-sdk-protocol`, and `@deepseek-ai/dsh-sdk-jsonrpc-server`; `@deepseek-ai/dsh-acp` remains the ACP protocol plugin. There is no Python-specific Node application, checked-in complete config, compatibility package, forwarding executable, fallback parser, or SDK/ACP launcher alias. ### Enforcement -`verify-application-entrypoints` scans application/package manifests, executable sources, and root demo scripts. The allowlist classifies the `dsh` product bin, vendor-excluded scope, the private WebWorker build tool, test support, and the private Python carrier. An unclassified shebang, a new package bin, or a demo wrapper that bypasses `apps/cli/src/bin.ts` fails hygiene and the primary/static CI aggregates. - -## Deferred Python migration - -The Python runtime follow-up must move the packaged process through `dsh --profile sdk`, preserve the wheel's closed dependency and native sidecar behavior, and delete `@deepseek-ai/dsh-sdk-python-runtime`. Only after those conditions pass on Linux x64, Linux arm64, and macOS arm64 does the executable family change from `dsh-jsonrpc-agent-pkg--` to `deepseek-harness-sdk-runtime--`. The temporary carrier and current artifact names make that obligation visible without weakening current Python compatibility. +`verify-application-entrypoints` scans application/package manifests, executable sources, and root demo scripts. The allowlist classifies the `dsh` product bin, vendor-excluded scope, the private WebWorker build tool, and test support. An unclassified shebang, a new package bin, or a demo wrapper that bypasses `apps/cli/src/bin.ts` fails hygiene and the primary/static CI aggregates. ## Existing decisions and supersession This decision supersedes the application-launch and package-name facts in [profile plugin bundles](2026-08-05-profile-plugin-bundles.md), [TypeScript SDK client and subagent backend](../feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md), [remove the SDK project toolchain](../simplification/2026-08-11-remove-sdk-project-toolchain.md), and [single-file Python SDK runtime distribution](2026-07-10-single-file-executable-sdk-runtime-distribution.md). Those notes retain independent authority for profile layering, client/wire semantics, deleted project tooling, and native packaging. -The [ACP automation-only protocol](../simplification/2026-07-23-acp-automation-only-protocol.md) remains authoritative for ACP wire and interaction scope. The [repository naming contract](2026-08-11-repository-naming-contract-and-rename-ledger.md) remains authoritative for role-based package names. No active note is fully superseded or eligible for archival. +The [ACP automation-only protocol](../simplification/2026-07-23-acp-automation-only-protocol.md) remains authoritative for ACP wire and interaction scope. The [repository naming contract](2026-08-11-repository-naming-contract-and-rename-ledger.md) remains authoritative for role-based package names. The [standalone sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.md) partially supersedes this note's base-first rule and complete-tree alternative while retaining this note's launcher ownership. No active note is fully superseded or eligible for archival. ## Alternatives considered @@ -69,7 +66,7 @@ The [ACP automation-only protocol](../simplification/2026-07-23-acp-automation-o **Keep forwarding compatibility bins.** Rejected: a forwarding executable remains another public launch name and compatibility promise. The pre-release repository can move callers directly to profiles. -**Put complete standalone Cordis trees behind profile wrappers.** Rejected: that centralizes argv without centralizing application composition. `dsh-base` plus thin app bundles gives shared policy one owner while retaining protocol-specific negative guarantees. +**Put caller-supplied complete Cordis trees behind profile wrappers.** Rejected: that centralizes argv without centralizing application composition. Full profiles use `dsh-base` plus thin app bundles so shared policy has one owner. A repository-owned, versioned standalone bundle is allowed only when an explicit roster is the product behavior, as [sdk-minimal](2026-08-24-standalone-sdk-minimal-profile.md) records. **Accept inline plugins or a complete `cordis.yml` in the TypeScript constructor.** Rejected: the SDK would become another package installer and application composer. Named profiles and patch files already provide persistent and per-launch customization through one resolution model. @@ -83,19 +80,19 @@ The [ACP automation-only protocol](../simplification/2026-07-23-acp-automation-o ## Verification -- Source and built CLI acceptance cover `sdk` and `acp` help, transport startup, stdout purity, EOF, signals, and root disposal. +- Source and built CLI acceptance cover `sdk`, `sdk-minimal`, and `acp` help, transport startup, stdout purity, EOF, signals, and root disposal. - Bundle configuration tests pin module HMR disabled in `dsh-base` and absent from shipped mode overrides; the custom live-profile e2e pins config reload through the launcher's watch-only fallback. - Focused unit suites cover profile launch resolution, initialization bounds, SDK retries, server readiness, and nested isolated homes with 100% coverage on the changed runtime sources. - Keyless ACP and SDK snapshots boot real `dsh` profiles and pin protocol output plus persisted logs; the nested SDK composition boots a second real profile runtime. - The real-API workflow caps file parallelism at four because one profile e2e file can own several complete `dsh` subprocess trees; workflow tests pin that resource bound. -- The Python suite exercises exe and node carriers; all packaged-runtime scenarios, native macOS executable construction, both wheels, and clean-wheel default/MCP smokes retain the existing artifact names. +- The Python suite exercises exe and node carriers; packaged-runtime scenarios, native macOS executable construction, both wheels, and clean-wheel default/MCP smokes pin the `deepseek-harness-sdk-runtime-*` artifacts and profile launch. - `verify-application-entrypoints` includes invalid fixtures for package bins, executable sources, package-launching demo wrappers, and unclassified demos. ## Consequences - A user changes an SDK application's plugin composition through a named profile and ordered patches, using the same installation and resolution model as every other dsh application. - A custom profile receives live config watching without server module HMR and opts into source-module replacement only through an explicit row override. -- SDK and ACP share the complete base application and one set of policy and tools; snapshots present intentional assembled differences explicitly. +- The full SDK and ACP profiles share the complete base application and one set of policy and tools; `sdk-minimal` owns its explicit standalone roster, and snapshots present intentional assembled differences. - Adding `@deepseek-ai/dsh` increases the TypeScript client's install size in exchange for a deterministic same-version runtime. - Trusted user patches can add a plugin that writes to stdout and corrupt their own protocol stream; shipped profiles guarantee purity, not arbitrary third-party composition. -- Python keeps a visibly private, narrowly allowed direct-config carrier until its platform artifact migration is independently proven. +- Python packages the ordinary `dsh` profile launcher while retaining a closed native runtime and no system-Node requirement for wheel users. diff --git a/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.zh.md b/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.zh.md index a6bb1909b0..dbf2fb3a5b 100644 --- a/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.zh.md @@ -8,19 +8,19 @@ Status: implemented DeepSeek Harness 应用进程需要由同一个机制负责组合、插件解析、环境发现、关闭和用户自定义。带完整 `cordis.yml` 的专用应用 bin 会在 profile 启动之外形成第二套生命周期:安装到 profile 的插件无法到达它,行为会与 `dsh-base` 偏离,SDK 调用方还需要学习任意进程 argv,而不是产品的组合模型。 -Python SDK 分发一个原生可执行文件和三个平台 wheel 包;其中嵌入的直读配置运行时只有在重建并验证完整 VFS 闭包后才能改变启动架构。该分发需要一个明确的临时例外,而不是另一种通用 Node 应用模式。 +Python SDK 分发一个原生可执行文件和三个平台 wheel 包。其打包进程必须使用同一 profile 启动器,同时保留封闭的 VFS 依赖树、原生伴随文件与 installed-wheel 证据。 ## Decision ### 启动范围 -所有受支持的 Node 应用都通过 `dsh` CLI 与一个具名 profile 启动。随附应用命令是 `dsh web`、`dsh --profile headless`、`dsh --profile sdk` 与 `dsh --profile acp`;`dsh web` 是刻意为 `--profile web` 保留的便捷别名,不是另一个应用入口。 +所有受支持的 Node 应用都通过 `dsh` CLI 与一个具名 profile 启动。随附应用命令是 `dsh web`、`dsh --profile headless`、`dsh --profile sdk`、`dsh --profile sdk-minimal` 与 `dsh --profile acp`;`dsh web` 是刻意为 `--profile web` 保留的便捷别名,不是另一个应用入口。 Vendor CLI、仅用于构建和测试的可执行文件、进程内直接挂载插件以及私有浏览器 WebWorker 预览都不属于应用启动清单。包应用 bin 或直接启动包入口的根 demo 都不是可接受的扩展点。 ### Profile 应用 -`@deepseek-ai/dsh-sdk-app` 与 `@deepseek-ai/dsh-acp-app` 在 `@deepseek-ai/dsh-base` 之上组合协议应用。SDK 组合包增加 JSON-RPC 服务器、应用自有帮助和 stdio 生命周期;ACP 组合包增加仅用于自动化的 ACP 服务器与相同的应用职责。两者都采用 base 层的模型、工具、持久化、settings、credentials、策略和环境行为。 +`@deepseek-ai/dsh-sdk-app` 与 `@deepseek-ai/dsh-acp-app` 在 `@deepseek-ai/dsh-base` 之上组合完整协议应用。SDK 组合包增加 JSON-RPC 服务器、应用自有帮助和 stdio 生命周期;ACP 组合包增加仅用于自动化的 ACP 服务器与相同的应用职责。两者都采用 base 层的模型、工具、持久化、settings、credentials、策略和环境行为。[独立 sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.zh.md)复用 SDK 启动与 JSON-RPC 服务,但刻意拥有不含 `dsh-base` 的完整显式配置树。 Profile manifest 负责 patch 重载: @@ -29,11 +29,12 @@ Profile manifest 负责 patch 重载: | `web` | `live` | | `headless` | `startup` | | `sdk` | `startup` | +| `sdk-minimal` | `startup` | | `acp` | `startup` | 自定义 profile 默认为 `live`。`startup` profile 仍会应用组合包、profile、home 级与调用时 `--patch` 各层,但启动后不会监视这些文件。`dsh-base` 插入的模块 HMR(热模块替换)配置项默认禁用;具有经过验证的源码模块重载生命周期的 profile 必须显式启用它。随附 profile 均不启用服务器模块 HMR:`patchReload: live` 使用启动器的仅配置 watcher,`startup` profile 则不安装 watcher。SDK 与 ACP 无法在一个自有 stdio 连接内安全替换其服务器、agent、持久化或工具注册表。 -随附协议 profile 将 stdout 保留给协议帧,显示帮助时不启动 transport,并通过有界根节点 dispose(资源释放)处理 stdin EOF 与信号。ACP 继续仅用于自动化。SDK JSON-RPC 方法、通知字段与 `initialize.serverInfo.name` 保持稳定。模型可见工具与持久化默认值来自 `dsh-base`,可运行快照负责钉住这些已组装的应用输出。 +随附协议 profile 将 stdout 保留给协议帧,显示帮助时不启动 transport,并通过有界根节点 dispose(资源释放)处理 stdin EOF 与信号。ACP 继续仅用于自动化。SDK JSON-RPC 方法、通知字段与 `initialize.serverInfo.name` 保持稳定。完整 profile 的模型可见工具与持久化默认值来自 `dsh-base`;`sdk-minimal` 拥有自己的显式默认值。可运行快照负责固定已组装的应用输出。 ### TypeScript SDK 自定义 @@ -43,25 +44,21 @@ SDK 用户通过 profile 自定义插件。`dsh plugin --profile ...` 管 直接使用 SDK 时遵循普通 Harness home 解析:显式 `dshHome`、继承的 `DSH_HOME`,最后是 `~/.dsh`。`subagent-dsh-sdk` 则要求显式绝对 home,因此嵌套运行时不会通过操作系统 home 发现个人 profile、已安装插件、凭据或会话。DSH 专用 ACP 子进程示例同样传入隔离 home;ACP 后端自身继续适用于非 DSH agent。 -### Python 例外与命名 +### Python 运行时 -Python SDK 的直读配置应用位于私有 `packages/sdk/python-runtime` 包,名称是 `@deepseek-ai/dsh-sdk-python-runtime`。它唯一的打包可执行入口是 `lib/packaged-bin.js`,由私有 `dsh-sdk-python-runtime-closure` 部署根消费。它没有公开 npm bin。可运行的直启 Python 示例是 `examples/python-sdk-agent`。 +Python 运行时 wheel 通过私有 `dsh-python-runtime-closure` 部署 manifest,打包来自 `node_modules/@deepseek-ai/dsh/lib/bin.js` 的普通 `@deepseek-ai/dsh` CLI。Python 客户端默认选择 `dsh --profile sdk`、有序 patch 文件与显式 Harness home;可运行 Python 示例选择 `sdk-minimal`。安装的 `dsh` 控制台命令暴露相同 profile 语法与单独打包的 `web` 应用。 -Python 可观察行为保持不变:Python API、SDK 协议格式、默认 `cordis.yml`、环境变量、wheel 包分发名称、打包可执行文件名称、伴随文件名称、显式运行时选项、零配置行为与支持平台。稳定 SDK 包族继续是 `@deepseek-ai/dsh-sdk-client`、`@deepseek-ai/dsh-sdk-protocol`、`@deepseek-ai/dsh-sdk-jsonrpc-server`,协议 identity 继续是 `deepseek-harness-sdk-runtime`;`@deepseek-ai/dsh-acp` 继续作为 ACP 协议插件。仓库不保留兼容包、转发可执行文件、后备解析器或 SDK/ACP 启动别名。 +可执行文件族是 `deepseek-harness-sdk-runtime--`。SDK 协议格式、wheel 与 import 分发名称、伴随文件名称,以及协议 identity `deepseek-harness-sdk-runtime` 保持稳定。SDK 包族是 `@deepseek-ai/dsh-sdk-client`、`@deepseek-ai/dsh-sdk-protocol` 与 `@deepseek-ai/dsh-sdk-jsonrpc-server`;`@deepseek-ai/dsh-acp` 继续作为 ACP 协议插件。仓库不保留 Python 专用 Node 应用、检入的完整配置、兼容包、转发可执行文件、后备解析器或 SDK/ACP 启动别名。 ### 强制校验 -`verify-application-entrypoints` 扫描应用/包 manifest、可执行源码和根 demo 脚本。允许清单对 `dsh` 产品 bin、排除的 vendor 范围、私有 WebWorker 构建工具、测试支持以及私有 Python 载体进行分类。未分类的 shebang、新包 bin 或绕过 `apps/cli/src/bin.ts` 的 demo wrapper 都会使 hygiene 与 primary/static CI 聚合失败。 - -## 暂缓的 Python 迁移 - -Python 运行时后续工作必须把打包进程迁移到 `dsh --profile sdk`,保持 wheel 包的封闭依赖与原生伴随文件行为,并删除 `@deepseek-ai/dsh-sdk-python-runtime`。只有这些条件在 Linux x64、Linux arm64 与 macOS arm64 全部通过后,可执行文件族才会从 `dsh-jsonrpc-agent-pkg--` 改名为 `deepseek-harness-sdk-runtime--`。临时载体与当前产物名称使这项义务清晰可见,同时不削弱当前 Python 兼容性。 +`verify-application-entrypoints` 扫描应用/包 manifest、可执行源码和根 demo 脚本。允许清单对 `dsh` 产品 bin、排除的 vendor 范围、私有 WebWorker 构建工具和测试支持进行分类。未分类的 shebang、新包 bin 或绕过 `apps/cli/src/bin.ts` 的 demo wrapper 都会使 hygiene 与 primary/static CI 聚合失败。 ## 既有决策与取代关系 本决策取代 [profile 插件组合包](2026-08-05-profile-plugin-bundles.zh.md)、[TypeScript SDK 客户端与 SDK subagent 后端](../feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md)、[移除 SDK 项目工具链](../simplification/2026-08-11-remove-sdk-project-toolchain.zh.md)和[单文件 Python SDK 运行时分发](2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md)中的应用启动与包名事实。这些 Note 对 profile 分层、客户端/协议语义、已删除的项目工具链与原生打包仍分别具有独立权威。 -[ACP 仅自动化协议](../simplification/2026-07-23-acp-automation-only-protocol.zh.md)继续负责 ACP 协议格式与交互范围。[仓库命名约定](2026-08-11-repository-naming-contract-and-rename-ledger.zh.md)继续负责基于角色的包名。没有任何活跃 Note 被完全取代,也没有 Note 符合归档条件。 +[ACP 仅自动化协议](../simplification/2026-07-23-acp-automation-only-protocol.zh.md)继续负责 ACP 协议格式与交互范围。[仓库命名约定](2026-08-11-repository-naming-contract-and-rename-ledger.zh.md)继续负责基于角色的包名。[独立 sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.zh.md)部分取代本 Note 的 base 优先规则与完整配置树替代方案,同时保留本 Note 对 launcher 所有权的决策。没有任何活跃 Note 被完全取代,也没有 Note 符合归档条件。 ## 考虑过的替代方案 @@ -69,7 +66,7 @@ Python 运行时后续工作必须把打包进程迁移到 `dsh --profile sdk` **保留转发兼容 bin。** 拒绝:转发可执行文件仍然形成另一个公开启动名称与兼容承诺。预发布仓库可以让调用方直接迁移到 profile。 -**把完整独立 Cordis 树放到 profile wrapper 后面。** 拒绝:这只集中 argv,没有集中应用组合。`dsh-base` 加轻量应用组合包让共享策略只有一个归属,同时保留协议专属的负面保证。 +**把调用方提供的完整 Cordis 树放到 profile wrapper 后面。** 拒绝:这只集中 argv,没有集中应用组合。完整 profile 使用 `dsh-base` 加轻量应用组合包,使共享策略只有一个归属。只有当显式清单本身属于产品行为时,才允许仓库自有且有版本的独立组合包,具体见 [sdk-minimal](2026-08-24-standalone-sdk-minimal-profile.zh.md)。 **在 TypeScript 构造函数中接受内联插件或完整 `cordis.yml`。** 拒绝:SDK 会因此成为另一个包安装器和应用组合器。具名 profile 与 patch 文件已通过统一解析模型提供持久与逐次启动自定义。 @@ -83,19 +80,19 @@ Python 运行时后续工作必须把打包进程迁移到 `dsh --profile sdk` ## 验证 -- 源码与构建后 CLI 验收覆盖 `sdk` 和 `acp` 的帮助、transport 启动、stdout 纯净性、EOF、信号与根节点 dispose。 -- 组合包配置测试钉住 `dsh-base` 默认禁用模块 HMR,随附模式覆盖层不再重复该策略;自定义 live profile 的 e2e 钉住启动器仅监视 fallback 提供的配置重载。 +- 源码与构建后 CLI 验收覆盖 `sdk`、`sdk-minimal` 和 `acp` 的帮助、transport 启动、stdout 纯净性、EOF、信号与根节点 dispose。 +- 组合包配置测试钉住 `dsh-base` 默认禁用模块 HMR,随附模式覆盖层不含该策略;自定义 live profile 的 e2e 钉住启动器仅监视 fallback 提供的配置重载。 - 聚焦单元套件覆盖 profile 启动解析、初始化时限、SDK 重试、服务器就绪和嵌套隔离 home,并对变更后的运行时源码实现 100% 覆盖率。 - 免密钥 ACP 与 SDK 快照启动真实 `dsh` profile,并钉住协议输出与持久化日志;嵌套 SDK 组合会启动第二个真实 profile 运行时。 - 真实 API 工作流把文件并行度限制为 4,因为一个 profile e2e 文件可能拥有多个完整 `dsh` 子进程树;工作流测试会钉住该资源上限。 -- Python 套件同时测试 exe 与 node 载体;全部打包运行时场景、原生 macOS 可执行文件构建、两个 wheel 包以及干净 wheel 默认/MCP 冒烟测试都保留既有产物名称。 +- Python 套件同时测试 exe 与 node 载体;打包运行时场景、原生 macOS 可执行文件构建、两个 wheel 包以及干净 wheel 默认/MCP 冒烟测试会钉住 `deepseek-harness-sdk-runtime-*` 产物与 profile 启动。 - `verify-application-entrypoints` 包含包 bin、可执行源码、直启包的 demo wrapper 与未分类 demo 等非法 fixture(测试前置数据)。 ## 影响 - 用户通过具名 profile 与有序 patch 更改 SDK 应用的插件组合,使用与其他所有 dsh 应用相同的安装与解析模型。 - 自定义 profile 可以在不启用服务器模块 HMR 的情况下获得实时配置监视,只有显式覆盖配置项才会启用源码模块替换。 -- SDK 与 ACP 共享完整 base 应用和同一份策略与工具;快照以显式差异呈现刻意采用的组装变化。 +- 完整 SDK 与 ACP profile 共享完整 base 应用和同一份策略与工具;`sdk-minimal` 拥有自己的显式独立清单,快照会呈现这些刻意采用的组装差异。 - 增加 `@deepseek-ai/dsh` 会扩大 TypeScript 客户端的安装体积,换来确定的同版本运行时。 - 受信任用户 patch 可以增加写入 stdout 的插件并破坏自己的协议流;随附 profile 保证纯净,不为任意第三方组合提供保证。 -- Python 保留一个清晰可见的私有直读配置载体,直到其平台产物迁移得到独立证明。 +- Python 打包普通 `dsh` profile 启动器,同时保留封闭原生运行时,wheel 用户无需系统 Node。 diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml new file mode 100644 index 0000000000..f418c62b83 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md +2026-08-23-python-sdk-dsh-profile-runtime.md: 3298224688e8f0cd4216f8ed68d9e4364014d2a9 +2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 400e70113e1112aa6e4979c64a5046199c07e8a0 diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md new file mode 100644 index 0000000000..3298224688 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md @@ -0,0 +1,55 @@ +# Agent Note: Python SDK runtime through the dsh profile launcher + +Status: implemented + +English | [中文](2026-08-23-python-sdk-dsh-profile-runtime.zh.md) + +## Problem + +The Python SDK distributed a private Node application that booted a complete external `cordis.yml`, while every other supported application entered through `dsh` profiles. That exception duplicated environment loading, configuration ownership, plugin resolution, shutdown, artifact names, and test paths. It also made SDK customization an all-or-nothing application tree: a caller replacing one plugin had to own the JSON-RPC server and every unrelated deployment row. + +A normal profile cannot be adopted only at the Python wrapper. The runtime executable must contain the `dsh` CLI, shipped profile and bundle files, native libraries, and a module-resolution path that works when profile files and external plugins live outside pkg's virtual filesystem. + +## Decision + +### One application launcher + +The runtime executable packages `@deepseek-ai/dsh` and runs its ordinary command grammar. The Python client selects `--profile sdk` by default, forwards ordered absolute `--patch` paths, and may select another `dsh` executable or profile. The runnable minimal example selects the shipped `sdk-minimal` profile. The private `@deepseek-ai/dsh-sdk-python-runtime` application package and checked-in runtime `cordis.yml` do not exist. JSON-RPC serving remains the `@deepseek-ai/dsh-sdk-app` bundle and `@deepseek-ai/dsh-sdk-jsonrpc-server` plugin, not a Python-owned boot path. + +The public Python configuration is `dsh_bin`, `profile`, ordered `patches`, `dsh_home`, process cwd/environment, provider/model/token selection, a bounded initialization timeout, and optional turn/shutdown timeouts. It does not expose a complete Cordis tree or arbitrary launch argv. `RunResult` reports the protocol-owned run values and does not duplicate the profile's persistence path. + +Every Python launch requires either explicit `dsh_home` or a non-empty `DSH_HOME` in the child environment. The SDK never discovers `~/.dsh`. The selected home consistently owns profiles, external plugins, credentials, settings, and sessions. + +### Plugin customization + +Persistent SDK customization uses the same profile interfaces as direct CLI use. `dsh plugin --profile ...` manages external dependencies and bundle order, `$DSH_HOME/profiles//cordis.patch.yml` owns persistent row changes, the home patch applies machine-local changes across profiles, and Python `patches` supplies invocation-specific overlays. A selected profile is valid only when it retains an SDK server row. Missing profiles, bundles, server rows, and invalid patches fail without a complete-config fallback; a profile that remains alive without serving JSON-RPC fails the independently bounded initialization handshake with a diagnostic naming that profile. + +The [standalone sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.md) lists one repository-owned bundle that inserts its complete explicit tree without `dsh-base`. Its persistent Bash and string-replace editor are present by composition rather than a server filter; dynamic runtime context, workspace instructions, settings, managed credentials, telemetry, compaction, and every other base row are absent. The same runtime still packages the full `sdk` and `web` profiles as separate choices. + +The runtime wheel installs a `dsh` console command. Ordinary profile and SDK execution remains Node-free; external package management requires a caller-installed `pnpm`. + +### Executable packaging + +The zero-code deployment manifest is `dsh-python-runtime-closure`. It packages `node_modules/@deepseek-ai/dsh/lib/bin.js` and profile, bundle, preset, native-addon, and shared-library assets into `deepseek-harness-sdk-runtime--`. The wheel distribution names, Python import modules, JSON-RPC messages, and wire-stable `serverInfo.name = deepseek-harness-sdk-runtime` remain unchanged. + +Plain Node profiles use symlinks in `$DSH_HOME/profiles/node_modules` to share installation packages with external plugins. An operating-system symlink cannot traverse pkg's `/snapshot` filesystem, so the packaged CLI writes small real ESM proxy packages instead. Each proxy resolves the source package's explicit ESM export map directly under Node import conditions, exposes targets that exist in the installation, and re-exports their virtual module URLs. Export rows without an ESM runtime target and executable-only or declaration-only packages produce no unusable proxy entry; malformed export maps fail startup. A complete matching generation returns without acquiring the cross-process writer lock. A missing or stale entry acquires the lock, rechecks the generation, and repairs it without exposing partial proxies; either carrier can replace the other carrier's managed entry. Loader rows and external plugin peers therefore resolve through the normal profile parent walk while retaining one Cordis and one instance of each bundled module. + +The published target set is Linux x64, Linux arm64, and macOS arm64. Installed-wheel black-box CI owns artifact provenance, default and patched profiles, external bundle installation, native tools, MCP, direct JSON-RPC, snapshots, and trusted real-provider turns on every target. + +## Existing decisions and supersession + +This decision implements and supersedes the Python exception and deferred-migration sections of [the single dsh application launcher](2026-08-22-single-dsh-application-launcher.md). It supersedes the private application, external complete-config, artifact-name, and customization facts in [the single-file Python SDK runtime distribution](2026-07-10-single-file-executable-sdk-runtime-distribution.md), which remains authoritative for pkg/SEA, wheel construction, native target validation, and publication. The [standalone sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.md) supersedes only this note's minimal-overlay realization. No active note is fully superseded, so none is archived. + +## Alternatives considered + +**Keep complete `cordis.yml` as an advanced escape hatch.** Rejected because it preserves a second application assembly and lets a caller bypass profile environment, plugin, and shutdown ownership. + +**Silently use `~/.dsh` for compatibility.** Rejected because an SDK process must not inherit a person's plugins, credentials, settings, or sessions without an explicit choice. + +**Copy the virtual dependency tree into every home.** Rejected because it duplicates hundreds of megabytes and loads a second Cordis instance. Export-preserving proxies are small and retain module identity. + +**Bundle pnpm and Node package management into every SDK launch.** Rejected because installed plugins are deployment state, not per-turn runtime work. Only `dsh plugin` needs the external package manager. + +## Consequences + +Python callers configure the same profile vocabulary as TypeScript and direct CLI users, and arbitrary external bundles can extend an SDK profile without introducing another launcher. Homes are selected explicitly, complete-config and `session_root` parameters are unavailable, and the executable includes shared-library assets plus profile-module proxies. The full `sdk`, standalone `sdk-minimal`, and `web` applications remain separate profiles inside the same packaged CLI. The installed-wheel CI makes those package, profile, native, and provider paths release requirements rather than source-only assumptions. diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md new file mode 100644 index 0000000000..400e70113e --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md @@ -0,0 +1,55 @@ +# Agent Note: 通过 dsh profile 启动器运行 Python SDK 运行时 + +Status: implemented + +[English](2026-08-23-python-sdk-dsh-profile-runtime.md) | 中文 + +## 问题 + +Python SDK 分发一个私有 Node 应用,直接启动完整外部 `cordis.yml`;其他所有受支持应用都从 `dsh` profile 进入。该例外重复了环境加载、配置所有权、插件解析、关闭流程、产物命名与测试路径。它还把 SDK 自定义变成全量替换应用树:只想替换一个插件的调用方也必须拥有 JSON-RPC server 和所有无关 deployment 配置项。 + +仅修改 Python wrapper 无法采用普通 profile。运行时可执行程序必须包含 `dsh` CLI、随附 profile 与 bundle 文件、原生库,以及在 profile 文件和外部插件位于 pkg 虚拟文件系统之外时仍可工作的模块解析路径。 + +## 决策 + +### 一个应用启动器 + +运行时可执行程序打包 `@deepseek-ai/dsh` 并运行其普通命令语法。Python 客户端默认选择 `--profile sdk`,转发有序绝对 `--patch` 路径,也可以选择另一个 `dsh` 可执行程序或 profile。可运行极简示例选择随附 `sdk-minimal` profile。私有 `@deepseek-ai/dsh-sdk-python-runtime` 应用包和检入的运行时 `cordis.yml` 均不存在。JSON-RPC 服务仍由 `@deepseek-ai/dsh-sdk-app` bundle 与 `@deepseek-ai/dsh-sdk-jsonrpc-server` 插件提供,而不是 Python 自有启动路径。 + +公开 Python 配置包括 `dsh_bin`、`profile`、有序 `patches`、`dsh_home`、进程 cwd/环境、provider/model/token 选择、有界初始化 timeout,以及可选的轮次/关闭 timeout。它不暴露完整 Cordis 树或任意启动 argv。`RunResult` 报告协议所有的运行值,不重复 profile 的持久化路径。 + +每次 Python 启动都要求显式 `dsh_home`,或子进程环境中的非空 `DSH_HOME`。SDK 绝不会发现 `~/.dsh`。所选 home 统一拥有 profile、外部插件、凭据、设置与会话。 + +### 插件自定义 + +持久 SDK 自定义使用与直接 CLI 相同的 profile 接口。`dsh plugin --profile ...` 管理外部依赖与 bundle 顺序,`$DSH_HOME/profiles//cordis.patch.yml` 负责持久配置项变更,home patch 对所有 profile 应用机器本地变更,Python `patches` 则提供单次启动 overlay。所选 profile 只有保留 SDK server 配置项时才有效。缺失 profile、bundle、server 配置项或非法 patch 都会直接失败,不存在完整配置回退;保持运行却不提供 JSON-RPC 服务的 profile 会在独立有界的初始化握手中失败,诊断会指明该 profile。 + +[独立 sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.zh.md)只列出一个仓库自有组合包,该组合包会插入不含 `dsh-base` 的完整显式配置树。持久 Bash 与字符串替换 editor 通过组合存在,而不是通过 server 筛选;动态运行时上下文、workspace 指令、settings、托管凭据、遥测、compaction 与其他所有 base 配置项均不存在。同一运行时仍会把完整 `sdk` 与 `web` profile 作为独立选择打包。 + +运行时 wheel 安装 `dsh` 控制台命令。普通 profile 与 SDK 运行仍不需要 Node;外部包管理要求调用方自行安装 `pnpm`。 + +### 可执行程序打包 + +零代码部署 manifest 是 `dsh-python-runtime-closure`。它把 `node_modules/@deepseek-ai/dsh/lib/bin.js` 以及 profile、bundle、preset、原生 addon 与共享库资源打包进 `deepseek-harness-sdk-runtime--`。Wheel distribution 名称、Python import 模块、JSON-RPC 消息和协议稳定的 `serverInfo.name = deepseek-harness-sdk-runtime` 保持不变。 + +普通 Node profile 在 `$DSH_HOME/profiles/node_modules` 中使用符号链接,让外部插件共享安装包。操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统,因此打包 CLI 改为写入小型真实 ESM 代理包。每个代理直接按 Node import 条件解析源包的显式 ESM exports map,公开安装中实际存在的目标,并重新导出其虚拟模块 URL。没有 ESM 运行时目标的 export 项以及仅含可执行入口或类型声明入口的包不会产生不可用的代理条目;格式错误的 exports map 会导致启动失败。完整且匹配的 generation 不会获取跨进程写入锁。缺失或过期的配置项会获取该锁、重新检查 generation,并在不暴露半成品代理的前提下修复;任一载体都可以替换另一载体留下的受管配置项。Loader 配置项和外部插件 peer 因而可以通过普通 profile 逐级向上查找解析,同时保留一个 Cordis 和每个内置模块的单一实例。 + +已发布目标集合是 Linux x64、Linux arm64 与 macOS arm64。Installed-wheel 黑盒 CI 在每个目标上负责产物来源、默认及 patched profile、外部 bundle 安装、原生工具、MCP、直接 JSON-RPC、快照,以及可信真实提供方轮次。 + +## 既有决策与取代关系 + +本决策实现并取代[单一 dsh 应用启动器](2026-08-22-single-dsh-application-launcher.zh.md)中的 Python 例外与延后迁移章节。它取代[单文件 Python SDK 运行时分发](2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md)中的私有应用、外部完整配置、产物名称与自定义事实;后者继续负责 pkg/SEA、wheel 构建、原生目标验证与发布。[独立 sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.zh.md)只取代本 Note 中的极简 overlay 实现。没有任何 active note 被完全取代,因此无需归档。 + +## 考虑过的替代方案 + +**把完整 `cordis.yml` 保留为高级逃生口。** 不予采用,因为它会保留第二套应用组装,并允许调用方绕过 profile 的环境、插件与关闭所有权。 + +**为兼容性静默使用 `~/.dsh`。** 不予采用,因为 SDK 进程不应在没有显式选择时继承个人插件、凭据、设置或会话。 + +**把虚拟依赖树复制到每个 home。** 不予采用,因为这会重复数百 MB,并加载第二个 Cordis 实例。保留 exports 的代理很小,而且维持模块身份。 + +**把 pnpm 与 Node 包管理纳入每次 SDK 启动。** 不予采用,因为已安装插件属于 deployment 状态,而不是逐轮运行时工作。只有 `dsh plugin` 需要外部包管理器。 + +## 结果 + +Python 调用方使用与 TypeScript 和直接 CLI 用户相同的 profile 词汇,任意外部 bundle 可以扩展 SDK profile,而无需引入另一个 launcher。调用方必须显式选择 home,完整配置与 `session_root` 参数不可用,可执行程序则包含共享库资源和 profile 模块代理。完整 `sdk`、独立 `sdk-minimal` 与 `web` 应用作为同一打包 CLI 内的不同 profile 保持分离。Installed-wheel CI 将包、profile、原生与提供方路径变成发布要求,而不是仅在源码中成立的假设。 diff --git a/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.i18n.yaml new file mode 100644 index 0000000000..4a06faa161 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.md +2026-08-24-standalone-sdk-minimal-profile.md: bc1a177dc4a232201004e6869caa452a7d55deb9 +2026-08-24-standalone-sdk-minimal-profile.zh.md: ae6fdb95079f748f26758a30c968c27548e9a87f diff --git a/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.md b/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.md new file mode 100644 index 0000000000..bc1a177dc4 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.md @@ -0,0 +1,57 @@ +# Agent Note: Standalone sdk-minimal profile under dsh + +Status: implemented + +English | [中文](2026-08-24-standalone-sdk-minimal-profile.zh.md) + +## Problem + +A minimal SDK agent needs an explicit plugin roster. Expressing it as an overlay on the full `sdk` profile leaves every `dsh-base` service mounted and makes exclusion depend on filters and disable entries spread across unrelated plugins. A later base row can change runtime behavior even when the model-facing tools remain filtered. + +A complete caller-supplied Cordis tree gives an exact roster but bypasses profile initialization, bundle resolution, persistent plugin management, home and invocation patch layers, and the `dsh`-owned process lifecycle. The minimal mode needs composition-level exclusion without creating another launcher or Python-owned application. + +## Decision + +### Launch and ownership + +`dsh --profile sdk-minimal` is a shipped startup-only profile. Its manifest lists only `@deepseek-ai/dsh-sdk-minimal`; it does not list `@deepseek-ai/dsh-base`. The bundle inserts the complete Cordis tree over the launcher's empty profile root, while the profile patch, home patch, and ordered invocation patches retain their ordinary precedence above it. + +The `dsh` CLI remains the only application launcher. The Python example selects `sdk-minimal` through the public `profile` field and an explicit Harness home. Python exposes no complete-config or arbitrary-argv path. The full Python and TypeScript SDK defaults remain `sdk`. + +The bundle reuses `@deepseek-ai/dsh-sdk-app` for command help, stdin EOF, and bounded shutdown. The startup provider accepts a profile-name config so both SDK profiles render their actual command without duplicating process lifecycle code. + +### Explicit composition + +The bundle owns one DeepSeek adapter, SDK JSON-RPC serving, the executor-less agent spine, local subprocess and unrestricted filesystem providers, persistent Bash, the string-replace editor, and uncompressed JSONL sessions under `$DSH_HOME/sessions`. The SDK initialization request owns the model id; `DSH_CONTEXT_WINDOW` supplies fallback capacity for models outside the adapter's advisory catalog. The persona comes from `DSH_SYSTEM_PROMPT`, and the credential from `DEEPSEEK_API_KEY`. + +Harness identity, runtime context, workspace instructions, skills, model-facing job controls, compaction, settings, managed credentials, telemetry, Web tools, subagents, and every other base row are absent rather than hidden. The profile pins `danger-full-access`, `maxTokensAsSuccess: false`, and startup-only patch loading. This layer is POSIX-only because its persistent terminal uses Bash. + +### Customization and Web + +`dsh plugin --profile sdk-minimal add ` installs persistent dependencies and bundle layers. The profile's `cordis.patch.yml`, the home patch, and Python `patches` provide persistent, machine-local, and invocation-specific row changes. Customization can expand or replace the explicit tree, but it still passes through the same launcher and profile resolution. + +The Python runtime continues to package `dsh-web-app` and the frontend assets. `dsh web` starts that separate browser application from the installed wheel; a Python SDK client cannot select `web` because it contains no JSON-RPC server row. + +## Existing decisions and supersession + +This decision partially supersedes the base-first and standalone-tree rejection in [one dsh launcher for application profiles](2026-08-22-single-dsh-application-launcher.md). Repository-owned, versioned standalone profile bundles are allowed when an explicit roster is the product behavior; caller-supplied complete trees and alternate executables remain rejected. + +It also supersedes the minimal-overlay realization in [Python SDK runtime through the dsh profile launcher](2026-08-23-python-sdk-dsh-profile-runtime.md) and the base-first default-profile statement in [profile plugin bundles](2026-08-05-profile-plugin-bundles.md). Those notes retain independent authority for launcher ownership, Python packaging and home requirements, general profile layering, and plugin management. No active note is fully superseded or eligible for archival. + +## Verification + +The bundle test pins the exact row and dependency roster. Profile-template and config-dump tests pin the one-bundle manifest, startup-only lifecycle, absence of `dsh-base`, and absence of module HMR. The keyless Python example test boots the real `dsh --profile sdk-minimal` process and asserts the generated manifest, complete system prompt, and two advertised tools. The installed-wheel minimal scenario exercises persistent shell state, editor effects, JSONL persistence, and the committed model-visible snapshot through the packaged executable. + +## Alternatives considered + +**Keep the minimal mode as an overlay on `sdk`.** Rejected because filtering model-visible tools does not remove base services, prompt contributors, persistence choices, or later runtime behavior. It also makes the minimal application depend on controls in shared SDK server and system-prompt interfaces. + +**Restore a Python `cordis` argument or environment-selected complete config.** Rejected because it recreates a Python-owned application composition and bypasses profile plugin management and launcher lifecycle. + +**Create a second minimal SDK startup plugin.** Rejected because profile-aware help is the only variation; the SDK startup provider can own that config while keeping EOF and shutdown behavior local. + +**Remove Web packages from the Python runtime closure.** Rejected because the wheel distributes the ordinary `dsh` application and Python deployments may also need `dsh web`; profile selection, not packaging divergence, separates those applications. + +## Consequences + +The minimal model and runtime roster changes only when its owning bundle changes or a trusted higher patch expands it. The price is deliberate duplication of a small complete application tree and omission of shared settings, credentials, policy controls, telemetry, and Web capabilities from that profile. Users choose the full `sdk` profile when they need those services, while both choices keep one launcher, one profile vocabulary, and one packaged runtime. diff --git a/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md b/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md new file mode 100644 index 0000000000..ae6fdb9507 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md @@ -0,0 +1,57 @@ +# Agent Note: dsh 下的独立 sdk-minimal profile + +Status: implemented + +[English](2026-08-24-standalone-sdk-minimal-profile.md) | 中文 + +## 问题 + +极简 SDK agent 需要显式插件清单。若把它表达为完整 `sdk` profile 上的 overlay,所有 `dsh-base` 服务仍保持挂载,排除逻辑则依赖分散在无关插件中的筛选器与 disable 配置项。以后新增的 base 配置项即使没有进入面向模型的工具清单,也可能改变运行时行为。 + +由调用方提供完整 Cordis 配置树可以得到确切清单,但会绕过 profile 初始化、组合包解析、持久插件管理、home 与调用 patch 层,以及由 `dsh` 拥有的进程生命周期。极简模式需要在组合层排除功能,同时不能创建另一个 launcher 或 Python 自有应用。 + +## 决策 + +### 启动与所有权 + +`dsh --profile sdk-minimal` 是随附的仅启动时 profile。其 manifest 只列出 `@deepseek-ai/dsh-sdk-minimal`,不列出 `@deepseek-ai/dsh-base`。该组合包在 launcher 的空 profile 根之上插入完整 Cordis 配置树,而 profile patch、home patch 与有序调用 patch 仍在其上保持普通优先级。 + +`dsh` CLI 仍是唯一应用 launcher。Python 示例通过公开 `profile` 字段与显式 Harness home 选择 `sdk-minimal`。Python 不暴露完整配置或任意 argv 路径。完整 Python 与 TypeScript SDK 的默认值仍是 `sdk`。 + +该组合包复用 `@deepseek-ai/dsh-sdk-app` 提供命令 help、stdin EOF 与有界关闭。启动提供方接受 profile 名称配置,因此两个 SDK profile 都能呈现自己的实际命令,且无需复制进程生命周期代码。 + +### 显式组合 + +该组合包拥有一个 DeepSeek 适配器、SDK JSON-RPC 服务、无执行器的 agent 主干、本地子进程与不受限文件系统提供方、持久 Bash、字符串替换 editor,以及位于 `$DSH_HOME/sessions` 的未压缩 JSONL 会话。SDK 初始化请求拥有模型 id;`DSH_CONTEXT_WINDOW` 为不在适配器建议目录中的模型提供后备容量。Persona 来自 `DSH_SYSTEM_PROMPT`,凭据来自 `DEEPSEEK_API_KEY`。 + +Harness 身份、运行时上下文、workspace 指令、skills、面向模型的 job 控制、compaction、settings、托管凭据、遥测、Web 工具、subagent 与其他所有 base 配置项均不存在,而不是被隐藏。该 profile 固定使用 `danger-full-access`、`maxTokensAsSuccess: false` 与仅启动时 patch 加载。由于持久终端使用 Bash,此层只支持 POSIX。 + +### 自定义与 Web + +`dsh plugin --profile sdk-minimal add ` 安装持久依赖与组合包层。Profile 自身的 `cordis.patch.yml`、home patch 与 Python `patches` 分别提供持久、机器本地和逐次调用的配置项变更。自定义可以扩展或替换显式配置树,但仍经过同一个 launcher 与 profile 解析。 + +Python 运行时继续打包 `dsh-web-app` 与前端产物。`dsh web` 会从已安装 wheel 启动这个独立浏览器应用;Python SDK client 不能选择 `web`,因为其中没有 JSON-RPC server 配置项。 + +## 既有决策与取代关系 + +本决策部分取代[应用 profile 使用同一个 dsh launcher](2026-08-22-single-dsh-application-launcher.zh.md)中的 base 优先规则与独立配置树否决。显式清单属于产品行为时,可以使用仓库自有且有版本的独立 profile 组合包;由调用方提供的完整配置树与替代可执行程序仍被否决。 + +本决策也取代 [Python SDK 运行时通过 dsh profile launcher 启动](2026-08-23-python-sdk-dsh-profile-runtime.zh.md)中的极简 overlay 实现,以及 [profile 插件组合包](2026-08-05-profile-plugin-bundles.zh.md)中默认 profile 均以 base 开头的表述。这些 Agent Note 对 launcher 所有权、Python 打包与 home 要求、普通 profile 分层及插件管理仍保持独立权威。没有活跃 Agent Note 被完全取代或符合归档条件。 + +## 验证 + +组合包测试固定确切配置项与依赖清单。Profile 模板与配置 dump 测试固定单组合包 manifest、仅启动时生命周期、`dsh-base` 缺席与模块 HMR 缺席。Keyless Python 示例测试启动真实 `dsh --profile sdk-minimal` 进程,并断言生成的 manifest、完整系统提示词与两个对外公布的工具。Installed-wheel 极简场景通过打包可执行程序验证持久 shell 状态、editor 文件效果、JSONL 持久化与已提交的模型可见快照。 + +## 考虑过的替代方案 + +**继续把极简模式作为 `sdk` 上的 overlay。** 否决:筛选面向模型的工具不会移除 base 服务、提示词贡献方、持久化选择或后续运行时行为,还会让极简应用依赖共享 SDK server 与系统提示词接口中的控制项。 + +**恢复 Python `cordis` 参数或由环境选择的完整配置。** 否决:这会重新创建 Python 自有应用组合,并绕过 profile 插件管理与 launcher 生命周期。 + +**创建第二个极简 SDK 启动插件。** 否决:唯一变化是 profile 感知的 help;SDK 启动提供方可以拥有该配置,同时把 EOF 与关闭行为保持在一处。 + +**从 Python 运行时闭包移除 Web 包。** 否决:wheel 分发普通 `dsh` 应用,而且 Python 部署也可能需要 `dsh web`;这些应用由 profile 选择隔离,而不是由打包差异隔离。 + +## 后果 + +极简模型与运行时清单只有在所属组合包变化,或受信任的上层 patch 扩展它时才会变化。代价是刻意重复一棵较小的完整应用树,并在该 profile 中省略共享 settings、凭据、策略控制、遥测与 Web 功能。需要这些服务的用户选择完整 `sdk` profile;两种选择仍共用一个 launcher、一套 profile 词汇与一个打包运行时。 diff --git a/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.i18n.yaml index bad485523f..49ebbb6567 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md -2026-08-10-minimal-preset-owns-rl-composition.md: 47a1bbe9f8f4875e2437b3ff955663c4b3de7dce -2026-08-10-minimal-preset-owns-rl-composition.zh.md: 0fab1e1f23a314bec80b5fd355abcb2881c1b1a5 +2026-08-10-minimal-preset-owns-rl-composition.md: 2e9a3e56252f8e91008a5559ad738a7ca678446b +2026-08-10-minimal-preset-owns-rl-composition.zh.md: 31df6ebfbc15f35208bc73b391725b2039fe7819 diff --git a/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md index 47a1bbe9f8..2e9a3e5625 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md +++ b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md @@ -22,7 +22,7 @@ The process-wide `core-web.cordis.yml` patch is absent. Browser UI, workspace at System-prompt and persona package tests prove final complete-section and runtime-context suppression, including waterfall mutation and duplicate rejection. The shipped-preset composition test asserts the exact prompt, Bash description, absolute editor schema, and two-tool catalog under the default native presentation. The keyless Web replay sends a real request through a `minimal` agent while global identity, Web-orientation text, dynamic policy contexts, and a test section are registered, asserts that no runtime-context snapshot exists, the entry-local filesystem is bare, and compaction is absent, then executes two persistent Bash calls to prove environment and cwd state survive and executes the editor through an absolute path. -The standalone [`minimal.cordis.yml`](../../../../examples/python-sdk-agent/minimal.cordis.yml) is the complete two-tool composition for the bundled JSON-RPC runtime. The [bare two-tool runtime decision](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md) owns its launch-specific environment configuration, bare filesystem, and absence of compaction. Its keyless SDK replay asserts the assembled system prompt and two-tool catalog, executes persistent Bash across calls, and exercises the editor; the Python SDK tutorial provides the runnable entry point. +The standalone [`sdk-minimal` bundle](../../../../packages/bundle/sdk-minimal/README.md) is the complete two-tool composition for `dsh --profile sdk-minimal`. The [bare two-tool runtime decision](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md) owns its launch-specific environment configuration, bare filesystem, and absence of compaction; the [standalone-profile decision](../architecture/2026-08-24-standalone-sdk-minimal-profile.md) owns its launcher and bundle placement. Its keyless SDK process test asserts the assembled system prompt and two-tool catalog, and the installed-wheel scenario executes persistent Bash across calls and exercises the editor; the Python SDK tutorial provides the runnable entry point. ## Alternatives considered @@ -36,4 +36,4 @@ The standalone [`minimal.cordis.yml`](../../../../examples/python-sdk-agent/mini ## Consequences -The Web RL prompt is fixed rather than environment-overridable; the standalone JSON-RPC prompt is deployment-selected. The Web preset and standalone JSON-RPC example state the same two-tool contract for their respective launch paths. The model sees only persistent `bash` and `str_replace_editor`; shell state is per agent and disappears with that agent. The Web preset pays for its own PTY and bare filesystem service instances, while other presets pay nothing for them. The local persistent-shell backend requires the supported POSIX terminal substrate, so this preset does not support Windows agents. +The Web RL prompt is fixed rather than environment-overridable; the standalone JSON-RPC prompt is deployment-selected. The Web preset and `sdk-minimal` profile state the same two-tool behavior for their respective launch paths. The model sees only persistent `bash` and `str_replace_editor`; shell state is per agent and disappears with that agent. The Web preset pays for its own PTY and bare filesystem service instances, while other presets pay nothing for them. The local persistent-shell backend requires the supported POSIX terminal substrate, so this preset does not support Windows agents. diff --git a/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md index 0fab1e1f23..31df6ebfbc 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md @@ -16,13 +16,13 @@ Status: implemented preset persona 恰好是 `You are a helpful software engineer assistant.`,它设置 `complete: true`,并为其 agent 作用域抑制 runtime context。complete `PromptSection` 参与常规组装,因此工具、变量和协作式监听器仍会解析;`system-prompt/assemble` waterfall(瀑布式事件)结束后,提示词注册表会将该段落的独立副本恢复为唯一的系统提示词段落,并丢弃每个动态上下文贡献。存在多个有效 complete 段时,组装会被拒绝。这些最终注册表约束可防止 harness 身份、Web 定位、工具引导、组装监听器、沙箱策略、批准策略、委派或其他动态上下文提供方添加模型输入。 -进程级 `core-web.cordis.yml` patch 不再存在。浏览器 UI、workspace 附加、持久化、子进程、沙箱、权限、模型路由及其他跨会话服务仍由宿主持有。选择 `minimal` 会改变一个 agent 面向模型的组合,并且仅为该 agent 遮蔽宿主文件系统提供方,不会改变 Web 进程中的其他会话。 +进程级 `core-web.cordis.yml` patch 缺席。浏览器 UI、workspace 附加、持久化、子进程、沙箱、权限、模型路由及其他跨会话服务仍由宿主持有。选择 `minimal` 会改变一个 agent 面向模型的组合,并且仅为该 agent 遮蔽宿主文件系统提供方,不会改变 Web 进程中的其他会话。 ## 验证 系统提示词与 persona 包测试证明了 complete 段最终约束与 runtime-context 抑制,包括 waterfall 修改与重复项拒绝。交付 preset 组合测试在默认原生呈现下断言精确的提示词、Bash 描述、要求绝对路径的编辑器 schema 和双工具目录。无密钥 Web 回放通过 `minimal` agent 发送一个真实请求,同时注册全局身份、Web 定位文本、动态策略上下文和一个测试段落;它断言不存在 runtime-context 快照、entry 本地文件系统是裸后端且压缩不存在,随后执行两次持久 Bash 调用,证明环境与 cwd 状态能够保留,并通过绝对路径执行编辑器。 -独立的 [`minimal.cordis.yml`](../../../../examples/python-sdk-agent/minimal.cordis.yml) 是内置 JSON-RPC 运行时的完整双工具组合。[裸双工具运行时决策](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md)说明其启动方式专属的环境配置、裸文件系统和无压缩选择。其无密钥 SDK 回放会断言组装后的系统提示词与双工具目录,跨调用执行持久 Bash,并使用编辑器;Python SDK 教程提供可运行的入口。 +独立的 [`sdk-minimal` 组合包](../../../../packages/bundle/sdk-minimal/README.zh.md)是 `dsh --profile sdk-minimal` 的完整双工具组合。[裸双工具运行时决策](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md)说明其启动方式专属的环境配置、裸文件系统和无 compaction 选择;[独立 profile 决策](../architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md)负责其 launcher 与组合包位置。其无密钥 SDK 进程测试会断言组装后的系统提示词与双工具目录,installed-wheel 场景会跨调用执行持久 Bash 并使用编辑器;Python SDK 教程提供可运行入口。 ## 考虑过的替代方案 @@ -36,4 +36,4 @@ preset persona 恰好是 `You are a helpful software engineer assistant.`,它 ## 后果 -Web RL 提示词固定不变,不能通过环境覆盖;独立 JSON-RPC 提示词由部署选择。Web preset 与独立 JSON-RPC 示例分别在各自的启动路径声明相同的双工具约定。模型只看到持久 `bash` 与 `str_replace_editor`;shell 状态按 agent 隔离,并随该 agent 一并消失。Web preset 为自身的 PTY 与裸文件系统服务实例承担开销,其他 preset 无需承担。持久 shell 的本地后端需要受支持的 POSIX 终端基础环境,因此该 preset 不支持 Windows agent。 +Web RL 提示词固定不变,不能通过环境覆盖;独立 JSON-RPC 提示词由部署选择。Web preset 与 `sdk-minimal` profile 分别为各自启动路径声明相同的双工具行为。模型只看到持久 `bash` 与 `str_replace_editor`;shell 状态按 agent 隔离,并随该 agent 一并消失。Web preset 为自身的 PTY 与裸文件系统服务实例承担开销,其他 preset 无需承担。持久 shell 的本地后端需要受支持的 POSIX 终端基础环境,因此该 preset 不支持 Windows agent。 diff --git a/.agents/notes/implemented/bug-fix/2026-08-20-running-draft-primary-send.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-20-running-draft-primary-send.i18n.yaml new file mode 100644 index 0000000000..5a1d90d7f1 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-20-running-draft-primary-send.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-20-running-draft-primary-send.md +2026-08-20-running-draft-primary-send.md: 79c8c3a74fc49a325b229324b0d96b2387308d3d +2026-08-20-running-draft-primary-send.zh.md: 180352810f57ecc04882322318f9708cf247a09c diff --git a/.agents/notes/implemented/bug-fix/2026-08-20-running-draft-primary-send.md b/.agents/notes/implemented/bug-fix/2026-08-20-running-draft-primary-send.md new file mode 100644 index 0000000000..79c8c3a74f --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-20-running-draft-primary-send.md @@ -0,0 +1,31 @@ +# Agent Note: Running drafts take the primary Send action + +Status: implemented + +English | [中文](2026-08-20-running-draft-primary-send.zh.md) + +## Problem + +The ordinary Web composer remains editable while a Turn is running, and keyboard submission can queue or steer its draft. Its single primary pointer control nevertheless stayed on Stop for the entire Turn. A pointer user who entered a follow-up and activated that control stopped the current Turn instead of submitting the visible draft, so the control contradicted the composer's editable state and the user's current content. + +## Decision + +`InputBar` chooses the ordinary session's primary action from the running state, draft content, and owner block. An empty running composer shows Stop and routes it through the existing session cancellation callback. Non-whitespace text or at least one attachment changes that same control to Send; the click uses the existing Queue submission path. An owner-blocked running composer keeps Stop even when a retained draft exists, because the block disables both editing and submission. Clearing the draft or completing a successful submission restores Stop while the Turn remains active. Idle sessions continue to show Send, disabled while the draft is empty or submission is unavailable. + +The pointer action does not inherit the `ui-conversation.busyEnter` preference. That preference continues to choose Queue or Steer only for the two keyboard gestures. Continuable subagents retain independent Send and Stop controls, and one-shot subagents retain their read-only behavior. + +## Verification + +The `InputBar` component test covers empty, text, cleared, submitted, attachment-only, and owner-blocked running drafts, including Queue submission while the keyboard preference selects Steer. The keyless assembled Web scenario parks a real composed Turn in the replay adapter, captures the running draft with Send, clicks it through the Host Queue path, observes Stop return after the draft clears, removes the queued row, and then cancels the Turn. + +## Alternatives considered + +**Keep Stop for the whole running Turn.** This preserves immediate cancellation but leaves the visible editable draft without a pointer submission action and makes the primary control act against the content beside it. + +**Render Send and Stop simultaneously for every running session.** Continuable subagents need two independent operations because their cancellation route differs from continuation delivery. Ordinary sessions have one established primary seat; adding a permanent second control would spend more space and create a different hierarchy when the draft itself already identifies the immediate action. + +**Apply the busy-Enter preference to pointer Send.** A button labeled Send would silently change between Queue and Steer according to a keyboard preference. Keeping pointer submission on Queue preserves the existing explicit distinction and avoids an invisible mode on the button. + +## Consequences + +Pointer users can submit a follow-up without waiting for the active Turn or using a keyboard shortcut. An actionable draft occupies the single primary seat, so Stop returns after the draft is cleared or accepted rather than remaining simultaneously visible; an owner block returns that seat to Stop because the retained draft cannot be edited or submitted. Keyboard delivery selection, cancellation transport, and subagent controls are unchanged. Issue #2850 records the user-facing defect and acceptance boundary. diff --git a/.agents/notes/implemented/bug-fix/2026-08-20-running-draft-primary-send.zh.md b/.agents/notes/implemented/bug-fix/2026-08-20-running-draft-primary-send.zh.md new file mode 100644 index 0000000000..180352810f --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-20-running-draft-primary-send.zh.md @@ -0,0 +1,31 @@ +# Agent Note: 运行中草稿取得主 Send 操作 + +Status: implemented + +[English](2026-08-20-running-draft-primary-send.md) | 中文 + +## 问题 + +普通 Web composer 在 Turn 运行期间仍可编辑,键盘提交也能把草稿送入 Queue 或 Steer。然而,其唯一的主指针控件会在整个 Turn 中一直保持 Stop。指针用户输入后续消息并激活该控件时,会停止当前 Turn,而不是提交眼前的草稿;该控件因而与 composer 的可编辑状态和用户当前内容相冲突。 + +## 决策 + +`InputBar` 根据运行状态、草稿内容和 owner block 选择普通会话的主操作。运行中的 composer 为空时显示 Stop,并通过既有会话取消回调执行。存在非空白文字或至少一个附件时,同一控件切换为 Send,点击后使用既有 Queue 提交路径。owner block 会禁用编辑与提交,因此即使保留了草稿,运行中的 composer 也保持 Stop。清空草稿或成功提交后,只要 Turn 仍在运行,就会恢复 Stop。空闲会话仍显示 Send;草稿为空或无法提交时,该按钮保持禁用。 + +指针操作不继承 `ui-conversation.busyEnter` 偏好。该偏好仍然只为两个键盘手势选择 Queue 或 Steer。可继续 subagent 保留相互独立的 Send 与 Stop 控件,one-shot subagent 保持只读行为。 + +## 验证 + +`InputBar` 组件测试覆盖运行中草稿的空白、文字、清空、提交成功、仅附件和 owner-blocked 状态,并证明键盘偏好选择 Steer 时,按钮提交仍使用 Queue。无密钥的组装 Web 场景通过 replay 适配器停住真实组合出的 Turn,捕获显示 Send 的运行中草稿,经 Host Queue 路径点击提交,在草稿清空后观察 Stop 恢复,移除 Queue 行,再取消该 Turn。 + +## 备选方案 + +**在整个运行中 Turn 保持 Stop。** 这样可以始终立即取消,但可见的可编辑草稿没有指针提交操作,主控件也会执行与相邻内容相反的动作。 + +**为每个运行中会话同时渲染 Send 与 Stop。** 可继续 subagent 需要两个独立操作,因为其取消路由与继续投递不同。普通会话已有单一主操作位置;永久增加第二个控件会占用更多空间,并在草稿本身已经指明当前操作时引入另一套层级。 + +**让指针 Send 采用 busy-Enter 偏好。** 标记为 Send 的按钮会随键盘偏好在 Queue 与 Steer 之间静默变化。保持指针提交始终使用 Queue,可以保留既有显式区分,避免按钮携带不可见模式。 + +## 影响 + +指针用户无需等待当前 Turn 结束或使用键盘快捷键,即可提交后续消息。可操作草稿会占用唯一的主操作位置,因此 Stop 会在草稿清空或被接纳后恢复,而不是同时显示;owner block 会让该位置恢复 Stop,因为保留的草稿无法编辑或提交。键盘投递选择、取消传输和 subagent 控件均不变。Issue #2850 记录用户可见缺陷与验收边界。 diff --git a/.agents/notes/implemented/bug-fix/2026-08-24-alpha-routed-image-quality-ladders.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-24-alpha-routed-image-quality-ladders.i18n.yaml new file mode 100644 index 0000000000..e2e60901af --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-24-alpha-routed-image-quality-ladders.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-24-alpha-routed-image-quality-ladders.md +2026-08-24-alpha-routed-image-quality-ladders.md: cdf5e7bd908782781e66a4f6494428f0f174e258 +2026-08-24-alpha-routed-image-quality-ladders.zh.md: fbe8215b0ca1c606cd66d60fe1109d2f9aa1cbad diff --git a/.agents/notes/implemented/bug-fix/2026-08-24-alpha-routed-image-quality-ladders.md b/.agents/notes/implemented/bug-fix/2026-08-24-alpha-routed-image-quality-ladders.md new file mode 100644 index 0000000000..cdf5e7bd90 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-24-alpha-routed-image-quality-ladders.md @@ -0,0 +1,33 @@ +# Agent Note: Alpha-routed image quality ladders replace colour-count codec routing + +Status: implemented + +English | [中文](2026-08-24-alpha-routed-image-quality-ladders.zh.md) + +## Problem + +Image normalization and request-image encoding in `@deepseek-ai/dsh-attachment-local` chose their codec by a 5-bit colour-count sample: images whose 128×128 nearest-neighbour sample stayed within 256 quantized colours went to palette PNG (libimagequant) before WebP, other alpha images to WebP, and other opaque images to JPEG. High-frequency photographic JPEGs routinely quantize below the threshold — issue #2885's 8000×8000 reproduction images measure 175 and 184 sampled colours against 2145 and 4077 real colours — and palette PNG is the slowest encoder in the pipeline while producing files about four times larger than JPEG on such content (measured 2657ms/3.95MiB versus 26ms/0.95MiB at the 2048px master size). The sample itself forces a full decode (`fastShrinkOnLoad: false`), costing 86 to 192ms on 64MP sources for every image. When every candidate exceeded the byte cap, both encoders also entered a proportional-downscale retry loop ending in an `IMAGE_TOO_LARGE` error, although measured worst-case inputs (uniform noise) fit the default budgets at the first quality. + +## Decision + +Both encoders route by one decoded fact only: sources with an alpha channel encode as lossy WebP at effort 0, opaque sources as JPEG (libjpeg-turbo), each down a shared quality ladder of 85, 75, 60 (`IMAGE_ENCODING_QUALITIES` / `WEBP_ENCODING_EFFORT` / `encodingLadder` in `encoding.ts`). The colour-count classifier and the palette PNG branch are deleted, not repaired, so the misclassification bug class cannot recur and no image pays the classification decode. `normalizedImageMaxBytes` and the route `maxBytes` become ladder targets rather than caps: the ladder still stops at the first quality that fits, but when every quality exceeds the target the smallest output is kept and the downscale retry loop is gone. Provider byte limits (DeepSeek 32MiB per image, inline budgets) remain enforced where the bytes are transmitted. Master dimensions move from a long-edge rule to a total-pixel budget: `normalizedImageMaxPixels` (default 2048x2048) scales the raster proportionally and `normalizedImageMaxDimension` (default 8192, matching the admission per-side cap) clamps the long edge afterwards, so extreme aspect ratios such as tall page screenshots keep their short-edge resolution (a 2000x20000 source keeps about 647px of width instead of 204px) while square sources normalize exactly as before. The request transform version moves to `request-image-v5`, so existing cached variants regenerate by identity; content-addressed masters stay valid without migration. The request cache read no longer rejects entries above the byte target, since a ladder-exhausted output is the deterministic result for its variant id. + +Pareto measurements over the issue #2885 reproduction set (PR #2989 appendices) back the choice: on photographic content JPEG is one to two orders of magnitude faster than every alternative, and WebP at effort 0 matches palette PNG's size on graphics content while never being misrouted; uniform-noise worst cases fit the default 4MiB/1MiB targets at quality 85 for opaque sources, and only an adversarial random-alpha plane exhausts the WebP ladder (about 6.3MiB, five times under the provider cap). + +This decision partially supersedes the [unified image request pipeline note](../feature/2026-08-20-unified-image-request-pipeline.md), whose normalization and request-encoding sections now describe this routing; its durable-version split, Files lifecycle, and offload projection stand unchanged. + +## Alternatives considered + +**Repair the classifier (higher-resolution sampling, gradient statistics) and keep palette PNG.** Rejected: any content classifier retains a misrouting class and the per-image classification decode; palette PNG's only frontier niche (graphics) is matched by WebP at a fraction of the encode time. + +**A single WebP ladder for everything.** Rejected: JPEG is four to six times faster on opaque photographic content, the dominant real workload, and the alpha probe is a metadata read costing nothing. + +**Keep the downscale retry loop for ladder-exhausted outputs.** Rejected: measured worst cases show the loop is dead code within default budgets, and its only reachable effect was degrading adversarial inputs to 1×1 before erroring. + +## Consequences + +- Opaque low-colour graphics (charts, text screenshots) now store as JPEG: two to three times larger than palette PNG in the hundreds-of-kilobytes range, with JPEG ringing on hard edges; the model-visible request version was already dominated by pixel-budget downscaling, so legibility impact is marginal. Reintroducing a graphics codec would add a WebP step to the opaque ladder, not restore classification. +- GIF sources decode with an alpha plane under gifload, so still-frame GIFs normalize onto the WebP ladder. +- `IMAGE_TOO_LARGE` no longer arises from encoding; it remains the admission error for oversized sources. +- A ladder-exhausted attachment can exceed its byte target on disk and on the wire until a provider cap rejects it; measured reachable only with adversarial random-alpha input. Re-submitting such an over-target master as a new upload fails the pass-through byte check and re-encodes it down the lossy ladder again, so normalization is not idempotent for this adversarial-only class and each round adds generation loss. +- Test evidence: `packages/attachment/attachment-local/tests` pins the routing, ladder-exhaustion, and readable-text behavior against real encoders, including the issue #2885 misrouting characteristics (high-frequency photographic content leaving the slow path). diff --git a/.agents/notes/implemented/bug-fix/2026-08-24-alpha-routed-image-quality-ladders.zh.md b/.agents/notes/implemented/bug-fix/2026-08-24-alpha-routed-image-quality-ladders.zh.md new file mode 100644 index 0000000000..fbe8215b0c --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-24-alpha-routed-image-quality-ladders.zh.md @@ -0,0 +1,33 @@ +# Agent Note: 按 alpha 路由的图片质量阶梯取代按色数分类的编码路由 + +Status: implemented + +[English](2026-08-24-alpha-routed-image-quality-ladders.md) | 中文 + +## 问题 + +`@deepseek-ai/dsh-attachment-local` 的图片规范化和请求版本编码此前按 5-bit 色数采样选择编码器:128×128 最近邻采样量化后不超过 256 色的图片先走 palette PNG(libimagequant)再退 WebP,其他透明图片走 WebP,其他不透明图片走 JPEG。高频摄影类 JPEG 的量化采样经常落在阈值以下,issue #2885 的 8000×8000 复现图片采样色数为 175 和 184,实际色数为 2145 和 4077,而 palette PNG 是管线里最慢的编码器,在这类内容上产物还比 JPEG 大约 4 倍(2048px master 尺寸实测 2657ms/3.95MiB 对 26ms/0.95MiB)。采样本身因 `fastShrinkOnLoad: false` 必须全尺寸解码,64MP 源图上每张都要付出 86 至 192ms。当所有候选都超过字节上限时,两个编码器还会进入按比例缩图重试的循环,最终以 `IMAGE_TOO_LARGE` 报错,而实测最坏输入(均匀噪声)在默认预算下第一档质量就能装下。 + +## 决定 + +两个编码器只按一个解码事实路由:带 alpha 通道的源图编码为 effort 0 的有损 WebP,不透明源图编码为 JPEG(libjpeg-turbo),共用质量阶梯 85、75、60(`encoding.ts` 的 `IMAGE_ENCODING_QUALITIES` / `WEBP_ENCODING_EFFORT` / `encodingLadder`)。色数分类器和 palette PNG 分支被删除而不是修复,误判这一 bug 类别因此不可能复发,也不再有图片付出分类解码成本。`normalizedImageMaxBytes` 和路由 `maxBytes` 的语义从上限改为阶梯目标:阶梯仍在第一个装得下的质量档停下,但全部档位都超过目标时保留最小产物,缩图重试循环被删除。提供方字节硬限制(DeepSeek 单图 32MiB、inline 预算)仍在传输字节的位置执行。master 尺寸规则从长边上限改为总像素预算:`normalizedImageMaxPixels`(默认 2048×2048)按比例缩放,`normalizedImageMaxDimension`(默认 8192,与准入单边上限一致)随后夹住长边,因此长页面截图这类极端长宽比保留短边分辨率(2000×20000 的源图短边保留约 647px 而不是 204px),正方形源图的规范化结果与之前完全一致。请求变换版本升到 `request-image-v5`,已有变体缓存按身份自然重建;内容寻址的 master 无需迁移,继续有效。请求缓存读取不再拒绝超过字节目标的条目,因为阶梯耗尽的产物就是该 variant id 的确定性结果。 + +对 issue #2885 复现集的 Pareto 实测(PR #2989 附录)支撑这个选择:摄影类内容上 JPEG 比其余所有编码器快 1 至 2 个数量级,effort 0 的 WebP 在图形类内容上体积与 palette PNG 相当且不会被误判;均匀噪声最坏输入在不透明链的 q85 一档即落入默认 4MiB/1MiB 目标,只有对抗性的随机 alpha 平面会耗尽 WebP 阶梯(约 6.3MiB,距提供方上限还有 5 倍)。 + +本决定部分取代[统一图片请求管线记录](../feature/2026-08-20-unified-image-request-pipeline.zh.md):其规范化与请求编码章节现在以本路由为准;其耐久版本拆分、Files 生命周期与卸载投影不变。 + +## 考虑过的替代方案 + +**修复分类器(提高采样分辨率、加入梯度统计)并保留 palette PNG。** 否决:任何内容分类器都保留一类误判和每张图的分类解码成本;palette PNG 唯一的前沿生态位(图形类)WebP 用远少的编码时间即可达到。 + +**全部走单一 WebP 阶梯。** 否决:JPEG 在不透明摄影内容(真实负载的大头)上快 4 至 6 倍,而 alpha 探测只是零成本的元数据读取。 + +**为阶梯耗尽的产物保留缩图重试循环。** 否决:实测最坏情况表明该循环在默认预算内是死代码,其唯一可达效果是把对抗性输入一路缩到 1×1 再报错。 + +## 后果 + +- 不透明的低色数图形(图表、文字截图)现在存为 JPEG:在几百 KB 量级上比 palette PNG 大 2 至 3 倍,锐利边缘有 JPEG 振铃;模型可见的请求版本本就被像素预算缩尺寸主导,可读性影响很小。将来若需要图形类专用编码,正确做法是给不透明阶梯加一档 WebP,而不是恢复分类。 +- GIF 源图经 gifload 解码后带 alpha 平面,因此静帧 GIF 规范化走 WebP 阶梯。 +- `IMAGE_TOO_LARGE` 不再产生于编码环节;它仍是超大源图的准入错误。 +- 阶梯耗尽的附件可能以超过字节目标的大小落盘和上行,直到提供方上限拒绝;实测只有对抗性随机 alpha 输入可达。把这样的超目标 master 再次作为新上传提交时,直通的字节检查不通过,会再走一遍有损阶梯,因此规范化对这一仅对抗性可达的类别不幂等,每轮都会累积代际损失。 +- 测试证据:`packages/attachment/attachment-local/tests` 用真实编码器钉住路由、阶梯耗尽和文字可读性行为,包括 issue #2885 误判特征(高频摄影内容离开慢路径)。 diff --git a/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.i18n.yaml b/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.i18n.yaml index 780c4af5ea..57a5f4244b 100644 --- a/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md -2026-06-21-subagent-capability-seam.md: bc84d88d701a5f3018bf00f0ecf8b60750917407 -2026-06-21-subagent-capability-seam.zh.md: 7932181d5667fc8a69ca7aa450fcbf6270ef14d5 +2026-06-21-subagent-capability-seam.md: 70ae99725dffee48292c3481df049563fb54a82c +2026-06-21-subagent-capability-seam.zh.md: 2f63c9022fb116daa2bb6ccd9150d96a823657de diff --git a/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md b/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md index bc84d88d70..70ae99725d 100644 --- a/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md +++ b/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md @@ -45,7 +45,7 @@ A provider exposes `start(request) → Promise`. Fulfillment publis ### Two kinds of optional capability, discovered two ways -- **Start-time features** (`outputSchema`, `depthLimit`, `toolFilter`, `persona`) ride on a static `provider.capabilities` descriptor. The service checks every requested one BEFORE delegating and **rejects loud** (`SubagentError('UNSUPPORTED_CAPABILITY')`) if the provider lacks it — never accepted-then-ignored. They must be checked before a run exists, which is why they cannot be runtime methods. +- **Start-time features** (`agentOptions`, `outputSchema`, `depthLimit`, `toolFilter`, `persona`) ride on a static `provider.capabilities` descriptor. The service checks every requested one BEFORE delegating and **rejects loud** (`SubagentError('UNSUPPORTED_CAPABILITY')`) if the provider lacks it — never accepted-then-ignored. They must be checked before a run exists, which is why they cannot be runtime methods. - **Continuable creation** is the optional `SubagentProvider.prepareContinuable` method; presence is the capability and TypeScript narrowing is the discovery mechanism, so no separate flag can drift from the implementation. The continuation manager owns later delivery and cold resume directly through `AgentHandle`, while one-shot `SubagentRun` has no steering or resume operation, as refined by [continuable subagents](2026-07-28-continuable-subagent-conversations.md). ### Fork vs. fresh are separate backends, not a flag @@ -60,9 +60,9 @@ Each in-process subagent runs in its **own `Session`** (own id, `parentSession` `dsh-tool-subagent` passes its execution signal to `start()`, awaits the child result, and disposes the run before reporting. Non-completed outcomes become error results rather than successful partial output; they present the optional safe diagnostic owned by the [non-interactive permissions decision](2026-08-15-product-subagent-noninteractive-permissions.md) separately from partial assistant text. Independent result and disposal rejections remain independently observable. -### Provider selection is config, not model-facing +### Transport provider selection is config, not model-facing -`dsh-tool-subagent` binds to exactly one provider name (`Config.provider`); the model sees only `{ description, prompt }`. To expose more than one transport, load the tool plugin more than once, each bound to a different provider and a distinct `toolName` (the tool registry rejects a duplicate name). The *service* holds the multi-provider registry; the *tool* picks one — the schema carries no provider/type parameter. +`dsh-tool-subagent` binds to exactly one subagent transport provider name (`Config.provider`). To expose more than one transport, load the tool plugin more than once, each bound to a different provider and a distinct `toolName` (the tool registry rejects a duplicate name). The *service* holds the multi-provider registry; the *tool* picks one — its schema carries no subagent transport/type parameter. A later opt-in adds child LLM provider/model fields without changing this transport decision; see [model-selected subagent routes](2026-08-18-model-selected-subagent-routes.md). ## Testing diff --git a/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.zh.md b/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.zh.md index 7932181d56..2f63c9022f 100644 --- a/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.zh.md +++ b/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.zh.md @@ -45,7 +45,7 @@ bash seam([能力 seam](../architecture/2026-06-13-capability-seams.zh.md)) ### 两类可选能力,两种发现方式 -- **启动时功能**(`outputSchema`、`depthLimit`、`toolFilter`、`persona`)挂在静态的 `provider.capabilities` 描述符上。服务在委派之前检查每个被请求的功能,如果提供方不支持则**响亮拒绝**(`SubagentError('UNSUPPORTED_CAPABILITY')`),绝不接受后静默忽略。这些功能必须在 run 存在之前检查,因此不能是运行时方法。 +- **启动时功能**(`agentOptions`、`outputSchema`、`depthLimit`、`toolFilter`、`persona`)挂在静态的 `provider.capabilities` 描述符上。服务在委派之前检查每个被请求的功能,如果提供方不支持则**响亮拒绝**(`SubagentError('UNSUPPORTED_CAPABILITY')`),绝不接受后静默忽略。这些功能必须在 run 存在之前检查,因此不能是运行时方法。 - **可继续创建**使用可选的 `SubagentProvider.prepareContinuable` 方法;方法是否存在本身即为能力,TypeScript 类型收窄即为发现机制,因此不需要可能与实现失同步的独立 flag。继续执行管理器直接通过 `AgentHandle` 负责后续投递与冷恢复,而一次性 `SubagentRun` 没有 steering 或 resume 操作,具体由[可继续 subagent](2026-07-28-continuable-subagent-conversations.zh.md) 细化。 ### Fork 与 fresh 是独立后端,而非一个 flag @@ -60,9 +60,9 @@ bash seam([能力 seam](../architecture/2026-06-13-capability-seams.zh.md)) `dsh-tool-subagent` 将其执行信号传给 `start()`,等待子 agent 结果,并在报告前 dispose 该 run。非完成态的结果变为错误结果,而非成功的部分输出;它会把由[非交互权限决策](2026-08-15-product-subagent-noninteractive-permissions.zh.md)负责的可选安全诊断与部分 assistant 文本分开呈现。结果与 dispose 的拒绝仍可彼此独立地观察。 -### 提供方选择是配置,不面向模型 +### 传输提供方选择是配置,不面向模型 -`dsh-tool-subagent` 绑定到恰好一个提供方名称(`Config.provider`);模型只看到 `{ description, prompt }`。若要暴露多种传输方式,请多次加载该工具插件,每次绑定不同的提供方和不同的 `toolName`(工具注册表拒绝重名)。*服务*持有多提供方注册表;*工具*选择其中一个——schema 中没有提供方/type 参数。 +`dsh-tool-subagent` 绑定到恰好一个 subagent 传输提供方名称(`Config.provider`)。若要暴露多种传输方式,请多次加载该工具插件,每次绑定不同的提供方和不同的 `toolName`(工具注册表拒绝重名)。*服务*持有多提供方注册表;*工具*选择其中一个——schema 中没有 subagent 传输/type 参数。后续 opt-in 增加了子 agent LLM 提供方/模型字段,但没有改变这项传输决策;见[模型选择的 subagent 路由](2026-08-18-model-selected-subagent-routes.zh.md)。 ## 测试 diff --git a/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.i18n.yaml b/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.i18n.yaml index 1d9c28595a..06e2503aa4 100644 --- a/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md -2026-07-27-typescript-sdk-and-sdk-subagent-backend.md: eda9d3a3de91944a298070d6cc22f632294f7a28 -2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md: 1a72c6b1cdb7453b8468d6e6be37aec76323f714 +2026-07-27-typescript-sdk-and-sdk-subagent-backend.md: 37c341e964b556c7ab5fdd9081416883066b97d1 +2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md: e97e951028de3bcda9fe11be0351072481c72dd9 diff --git a/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md b/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md index eda9d3a3de..37c341e964 100644 --- a/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md +++ b/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md @@ -17,7 +17,7 @@ Three packages, layered exactly like the existing Python stack, plus one Service - **`@deepseek-ai/dsh-subagent-dsh-sdk`** (`packages/subagent/subagent-dsh-sdk/`) — the second out-of-process `SubagentProvider`, structured as `subagent-acp`'s sibling: same all-false capabilities and `inheritsParentContext: false`, same publish-after-handshake ownership transaction, same result-never-rejects flattening through an `onError` sink, same parent-namespace run id. The child answer is read from streamed `session.event`s — the last complete `assistant/message`, else accumulated `text-delta` chunks, so partial answers survive cancellation. Stop reasons map from the child's structured `TurnEndReason` (`completed`/`max-tokens`/`aborted` pass through; everything else, including a settled-without-turn child, is `error`). Its `dshBin`/profile/patch/home config selects an isolated SDK application, `provider`/`model` feeds the child's `initialize`, and `env` supplies explicit child-only values such as its API key. - **The subagent seam grows `out-of-process.ts`**: the provider-side vocabulary both out-of-process backends share — `NO_START_CAPABILITIES`, timing-bound validation, child cwd resolution (config override, else the delegating parent session's workspace), the never-reject `settleRunResult`, and the `subprocessRunHandle` publication. Process mechanics (spawn, env scrub, tree-scoped teardown) live in the `dsh-subprocess` seam; `subagent-acp` spawns through `ctx.subprocess`, while this backend spawns through the SDK client (the subprocess README's documented exception for SDK-managed transports) and applies the seam's `scrubbedParentEnv()` itself. -`dsh-sdk-jsonrpc-server` keeps serving unchanged (the wire is byte-identical); the private `@deepseek-ai/dsh-sdk-python-runtime` carrier consumes the shared protocol through its packaged closure. +`dsh-sdk-jsonrpc-server` keeps serving unchanged (the wire is byte-identical). TypeScript and Python clients both consume the shared protocol through `dsh --profile sdk`; the Python wheel packages that CLI and its closed dependency tree. ## Testing diff --git a/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md b/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md index 1a72c6b1cd..e97e951028 100644 --- a/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md +++ b/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md @@ -17,7 +17,7 @@ stdio JSON-RPC 对外服务接口(`@deepseek-ai/dsh-sdk-jsonrpc-server`,见[ - **`@deepseek-ai/dsh-subagent-dsh-sdk`**(`packages/subagent/subagent-dsh-sdk/`)—— 第二个进程外 `SubagentProvider`,采用与 `subagent-acp` 对等的结构:同样的全 false 能力与 `inheritsParentContext: false`,同样的握手后发布所有权事务,同样通过 `onError` sink 将结果归一为绝不拒绝,同样的父命名空间 run id。子答案从流式 `session.event` 读取——最后一条完整 `assistant/message`,否则累积的 `text-delta` 块,部分答案在取消时得以保留。停止原因由子进程的结构化 `TurnEndReason` 映射(`completed`/`max-tokens`/`aborted` 直通;其余一切、包括未运行任何轮次便已结束的子进程,都是 `error`)。其 `dshBin`/profile/patch/home 配置选择隔离的 SDK 应用,`provider`/`model` 写入子进程 `initialize`,`env` 则提供子进程专用的显式值,例如其 API key。 - **subagent seam 新增 `out-of-process.ts`**:两个进程外后端共享的 provider 侧词汇——`NO_START_CAPABILITIES`、时限校验、子进程 cwd 解析(配置覆盖、否则发起委托的父会话工作区)、绝不拒绝的 `settleRunResult`、以及 `subprocessRunHandle` 发布。进程机制(spawn、环境清理、进程树清理)属于 `dsh-subprocess` seam;`subagent-acp` 经 `ctx.subprocess` spawn 子进程,本后端则经 SDK 客户端 spawn 子进程(subprocess README 记载的 SDK 托管传输例外)并自行应用该 seam 的 `scrubbedParentEnv()`。 -`dsh-sdk-jsonrpc-server` 的服务不变(协议字节完全一致);私有 `@deepseek-ai/dsh-sdk-python-runtime` 载体通过其打包闭包消费共享协议。 +`dsh-sdk-jsonrpc-server` 的服务不变(协议字节完全一致)。TypeScript 与 Python 客户端都通过 `dsh --profile sdk` 消费共享协议;Python wheel 会打包该 CLI 及其封闭依赖树。 ## 测试 diff --git a/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.i18n.yaml index 5d04c6b846..2d37aaba9f 100644 --- a/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md -2026-07-31-web-telemetry-default-mount.md: 9399cbced62e326a46058a5f0c21c949e8737eff -2026-07-31-web-telemetry-default-mount.zh.md: 133c5293fce676567a9ef8d2a47196678f4dc0e4 +2026-07-31-web-telemetry-default-mount.md: a492356eccba9f272ee216777eb518750c7b6b62 +2026-07-31-web-telemetry-default-mount.zh.md: 3d852229c069ae32f328c8dae38cfdf1744c7293 diff --git a/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md b/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md index 9399cbced6..a492356ecc 100644 --- a/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md +++ b/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md @@ -10,7 +10,7 @@ The telemetry seam and OTel backend ([revival Note](2026-07-23-session-telemetry ## Decision -The shared dsh base bundle (`packages/bundle/base/cordis.patch.yml`) mounts the `session-telemetry-otel` row with a baked-in production endpoint, so every profile has one consistent telemetry capability. The [default-off decision](2026-08-10-telemetry-default-off.md) keeps that row in `DISABLED` mode unless a deployment explicitly selects `FULL` or `FEEDBACK_ONLY`; the endpoint alone does not authorize reporting. Web and headless use the [bounded, escalating process-shutdown controller](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.md) on SIGINT/SIGTERM, giving an enabled backend's three-second shutdown deadline time to drain before the five-second launcher bound. +The shared dsh base bundle (`packages/bundle/base/cordis.patch.yml`) mounts the `session-telemetry-otel` row with a baked-in production endpoint, so every base-backed profile has one consistent telemetry capability. The standalone [`sdk-minimal` profile](../architecture/2026-08-24-standalone-sdk-minimal-profile.md) deliberately omits that row. The [default-off decision](2026-08-10-telemetry-default-off.md) keeps the mounted row in `DISABLED` mode unless a deployment explicitly selects `FULL` or `FEEDBACK_ONLY`; the endpoint alone does not authorize reporting. Web and headless use the [bounded, escalating process-shutdown controller](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.md) on SIGINT/SIGTERM, giving an enabled backend's three-second shutdown deadline time to drain before the five-second launcher bound. | Ruling | Value | Rationale | |---|---|---| @@ -27,7 +27,7 @@ The base bundle test pins the shipped `DISABLED` mode expression, the backend su ## Alternatives considered -**No default mount; deployments add the row themselves.** Rejected because the mounted `DISABLED` mode retains a local feedback warning and gives all profiles one patch target without authorizing any upload. +**No default mount; deployments add the row themselves.** Rejected because the mounted `DISABLED` mode retains a local feedback warning and gives every base-backed profile one patch target without authorizing any upload. **A config field instead of an env patch for the switch.** Infeasible: cordis rows have no config-level disable semantic, and `exporter.url` validation fails loud at plugin construction, so the switch must take effect before the Loader — AppCLIEntry's patch layer is the only seat. diff --git a/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.zh.md b/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.zh.md index 133c5293fc..3d852229c0 100644 --- a/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.zh.md +++ b/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.zh.md @@ -10,7 +10,7 @@ Status: implemented ## 决策 -共享 dsh 基础组合包(`packages/bundle/base/cordis.patch.yml`)挂载带有内置生产 endpoint 的 `session-telemetry-otel` 配置行,使每个 profile 都具有一致的遥测能力。[默认关闭决策](2026-08-10-telemetry-default-off.zh.md)让该配置行保持 `DISABLED` 模式,除非部署方显式选择 `FULL` 或 `FEEDBACK_ONLY`;仅配置 endpoint 不构成上报授权。Web 与 headless 在 SIGINT/SIGTERM 时使用[有界、可升级的进程关闭控制器](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.zh.md),在启动器 5 秒上限到期前,先给已启用的后端 3 秒关闭截止时间完成排空。 +共享 dsh 基础组合包(`packages/bundle/base/cordis.patch.yml`)挂载带有内置生产 endpoint 的 `session-telemetry-otel` 配置行,使每个基于 base 的 profile 都具有一致的遥测能力。独立的 [`sdk-minimal` profile](../architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md)刻意省略该配置项。[默认关闭决策](2026-08-10-telemetry-default-off.zh.md)让已挂载配置项保持 `DISABLED` 模式,除非部署方显式选择 `FULL` 或 `FEEDBACK_ONLY`;仅配置 endpoint 不构成上报授权。Web 与 headless 在 SIGINT/SIGTERM 时使用[有界、可升级的进程关闭控制器](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.zh.md),在启动器 5 秒上限到期前,先给已启用的后端 3 秒关闭截止时间完成排空。 | 决策项 | 取值 | 理由 | |---|---|---| @@ -28,7 +28,7 @@ Status: implemented ## 考虑过的替代方案 -**默认不挂载,部署方自行添加配置行。** 不采用:挂载的 `DISABLED` 模式会保留本地反馈警告,并为所有 profile 提供同一个 patch 目标,同时不授权任何上传。 +**默认不挂载,部署方自行添加配置行。** 不采用:挂载的 `DISABLED` 模式会保留本地反馈警告,并为每个基于 base 的 profile 提供同一个 patch 目标,同时不授权任何上传。 **开关做成 config 字段而非 env patch。** 不可行:cordis 行没有 config 层的 disable 语义,且 `exporter.url` 校验在插件构造期 fail-loud,开关必须在 Loader 之前生效——AppCLIEntry patch 层是唯一落点。 diff --git a/.agents/notes/implemented/feature/2026-08-06-web-queue-steer-all-gesture.i18n.yaml b/.agents/notes/implemented/feature/2026-08-06-web-queue-steer-all-gesture.i18n.yaml index b47b635768..e7612e322b 100644 --- a/.agents/notes/implemented/feature/2026-08-06-web-queue-steer-all-gesture.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-06-web-queue-steer-all-gesture.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-06-web-queue-steer-all-gesture.md -2026-08-06-web-queue-steer-all-gesture.md: e546f68647dfc9b91ce4699cef4a64694ebc4f76 -2026-08-06-web-queue-steer-all-gesture.zh.md: 1f65933ac430ca22ac8d6c78471a3bd08b421ad6 +2026-08-06-web-queue-steer-all-gesture.md: c51e837d37b61a73f9602445daea9fbdfc77a299 +2026-08-06-web-queue-steer-all-gesture.zh.md: b0009a7a51ebe89f61d1a4b995b1138a7247d266 diff --git a/.agents/notes/implemented/feature/2026-08-06-web-queue-steer-all-gesture.md b/.agents/notes/implemented/feature/2026-08-06-web-queue-steer-all-gesture.md index e546f68647..c51e837d37 100644 --- a/.agents/notes/implemented/feature/2026-08-06-web-queue-steer-all-gesture.md +++ b/.agents/notes/implemented/feature/2026-08-06-web-queue-steer-all-gesture.md @@ -30,4 +30,4 @@ The per-row 插话发送 action and its strict-steer boundary are owned by [Stee - **Steering via `session.prompt(mode: 'steer')` per row.** Rejected: that mints new messages instead of transferring the pending occurrences and would split the dock's immutable-message contract; `updateQueue({ kind: 'steer' })` already atomically transfers the exact occurrence. - **Firing all row steers concurrently.** Rejected: arrival order at the host is not guaranteed, and steering order is model-visible; sequential awaits preserve FIFO. - **A new host RPC for steer-all.** Rejected: the existing per-item operation is idempotent enough — each row is one strict steer, and mid-flush closure converges silently — so a protocol change buys nothing. -- **A send-button tooltip.** Rejected: the primary button is Stop while an ordinary session is running, which is the only window where the whole-queue gesture is available. The empty-draft placeholder occupies that exact window and can describe the keyboard action directly. +- **A send-button tooltip.** Rejected: the primary button is Stop in the empty-draft running window, which is also the only window where the whole-queue gesture is available. The placeholder occupies that exact window and can describe the keyboard action directly. diff --git a/.agents/notes/implemented/feature/2026-08-06-web-queue-steer-all-gesture.zh.md b/.agents/notes/implemented/feature/2026-08-06-web-queue-steer-all-gesture.zh.md index 1f65933ac4..b0009a7a51 100644 --- a/.agents/notes/implemented/feature/2026-08-06-web-queue-steer-all-gesture.zh.md +++ b/.agents/notes/implemented/feature/2026-08-06-web-queue-steer-all-gesture.zh.md @@ -30,4 +30,4 @@ Status: implemented - **逐条用 `session.prompt(mode: 'steer')` 插话。** 已拒绝:那会铸造新消息而不是转移 pending 行,破坏 dock 的不可变消息契约;`updateQueue({ kind: 'steer' })` 已经原子地转移了确切的那条。 - **并发触发所有行。** 已拒绝:host 到达顺序无法保证,而插话顺序对模型可见;顺序 await 保证 FIFO。 - **为 steer-all 新增 host RPC。** 已拒绝:现有逐条操作已足够幂等——每行一次严格 steer,中途关闭静默收敛——协议改动没有收益。 -- **发送按钮 tooltip。** 已拒绝:普通会话运行时,主按钮是 Stop,这也是整队列手势唯一可用的窗口。空草稿时的 placeholder 恰好在该窗口显示,可以直接说明这项键盘操作。 +- **发送按钮 tooltip。** 已拒绝:主按钮在空草稿的运行窗口内是 Stop,而这也正是整队列手势唯一可用的窗口。placeholder 恰好在该窗口显示,可以直接说明这项键盘操作。 diff --git a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.i18n.yaml b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.i18n.yaml index c548bbbcad..4a576e9339 100644 --- a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md -2026-08-11-minimal-profiles-bare-two-tool-runtime.md: 7d068aebb6642602aac0a039c8635acf555ccfe8 -2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md: c32a9a2e09ff0acda592062f780427c636fd65dd +2026-08-11-minimal-profiles-bare-two-tool-runtime.md: 6ea86832b632c631b7e02d6c486f3858fd2632a4 +2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md: 73f6f16a51c14a7c98915a84878b39596d12f245 diff --git a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md index 7d068aebb6..6ea86832b6 100644 --- a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md +++ b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md @@ -12,17 +12,17 @@ The two launch paths also have different configuration owners. Web mounts a per- ## Decision -Both shipped minimal profiles expose exactly persistent `bash` and `str_replace_editor`, mount no context-compaction provider, suppress every `dsh-system-prompt` runtime-context contribution for fresh sessions, and run the editor against `@deepseek-ai/dsh-fs-local`. The Web preset isolates `ctx.fs` inside the agent entry and mounts `fs-local` beside the editor, so other Web agents retain the host filesystem provider. Its persona remains the fixed complete prompt owned by the earlier [minimal-preset composition decision](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md) and applies runtime-context suppression only to that agent scope. The standalone spine forwards the same setting to its process-owned system-prompt service. Sandbox and approval services remain mounted and enforce their policies; only their model-facing dynamic context is absent. +Both shipped minimal profiles expose exactly persistent `bash` and `str_replace_editor`, mount no context-compaction provider, suppress every `dsh-system-prompt` runtime-context contribution for fresh sessions, and run the editor against `@deepseek-ai/dsh-fs-local`. The Web preset isolates `ctx.fs` inside the agent entry and mounts `fs-local` beside the editor, so other Web agents retain the host filesystem provider. Its persona remains the fixed complete prompt owned by the earlier [minimal-preset composition decision](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md) and applies runtime-context suppression only to that agent scope. The standalone spine forwards the same setting to its process-owned system-prompt service. The Web host retains its sandbox and approval services; the standalone profile mounts a danger-full-access sandbox policy and no approval service. Neither contributes model-facing policy context. -The standalone [`minimal.cordis.yml`](../../../../examples/python-sdk-agent/minimal.cordis.yml) remains a complete JSON-RPC process composition. It mounts `dsh-sdk-jsonrpc-server`, the local PTY and subprocess services required by persistent Bash, `fs-local`, the two tool consumers, and uncompressed JSONL persistence. It does not mount `token-meter`, `compaction-basic`, `fs-sandbox`, or `fs-observation-policy`. Persistent Bash still consumes the deployment's danger-full-access sandbox policy; the editor is not confined by that policy. +The standalone [`@deepseek-ai/dsh-sdk-minimal` bundle](../../../../packages/bundle/sdk-minimal/README.md) remains a complete JSON-RPC process composition behind `dsh --profile sdk-minimal`. It mounts SDK startup and JSON-RPC serving, the local PTY and subprocess services required by persistent Bash, `fs-local`, the two tool consumers, and uncompressed JSONL persistence under `$DSH_HOME/sessions`. It does not mount `token-meter`, `compaction-basic`, `fs-sandbox`, or `fs-observation-policy`. Persistent Bash still consumes the profile's danger-full-access sandbox policy; the editor is not confined by that policy. The [standalone-profile decision](../architecture/2026-08-24-standalone-sdk-minimal-profile.md) owns this bundle placement and its separation from `dsh-base`. -`DSH_SYSTEM_PROMPT` selects the standalone persona. `DSH_MODEL` names the DeepSeek provider catalog entry, and `DSH_CONTEXT_WINDOW` supplies that entry's capacity. Because the SDK client owns the JSON-RPC `initialize` request, [`minimal.py`](../../../../examples/python-sdk-agent/minimal.py) also uses `DSH_MODEL` as its default `model` argument; an explicit `--model` remains authoritative. Endpoint and credential variables stay owned by the DeepSeek adapter's existing environment-resolution path. +`DSH_SYSTEM_PROMPT` selects the standalone persona, and `DSH_CONTEXT_WINDOW` supplies fallback capacity for a model without exact catalog metadata. The SDK client's JSON-RPC `initialize` request is the sole runtime model selection. [`minimal.py`](../../../../examples/python-sdk-agent/minimal.py) may read `DSH_MODEL` only as the command's default `model` argument; an explicit `--model` needs no matching child environment value. Endpoint and credential variables stay owned by the DeepSeek adapter's existing environment-resolution path. ## Verification The Web replay boots the complete Web host, creates the agent through the preset service, and asserts that the scoped filesystem is bare, no scoped compaction service exists, no system-prompt-owned runtime-context message was appended, and the assembled request contains exactly the fixed prompt and two tools. It then executes persistent Bash and the editor against the real scoped services. -The SDK replay boots the real JSON-RPC agent process through the SDK client, injects an environment-selected prompt, asserts the assembled prompt, exact two-tool catalog, and absence of every system-prompt-owned runtime-context message, and executes both tools. Python SDK bundled-runtime coverage initializes the standalone configuration through each available packaged carrier with environment-selected model, model capacity, and prompt values. Cordis validation checks that both configurations resolve their declared plugins and configuration fields. +The SDK keyless process test boots real `dsh --profile sdk-minimal`, injects an environment-selected prompt, and asserts the generated one-bundle manifest, assembled prompt, exact two-tool catalog, and absence of every system-prompt-owned runtime-context message. Python SDK bundled-runtime coverage initializes the standalone profile through each available packaged carrier with environment-selected model, model capacity, and prompt values, then executes both tools. Cordis validation checks that both configurations resolve their declared plugins and configuration fields. ## Alternatives considered @@ -32,8 +32,8 @@ The SDK replay boots the real JSON-RPC agent process through the SDK client, inj **Use one Cordis leaf for Web and Python SDK startup.** Rejected because a Web preset contributes agent-scoped services to an existing multi-session host, while the Python SDK must launch a complete process containing the JSON-RPC server and its process-wide dependencies. -**Read `DSH_MODEL` only inside Cordis.** Rejected because Cordis configures the provider catalog but does not own the SDK client's JSON-RPC `initialize` request. The launcher must pass the same model to the client request for the environment value to select the routed model. +**Mirror the requested model into `DSH_MODEL`.** Rejected because the direct adapter accepts model ids outside its advisory catalog and resolves fallback context metadata for them. Mirroring creates two inputs for one selection; the SDK initialization request is authoritative, while `DSH_MODEL` remains only a convenience default in `minimal.py`. ## Consequences -Minimal sessions never summarize or replace earlier history and never add a runtime-context snapshot; callers must keep turns within the selected model's context capacity and must not rely on model-visible narration of standing sandbox or approval policy. The editor can address any absolute path visible to the runtime process, independently of the persistent shell's sandbox policy. The two launch paths share their model-facing tool, no-context, and no-compaction guarantees while retaining different prompt and model configuration appropriate to their owners. The Python SDK path continues to communicate only through the bundled stdio JSON-RPC runtime. +Minimal sessions never summarize or replace earlier history and never add a runtime-context snapshot; callers must keep turns within the selected model's context capacity and must not rely on model-visible narration of standing sandbox or approval policy. The editor can address any absolute path visible to the runtime process, independently of the persistent shell's sandbox policy. The two launch paths share their model-facing tool, no-context, and no-compaction guarantees while retaining different prompt and model configuration appropriate to their owners. The Python SDK path communicates only through the bundled `dsh` stdio JSON-RPC profile. diff --git a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md index c32a9a2e09..73f6f16a51 100644 --- a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md +++ b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md @@ -12,17 +12,17 @@ Web `minimal` preset 与独立 JSON-RPC minimal 组合对外提供持久 `bash` ## 决策 -两种随附 minimal profile 都只对外提供持久 `bash` 与 `str_replace_editor`,不挂载上下文压缩提供方,为新建会话抑制每个 `dsh-system-prompt` runtime-context 贡献,并让编辑器使用 `@deepseek-ai/dsh-fs-local`。Web preset 在 agent entry 内隔离 `ctx.fs`,将 `fs-local` 与编辑器一起挂载,因此其他 Web agent 仍使用宿主文件系统提供方。其 persona 继续采用较早的 [minimal preset 组合决策](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md)所拥有的固定 complete 提示词,并仅为该 agent 作用域实施 runtime-context 抑制。独立 spine 将同一设置转发给其进程拥有的 system-prompt 服务。沙箱与批准服务仍保持挂载并强制其策略;只有它们面向模型的动态上下文缺席。 +两种随附 minimal profile 都只对外提供持久 `bash` 与 `str_replace_editor`,不挂载上下文压缩提供方,为新建会话抑制每个 `dsh-system-prompt` runtime-context 贡献,并让编辑器使用 `@deepseek-ai/dsh-fs-local`。Web preset 在 agent entry 内隔离 `ctx.fs`,将 `fs-local` 与编辑器一起挂载,因此其他 Web agent 仍使用宿主文件系统提供方。其 persona 继续采用较早的 [minimal preset 组合决策](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md)所拥有的固定 complete 提示词,并仅为该 agent 作用域实施 runtime-context 抑制。独立 spine 将同一设置转发给其进程拥有的 system-prompt 服务。Web 宿主保留沙箱与批准服务;独立 profile 挂载 danger-full-access 沙箱策略,不挂载批准服务。两者都不贡献面向模型的策略上下文。 -独立的 [`minimal.cordis.yml`](../../../../examples/python-sdk-agent/minimal.cordis.yml) 仍是完整的 JSON-RPC 进程组合。它挂载 `dsh-sdk-jsonrpc-server`、持久 Bash 所需的本地 PTY 和子进程服务、`fs-local`、两个工具消费方,以及未压缩的 JSONL 持久化。它不挂载 `token-meter`、`compaction-basic`、`fs-sandbox` 或 `fs-observation-policy`。持久 Bash 仍消费部署的 danger-full-access 沙箱策略;编辑器不受该策略限制。 +独立的 [`@deepseek-ai/dsh-sdk-minimal` 组合包](../../../../packages/bundle/sdk-minimal/README.zh.md)仍是 `dsh --profile sdk-minimal` 后面的完整 JSON-RPC 进程组合。它挂载 SDK 启动与 JSON-RPC 服务、持久 Bash 所需的本地 PTY 和子进程服务、`fs-local`、两个工具消费方,以及位于 `$DSH_HOME/sessions` 的未压缩 JSONL 持久化。它不挂载 `token-meter`、`compaction-basic`、`fs-sandbox` 或 `fs-observation-policy`。持久 Bash 仍消费该 profile 的 danger-full-access 沙箱策略;编辑器不受该策略限制。[独立 profile 决策](../architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md)负责该组合包的位置及其与 `dsh-base` 的分离。 -`DSH_SYSTEM_PROMPT` 选择独立组合的 persona。`DSH_MODEL` 命名 DeepSeek 提供方目录项,`DSH_CONTEXT_WINDOW` 提供该目录项的容量。由于 SDK 客户端拥有 JSON-RPC `initialize` 请求,[`minimal.py`](../../../../examples/python-sdk-agent/minimal.py)也使用 `DSH_MODEL` 作为 `model` 参数的默认值;显式 `--model` 仍具有最高优先级。端点与凭据变量继续由 DeepSeek 适配器现有的环境解析路径持有。 +`DSH_SYSTEM_PROMPT` 选择独立组合的 persona,`DSH_CONTEXT_WINDOW` 为没有确切目录元数据的模型提供后备容量。SDK 客户端的 JSON-RPC `initialize` 请求是唯一运行时模型选择。[`minimal.py`](../../../../examples/python-sdk-agent/minimal.py)可以只把 `DSH_MODEL` 读作命令的默认 `model` 参数;显式 `--model` 不需要匹配的子进程环境值。端点与凭据变量继续由 DeepSeek 适配器现有的环境解析路径持有。 ## 验证 Web 回放会启动完整 Web 宿主,通过 preset 服务创建 agent,并断言作用域文件系统为裸后端、不存在作用域压缩服务、没有追加 system-prompt 拥有的 runtime-context 消息,而且组装请求只包含固定提示词与两个工具。随后,它通过真实作用域服务执行持久 Bash 和编辑器。 -SDK 回放通过 SDK 客户端启动真实 JSON-RPC agent 进程,注入由环境选择的提示词,断言组装提示词与精确双工具目录,另外断言不存在任何 system-prompt 拥有的 runtime-context 消息,并执行两个工具。Python SDK 内置运行时覆盖会通过每种可用的打包载体,使用环境选择的模型、模型容量和提示词值初始化独立配置。Cordis 校验会检查两份配置能否解析声明的插件和配置字段。 +SDK keyless 进程测试启动真实 `dsh --profile sdk-minimal`,注入由环境选择的提示词,并断言生成的单组合包 manifest、组装提示词、精确双工具目录,以及不存在任何 system-prompt 拥有的 runtime-context 消息。Python SDK 内置运行时覆盖会通过每种可用的打包载体,使用环境选择的模型、模型容量和提示词值初始化独立 profile,然后执行两个工具。Cordis 校验会检查两份配置能否解析声明的插件和配置字段。 ## 考虑过的替代方案 @@ -32,8 +32,8 @@ SDK 回放通过 SDK 客户端启动真实 JSON-RPC agent 进程,注入由环 **为 Web 与 Python SDK 启动使用同一个 Cordis leaf。** 不予采用,因为 Web preset 向现有多会话宿主贡献 agent 作用域服务,而 Python SDK 必须启动包含 JSON-RPC 服务器及其进程级依赖的完整进程。 -**只在 Cordis 内读取 `DSH_MODEL`。** 不予采用,因为 Cordis 配置提供方目录,但不拥有 SDK 客户端的 JSON-RPC `initialize` 请求。launcher 必须向客户端请求传递同一个模型,环境值才能选择路由模型。 +**把请求模型镜像到 `DSH_MODEL`。** 不予采用,因为直接适配器接受不在建议目录中的模型 id,并为它们解析后备上下文元数据。镜像会为同一项选择制造两个输入;SDK 初始化请求具有权威,`DSH_MODEL` 只保留为 `minimal.py` 的便捷默认值。 ## 后果 -Minimal 会话不会摘要或替换较早历史,也不会添加 runtime-context 快照;调用方必须让会话轮次保持在所选模型的上下文容量内,且不得依赖模型可见的常驻沙箱或批准策略说明。编辑器可以访问运行时进程可见的任何绝对路径,且不受持久 shell 沙箱策略影响。两条启动路径共享面向模型的工具、无上下文与无压缩保证,同时保留适合各自所有者的不同提示词和模型配置。Python SDK 路径继续仅通过内置 stdio JSON-RPC 运行时通信。 +Minimal 会话不会摘要或替换较早历史,也不会添加 runtime-context 快照;调用方必须让会话轮次保持在所选模型的上下文容量内,且不得依赖模型可见的常驻沙箱或批准策略说明。编辑器可以访问运行时进程可见的任何绝对路径,且不受持久 shell 沙箱策略影响。两条启动路径共享面向模型的工具、无上下文与无压缩保证,同时保留适合各自所有者的不同提示词和模型配置。Python SDK 路径只通过内置 `dsh` stdio JSON-RPC profile 通信。 diff --git a/.agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.i18n.yaml b/.agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.i18n.yaml new file mode 100644 index 0000000000..d21dd5be39 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.md +2026-08-18-model-selected-subagent-routes.md: 1602e3ac90870edbd0206cd87fdf97ecc34cad41 +2026-08-18-model-selected-subagent-routes.zh.md: 0dad8b9d030e6de65cb3fa1e0e93ad7c28bcc5c1 diff --git a/.agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.md b/.agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.md new file mode 100644 index 0000000000..1602e3ac90 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.md @@ -0,0 +1,61 @@ +# Agent Note: Model-selected subagent routes + +Status: implemented + +English | [中文](2026-08-18-model-selected-subagent-routes.zh.md) + +## Problem + +`dsh-tool-subagent` can configure child `AgentOptions`, and both in-process providers merge those values over the parent Agent's LLM selection. The model-facing tool could not request a different provider, model, or reasoning effort for one suitable subtask. Loading one distinctly named delegation tool per LLM route duplicates schemas and turns a per-call scheduling choice into deployment configuration. + +The model also needs a bounded way to discover live providers and model-owned effort ids. Rendering the adapter directory into every delegation description would make an advisory, mutable catalog part of the prompt prefix. + +## Decision + +`dsh-tool-subagent` exposes optional `provider`, `model`, and `reasoning_effort` fields only when its instance enables `enableModelSelection`, or its Agent-scoped `modelSelectionSettings` instance resolves an enabled Session decision, and the bound subagent provider advertises `SubagentCapabilities.agentOptions`. No route allowlist is required. Registered LLM provider routes are available for child selection; this tool does not add a second authorization policy over the deployment's LLM registry. Disabled instances omit and reject model-facing selection, while configured `Config.agentOptions` remain deployment-owned defaults. Either selection mode against a provider without the capability fails the plugin mount. + +Provider and model form one route and must be supplied together. An effort may be supplied alone when configured or parent values provide the effective route. Model arguments override `Config.agentOptions`, and configured fields override the parent Agent's latest logged request selection; creation options supply the fallback before its first request and retain the configured output-token limit. Reasoning-effort identifiers remain adapter-owned. An unchanged route inherits an omitted effort, while changing provider or model without naming an effort clears the lower layer's route-owned value so the selected model resolves its own default. `AgentOptions` carries the resulting effort into the child loop, whose request header logs the effective value. A continuable descriptor records it with the resolved provider and model so a child that has not logged its first request can cold-resume with the same selection. + +An explicit or configured provider, model, or effort resolves through `ctx.llm.resolveCallConfig()` before child creation. That lookup owns provider registration, exact-model metadata, reasoning-effort validation, and adapter defaults. The tool checks cancellation again after the asynchronous lookup and before creating a child or background job. Calls with no model-facing selection and no configured route fields preserve the existing provider path without requiring the optional LLM service. + +An enabled definition registers `list_subagent_models`. With no arguments the tool lists registered providers; with `provider` it calls that adapter's advisory model catalog; with `provider` and `model` it resolves the exact model and returns its reasoning efforts and default. At most one instance in a tool scope enables selection because the discovery name is global. Shipped product compositions put `modelSelectionSettings: true` on the primary Agent-scoped `subagent` instance and register the Host-owned `subagent-model-selection` settings namespace with `enabled: false`. A new top-level Session samples that preference during composition and logs an enabled decision as `subagent/model-selection-enabled` before any model request. A child Session inherits the live parent's decision, and a resumed Session uses its existing marker instead of the current preference. Therefore a settings edit affects only subsequently composed top-level Sessions. The fixed discovery definition remains available without the optional LLM service, while discovery and selected-route calls fail until that service is present. An unlisted model remains selectable when the adapter accepts its id. + +Shipped `subagent_fork` instances leave `enableModelSelection` disabled even though the in-process fork provider supports `agentOptions`. A fork inherits the parent's effective provider and model so its copied conversation prefix remains eligible for provider-side KV Cache reuse. Changing either route component requires the new route to prefill that inherited history again, and that recomputation can dominate the delegated task's cost. This restriction is independent of the discovery tool's global name: separating discovery ownership would permit the configuration but would not preserve reuse. Fork route selection remains unavailable until a route change can retain prefix reuse or the caller can explicitly bound and accept the recomputation cost. + +The delegation definition is static across adapter registration and catalog changes, so live topology neither expands every parent request nor invalidates its cache prefix. The discovery result enters the transcript only when called. A custom inheritance-capable instance that enables selection warns that changing provider or model can prevent provider-side reuse of the inherited conversation prefix. + +`SubagentCapabilities.agentOptions` remains the transport truth. The service rejects a request carrying those options before calling a provider that advertises `false`. Both in-process providers advertise `true`; the current ACP, Codex, Claude Code, and DSH SDK transports advertise `false`. Tool configuration that supplies `agentOptions`, statically enables model selection, or makes it settings-controlled also fails when its bound provider lacks the capability. + +## Alternatives considered + +**Keep a deployment-configured route allowlist.** Rejected because it duplicates the live LLM registry, requires configuration before the model can use an already registered route, and creates a second policy surface for clients to edit. Deployments that must restrict LLM access should control which provider routes they register. + +**Render the live adapter catalog in every delegation description.** Rejected because one provider can advertise hundreds of models, inflating every request, and catalog changes would rewrite an early cache-prefix definition. The on-demand directory keeps mutable data out of the fixed schema. + +**Use the advertised catalog as an allowlist.** Rejected because adapter catalogs are advisory and some providers accept arbitrary exact model ids. Exact resolution remains authoritative. + +**Add discovery methods to the subagent service.** Rejected because provider/model/effort metadata already belongs to `ctx.llm`; the new tool is a model-facing consumer of that existing capability. + +**Export discovery as a separately loaded plugin entry.** Rejected because shipped compositions always pair discovery with their primary delegation tool. Explicit ownership on that instance prevents duplicate global names without another Cordis config entry or lifecycle. + +**Configure discovery independently from model-facing selection.** Rejected because discovery exists to supply valid route and effort identifiers to the same model that can select them. One switch prevents a tool schema from advertising selection without its discovery path, or discovery without an applicable delegation route. + +**Enable model-facing route selection on shipped fork tools.** Not shipped because changing provider or model forfeits the inherited prefix's KV Cache reuse and can make prefix recomputation more expensive than the delegated work. The option can be reconsidered when reuse survives the route change or the interface makes that cost explicit and bounded. + +**Use a global reasoning-effort enum.** Rejected because effort identifiers and defaults belong to an exact provider/model route. The LLM adapter validates them without central translation or clamping. + +**Allow remote providers to ignore the fields.** Rejected because the request would claim a route choice that did not happen. The capability flag makes the unsupported path fail before child creation. + +## Consequences + +- An enabled delegation tool can select any live child LLM route without deployment selector configuration; disabled instances omit and reject model-facing route fields. +- The primary delegation-tool instance defaults selection off, exposes a Models-page opt-in for new Sessions, and registers `list_subagent_models` only in Sessions whose durable decision is enabled; its catalog rows do not restrict delegation. +- Shipped fork tools inherit the parent's provider and model and omit model-facing route fields so the inherited conversation prefix remains eligible for KV Cache reuse. +- Omission retains configured defaults and compatible inheritance from the parent's latest logged request; a route change without an explicit effort uses the selected model's default. +- Adapter catalog and topology changes leave the delegation definition and its prompt-cache prefix unchanged. +- Out-of-process subagent providers reject configured and model-selected Agent options until they implement and advertise the capability. +- Unit coverage owns the default-off Host preference, new-Session sampling, child inheritance, resumed decisions, opt-in schema and execution enforcement, merge precedence, route-aware effort inheritance, preflight cancellation, live discovery, diagnostics, definition stability, capability rejection, and optional-service behavior. A shipped headless snapshot pins inheritance from a logged parent selection; the shipped examples also own the assembled keyless model-visible schemas. + +## Related decisions + +This note refines only child LLM routing. The fixed subagent transport remains owned by the [subagent capability seam](2026-06-21-subagent-capability-seam.md), while the separate effect of child-scoped prompt and tool additions on fork prefix reuse remains owned by [cache-preserving forked children stay one-shot](../architecture/2026-08-10-fork-children-stay-one-shot.md). diff --git a/.agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.zh.md b/.agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.zh.md new file mode 100644 index 0000000000..0dad8b9d03 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.zh.md @@ -0,0 +1,61 @@ +# Agent Note: 模型选择的 subagent 路由 + +Status: implemented + +[English](2026-08-18-model-selected-subagent-routes.md) | 中文 + +## 问题 + +`dsh-tool-subagent` 可以配置子级 `AgentOptions`,两个进程内提供方也已把这些值合并到父 Agent 的 LLM 选择之上。但面向模型的工具不能为某个适合的子任务请求不同的提供方、模型或推理强度。为每条 LLM 路由加载一个名称不同的委派工具会重复 schema,并把每次调用的调度选择变成部署配置。 + +模型还需要一种有界方式来发现实时提供方和模型自有的推理强度 ID。把 adapter 目录渲染到每一份委派描述中,会让仅供参考且会变化的目录进入 prompt 前缀。 + +## 决策 + +只有实例启用 `enableModelSelection`,或其 Agent 作用域的 `modelSelectionSettings` 实例解析出已启用的 Session 决定,且绑定的 subagent 提供方声明 `SubagentCapabilities.agentOptions` 时,`dsh-tool-subagent` 才公开可选的 `provider`、`model` 与 `reasoning_effort` 字段,不要求配置路由允许列表。已注册的 LLM 提供方路由都可供子级选择;本工具不会在部署的 LLM 注册表之上增加第二套授权策略。禁用的实例会省略并拒绝面向模型的选择,而配置的 `Config.agentOptions` 仍是部署方所有的默认值。如果提供方缺少该能力,任一种选择模式都会使插件挂载失败。 + +提供方与模型共同组成一条路由,必须一起提供。如果配置值或父级值能够提供生效路由,则可以只提供推理强度。模型参数覆盖 `Config.agentOptions`,配置字段覆盖父 Agent 最新记录的请求选择;首个请求之前由创建选项提供回退,并保留其中配置的输出 token 上限。推理强度 ID 仍由 adapter 所有。路由不变时会继承省略的强度;更换提供方或模型但没有指定强度时,会清除下层路由自有的值,使所选模型解析自己的默认值。`AgentOptions` 把结果强度传入子级循环,其请求 header 会记录生效值。可继续描述符会把它与解析后的提供方和模型一同记录,使尚未写入首个请求的子级能以相同选择冷恢复。 + +显式或配置的提供方、模型或强度会在创建子级前通过 `ctx.llm.resolveCallConfig()` 解析。该查询负责提供方注册、精确模型元数据、推理强度校验和 adapter 默认值。异步查询完成后、创建子级或后台 job 之前,工具会再次检查取消状态。既没有面向模型的选择、也没有配置路由字段的调用会保留原有提供方路径,不要求可选 LLM 服务存在。 + +启用的定义会注册 `list_subagent_models`。无参数调用列出已注册提供方;提供 `provider` 时调用该适配器的建议性模型目录;同时提供 `provider` 与 `model` 时解析精确模型,并返回其推理强度和默认值。因为发现工具使用全局名称,一个工具作用域最多由一个实例启用选择。随附产品组合在 Agent 作用域的主 `subagent` 实例上设置 `modelSelectionSettings: true`,并注册默认 `enabled: false` 的 Host 自有 `subagent-model-selection` settings namespace。新的顶层 Session 会在组合期间读取该偏好,并在任何模型请求之前把启用决定记录为 `subagent/model-selection-enabled`。子 Session 继承在线父级的决定;恢复的 Session 使用已有标记,而不是当前偏好。因此,设置修改只影响之后组合的顶层 Session。即使缺少可选 LLM 服务,固定发现定义仍保持可用;发现调用和所选路由调用会在该服务出现前失败。只要适配器接受某个未列出的模型 ID,仍可选择该模型。 + +随附的 `subagent_fork` 实例不会启用 `enableModelSelection`,即使进程内 fork 提供方支持 `agentOptions` 也是如此。fork 会继承父级生效的提供方与模型,使复制的对话前缀仍可供提供方侧 KV Cache 复用。更改任一路由组件都会要求新路由重新预填充继承的历史,而这项重算成本可能超过委派任务本身。该限制与发现工具的全局名称无关:分离发现工具的持有权可以让配置生效,却无法保留复用。只有在路由变化仍能保留前缀复用,或调用方可以显式限制并接受重算成本时,才重新考虑 fork 路由选择。 + +委派定义不会随 adapter 注册和目录变化而改变,因此实时拓扑既不会扩大每个父级请求,也不会使缓存前缀失效。只有调用发现工具时,目录结果才进入 transcript。自定义的上下文继承实例如果启用选择,其描述会警告,更改提供方或模型可能阻止提供方复用继承的对话前缀。 + +`SubagentCapabilities.agentOptions` 仍是传输事实。如果请求携带这些选项,而提供方声明为 `false`,服务会在调用提供方前拒绝。两个进程内提供方声明为 `true`;当前 ACP、Codex、Claude Code 与 DSH SDK 传输声明为 `false`。工具配置提供 `agentOptions`、静态启用模型选择或让它受 settings 控制时,如果绑定的提供方缺少该能力,也会失败。 + +## 考虑过的替代方案 + +**保留部署配置的路由允许列表。** 不采用,因为它重复实时 LLM 注册表,要求先配置才能让模型使用已经注册的路由,并为客户端增加第二套策略编辑界面。需要限制 LLM 访问的部署应控制所注册的提供方路由。 + +**在每一份委派描述中渲染实时 adapter 目录。** 不采用,因为一个提供方可能公布数百个模型,从而扩大每次请求,而且目录变化会改写缓存前缀中的早期定义。按需目录让可变数据留在固定 schema 之外。 + +**把公布的目录当作允许列表。** 不采用,因为 adapter 目录只提供建议,有些提供方接受任意精确模型 ID。精确解析仍是权威。 + +**在 subagent 服务中增加发现方法。** 不采用,因为提供方/模型/强度元数据已经属于 `ctx.llm`;新工具只是该现有能力面向模型的 Consumer。 + +**把发现工具作为独立加载的插件入口导出。** 不采用,因为随附组合总是把发现工具与主委派工具配套加载。在该实例上显式指定持有权,可以避免重复的全局工具名,无需增加 Cordis 配置项或独立生命周期。 + +**分别配置发现与面向模型的选择。** 不采用,因为发现功能用于向能够选择这些值的同一个模型提供有效的路由与强度 ID。一个开关可以避免工具 schema 公开选择却没有对应发现路径,或公开发现却没有适用的委派路由。 + +**在随附 fork 工具上启用面向模型的路由选择。** 不随产品提供,因为更改提供方或模型会失去继承前缀的 KV Cache 复用,重新预填充前缀的成本可能高于委派工作本身。只有在路由变化仍能保留复用,或接口能把这项成本显式化并限制住时,才重新考虑该选项。 + +**使用全局推理强度枚举。** 不采用,因为推理强度 ID 和默认值属于精确的提供方/模型路由。LLM adapter 会直接校验,无需中心化翻译或截断。 + +**允许远程提供方忽略这些字段。** 不采用,因为请求会声称发生了实际上没有发生的路由选择。能力标记会让不支持的路径在创建子级前失败。 + +## 结果 + +- 启用的委派工具无需部署选择器配置,即可选择任意实时子级 LLM 路由;禁用的实例会省略并拒绝面向模型的路由字段。 +- 主委派工具实例默认关闭选择,为新 Session 提供 Models 页面 opt-in,并且只在持久决定已启用的 Session 中注册 `list_subagent_models`;其目录条目不会限制委派。 +- 随附 fork 工具会继承父级的提供方与模型,并省略面向模型的路由字段,使继承的对话前缀仍可供 KV Cache 复用。 +- 省略选择时保留配置默认值,并从父级最新记录的请求中进行兼容继承;改变路由但不显式指定强度时,使用所选模型的默认值。 +- adapter 目录和拓扑变化不会改变委派定义及其 prompt 缓存前缀。 +- 进程外 subagent 提供方在实现并声明该能力前,会拒绝配置和模型选择的 Agent 选项。 +- 单元测试覆盖默认关闭的 Host 偏好、新 Session 读取、子级继承、恢复决定、选择启用时的 schema 与执行强制、合并优先级、路由相关强度继承、预检取消、实时发现、诊断、定义稳定性、能力拒绝与可选服务行为。随附的 headless 快照固定从父级已记录选择继承的行为;随附示例还覆盖组装后无密钥、模型可见的 schema。 + +## 相关决策 + +本 Note 仅细化子级 LLM 路由。固定的 subagent 传输仍由 [subagent 能力 seam](2026-06-21-subagent-capability-seam.zh.md)负责,而子级作用域提示词与工具增量对 fork 前缀复用产生的独立影响仍由[保留缓存的 fork child 保持 one-shot](../architecture/2026-08-10-fork-children-stay-one-shot.zh.md)负责。 diff --git a/.agents/notes/implemented/feature/2026-08-20-unified-image-request-pipeline.i18n.yaml b/.agents/notes/implemented/feature/2026-08-20-unified-image-request-pipeline.i18n.yaml index 66ba6d1139..55331d4816 100644 --- a/.agents/notes/implemented/feature/2026-08-20-unified-image-request-pipeline.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-20-unified-image-request-pipeline.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-20-unified-image-request-pipeline.md -2026-08-20-unified-image-request-pipeline.md: 85296f1d8bd7d6ee458f8bc4230e52f1d3128bff -2026-08-20-unified-image-request-pipeline.zh.md: bcbc0110001e9f974e57a456140e9ecdf4eb888c +2026-08-20-unified-image-request-pipeline.md: 1c7a8040232bab0d73f578a9170e235c2f175c07 +2026-08-20-unified-image-request-pipeline.zh.md: af6497f5a06018b87d166842615f6b1e12b1aed4 diff --git a/.agents/notes/implemented/feature/2026-08-20-unified-image-request-pipeline.md b/.agents/notes/implemented/feature/2026-08-20-unified-image-request-pipeline.md index 85296f1d8b..1c7a804023 100644 --- a/.agents/notes/implemented/feature/2026-08-20-unified-image-request-pipeline.md +++ b/.agents/notes/implemented/feature/2026-08-20-unified-image-request-pipeline.md @@ -14,17 +14,17 @@ The image path has two explicit versions. The attachment backend owns a provider ### Provider-independent normalized attachment -Admission accepts at most 20 images and 200MiB of encoded source bytes per message. Each source is fully decoded under configurable 20MiB, 64,000,000-pixel, and 8192px-per-side limits. Normalization applies EXIF orientation, removes metadata and color profiles, converts to 8-bit sRGB/sRGBA, and preserves aspect ratio while limiting the long edge to `normalizedImageMaxDimension`, 2048px by default. When scaling reduces the raster, `originalDimensions` records its orientation-applied width and height before normalization. +Admission accepts at most 20 images and 200MiB of encoded source bytes per message. Each source is fully decoded under configurable 20MiB, 64,000,000-pixel, and 8192px-per-side limits. Normalization applies EXIF orientation, removes metadata and color profiles, converts to 8-bit sRGB/sRGBA, and preserves aspect ratio while scaling into the `normalizedImageMaxPixels` total-pixel budget (2048x2048 by default) under a `normalizedImageMaxDimension` long-edge cap (8192px by default). When scaling reduces the raster, `originalDimensions` records its orientation-applied width and height before normalization. -The normalized attachment has an independent `normalizedImageMaxBytes` safety cap, 4MiB by default. Alpha is never flattened. A nearest-neighbour bounded sample classifies color complexity without averaging high-frequency pixels. Confirmed low-color input tries PNG, with palette encoding only when no alpha channel is present, followed by WebP qualities 85, 80, and 75. Other alpha input tries WebP at those qualities; other opaque input tries JPEG. Candidates execute in order and stop at the first result within the cap. Dimensions shrink only after every candidate at one size exceeds the cap. The source extension does not classify a PNG as low color. A clean, single-frame 8-bit sRGB/sRGBA PNG, JPEG, or WebP within both normalization limits passes through byte-identically and retains content-addressed deduplication. GIF, animation, metadata, orientation, 16-bit PNG, and incompatible color spaces force conversion. The source and a converted output are each fully decoded once; the output must match its format, dimensions, depth, color space, and alpha facts before its digest enters the reference. +The normalized attachment has an independent `normalizedImageMaxBytes` encoded-byte target, 4MiB by default. Alpha is never flattened. Codec routing is by the decoded alpha fact alone — alpha input encodes as WebP (effort 0) and opaque input as JPEG, each down the shared 85/75/60 quality ladder; when every quality exceeds the target the smallest output is kept, per the superseding [alpha-routed quality ladders note](../bug-fix/2026-08-24-alpha-routed-image-quality-ladders.md). A clean, single-frame 8-bit sRGB/sRGBA PNG, JPEG, or WebP within every normalization limit passes through byte-identically and retains content-addressed deduplication. GIF, animation, metadata, orientation, 16-bit PNG, and incompatible color spaces force conversion. The source and a converted output are each fully decoded once; the output must match its format, dimensions, depth, color space, and alpha facts before its digest enters the reference. Batch admission prepares and verifies every normalized attachment once before publishing any member. Validation failure starts no writes. Publication uses those prepared bytes directly, so a large batch does not repeat full decoding and encoding during commit. A later storage failure returns no partial references; already published immutable objects may remain unreachable under the existing storage rule. ### Deterministic request versions -`AttachmentStore.readImageRequest` derives a request version under route-owned total-pixel and encoded-byte budgets. Scaling is `min(1, sqrt(maxPixels / (width * height)))`, with no enlargement, followed by inward integer rounding so the encoded raster never exceeds the total-pixel cap. DeepSeek V4 Flash Vision Exp uses 640,000 total pixels and 1MiB raw encoded bytes by default. Its catalog uses one `imagePixelBudget` field: a positive integer selects an exact total-pixel budget, `low` selects 512 by 512 total pixels, and omission selects the route default. A 2048 by 1024 normalized attachment projects to 1130 by 565 under the hard cap. Request encoding uses the same color branches, with PNG (palette only without alpha) then WebP 85 and 80 for low-color input, WebP 85 then 80 for other alpha input, and JPEG 85 then 80 for other opaque input. Each fallback runs only after the previous result exceeds 1MiB, and dimensions shrink only after both quality attempts exceed it. The same derivation is used by normal agent turns, direct `ctx.llm.stream` calls, compaction, and other auxiliary streams. +`AttachmentStore.readImageRequest` derives a request version under route-owned total-pixel and encoded-byte budgets. Scaling is `min(1, sqrt(maxPixels / (width * height)))`, with no enlargement, followed by inward integer rounding so the encoded raster never exceeds the total-pixel cap. DeepSeek V4 Flash Vision Exp uses 640,000 total pixels and a 1MiB raw encoded-byte target by default. Its catalog uses one `imagePixelBudget` field: a positive integer selects an exact total-pixel budget, `low` selects 512 by 512 total pixels, and omission selects the route default. A 2048 by 1024 normalized attachment projects to 1130 by 565 under the hard cap. Request encoding uses the same alpha routing and 85/75/60 quality ladder as normalization, executed lazily; a target no quality meets keeps the smallest ladder output (see the [alpha-routed quality ladders note](../bug-fix/2026-08-24-alpha-routed-image-quality-ladders.md)). The same derivation is used by normal agent turns, direct `ctx.llm.stream` calls, compaction, and other auxiliary streams. -The `variantId` and cache path cover the normalized attachment id, transform version, route pixel and byte budgets, and fixed encoder parameters. A new cache entry is fully decoded before publication. Cache hits use a header probe to check format, 8-bit sRGB/sRGBA facts, dimensions, alpha, and byte limits without decoding the complete raster again; a mismatch regenerates the entry. DeepSeek Files and pi-ai inline base64 therefore use the same deterministic bytes for the same policy. Inline accounting uses the derived byte length after base64 expansion, not the normalized attachment byte count. Equal in-process `variantId` calls share one transform and cache write. Each caller can cancel its own wait; the shared transform is aborted only after every waiter has cancelled. Callers preserve order by applying `Promise.all` to singular `readImageRequest` calls. The local implementation runs normalization and request transforms through one FIFO limiter; `imageCompressionConcurrency` is configurable from 1 through 8 and defaults to 2. Batch publication remains sequential after every normalized attachment has been prepared. +The `variantId` and cache path cover the normalized attachment id, transform version, route pixel and byte budgets, and fixed encoder parameters. A new cache entry is fully decoded before publication. Cache hits use a header probe to check format, 8-bit sRGB/sRGBA facts, dimensions, and alpha without decoding the complete raster again; a mismatch regenerates the entry. DeepSeek Files and pi-ai inline base64 therefore use the same deterministic bytes for the same policy. Inline accounting uses the derived byte length after base64 expansion, not the normalized attachment byte count. Equal in-process `variantId` calls share one transform and cache write. Each caller can cancel its own wait; the shared transform is aborted only after every waiter has cancelled. Callers preserve order by applying `Promise.all` to singular `readImageRequest` calls. The local implementation runs normalization and request transforms through one FIFO limiter; `imageCompressionConcurrency` is configurable from 1 through 8 and defaults to 2. Batch publication remains sequential after every normalized attachment has been prepared. Request-size offload is a deterministic oldest-first projection. Before reading attachments, each route uses `min(attachmentBytes, requestVersionMaxBytes)` as a conservative upper bound and removes the oldest over-budget prefix. Only retained attachments are read and transformed, so an omitted missing or corrupt object cannot block the request. A second projection uses exact derived lengths without bringing omitted images back. DeepSeek defaults to 128MiB and 600 referenced images. Its removed prefix advances past successive 64MiB byte boundaries and in 20-image count quanta, so 129 one-megabyte images remove the oldest 65, retain 64MiB, and keep that prefix stable until total history passes 192MiB. Pi-ai retains a configurable base64 request bound. Each omitted image becomes a per-image placeholder that retains its identity and access resolved for the current tool execution world, including nested tool-result images, while append-only session history keeps the original references. @@ -62,7 +62,7 @@ Historical attachment objects that later disappear or fail integrity verificatio ## Verification -Package tests generate 16-bit RGB and RGBA PNG fixtures, prove 8-bit conversion and clean 8-bit passthrough, retain alpha under byte pressure, distinguish high-frequency and ordinary photos from low-color graphics, stop lazy encoding after the first fitting candidate, cover square and wide 640,000-pixel projections, enforce 1MiB request bytes, singleflight equal variants and uploads without shared-cancellation leaks, bound transform concurrency, preserve cache and upload identity, skip attachment reads for conservatively offloaded history, prepare batches once, reject inconsistent Files responses, refresh near-expiry ids without retrieve, recover once from single-id, multiple-id, and ambiguous stale responses, fall back to bounded all-inline requests after file resolution failure, paginate before quota deletion, normalize provider diagnostics, project text-only history, and share normal/compaction request bytes. Keyless assembled snapshots cover the real tool schemas and image request path. A credentialed test uses the built-in `deepseek-official` route and its configured endpoint, never a custom provider entry. +Package tests generate 16-bit RGB and RGBA PNG fixtures, prove 8-bit conversion and clean 8-bit passthrough, retain alpha under byte pressure, stop lazy encoding after the first fitting candidate, keep the smallest ladder output above an unreachable byte target, cover square and wide 640,000-pixel projections, singleflight equal variants and uploads without shared-cancellation leaks, bound transform concurrency, preserve cache and upload identity, skip attachment reads for conservatively offloaded history, prepare batches once, reject inconsistent Files responses, refresh near-expiry ids without retrieve, recover once from single-id, multiple-id, and ambiguous stale responses, fall back to bounded all-inline requests after file resolution failure, paginate before quota deletion, normalize provider diagnostics, project text-only history, and share normal/compaction request bytes. Keyless assembled snapshots cover the real tool schemas and image request path. A credentialed test uses the built-in `deepseek-official` route and its configured endpoint, never a custom provider entry. ## Consequences diff --git a/.agents/notes/implemented/feature/2026-08-20-unified-image-request-pipeline.zh.md b/.agents/notes/implemented/feature/2026-08-20-unified-image-request-pipeline.zh.md index bcbc011000..af6497f5a0 100644 --- a/.agents/notes/implemented/feature/2026-08-20-unified-image-request-pipeline.zh.md +++ b/.agents/notes/implemented/feature/2026-08-20-unified-image-request-pipeline.zh.md @@ -14,17 +14,17 @@ Status: implemented ### 提供方无关的规范化附件 -每条消息最多准入 20 张图片,源图编码字节总量不超过 200MiB。每张源图会在可配置的 20MiB、64,000,000 像素和单边 8192px 限制内完整解码。规范化过程会应用 EXIF 方向,删除元数据和色彩配置文件,转换为 8-bit sRGB/sRGBA,并保持宽高比把长边限制到 `normalizedImageMaxDimension`,默认 2048px。缩放减小光栅时,`originalDimensions` 记录规范化之前、应用方向之后的输入宽高。 +每条消息最多准入 20 张图片,源图编码字节总量不超过 200MiB。每张源图会在可配置的 20MiB、64,000,000 像素和单边 8192px 限制内完整解码。规范化过程会应用 EXIF 方向,删除元数据和色彩配置文件,转换为 8-bit sRGB/sRGBA,并保持宽高比缩放进 `normalizedImageMaxPixels` 总像素预算(默认 2048×2048),随后受 `normalizedImageMaxDimension` 长边上限约束(默认 8192px)。缩放减小光栅时,`originalDimensions` 记录规范化之前、应用方向之后的输入宽高。 -规范化附件有独立的 `normalizedImageMaxBytes` 安全上限,默认 4MiB。透明通道绝不铺平。系统通过 nearest-neighbour 对有界样本判断色彩复杂度,不会通过像素平均把高频图片误判为低色数。确认的低色数输入先尝试 PNG,只有不带 alpha 通道时才使用 palette,随后依次尝试质量 85、80、75 的 WebP;其他透明输入依次尝试这些质量的 WebP;其他非透明输入依次尝试这些质量的 JPEG。候选按顺序执行,首个不超过上限的结果会立即返回。同一尺寸的候选全部超限后才会缩小尺寸。源扩展名不会把 PNG 归类为低色数图片。处于两个规范化上限内的干净、单帧、8-bit sRGB/sRGBA PNG、JPEG 或 WebP 按字节原样直通,并保留内容寻址去重。GIF、动图、元数据、方向、16-bit PNG 和不兼容色彩空间都会触发转换。源图和转换输出各完整解码一次;输出的格式、尺寸、位深、色彩空间和透明通道事实通过校验后,其摘要才会进入引用。 +规范化附件有独立的 `normalizedImageMaxBytes` 编码字节目标,默认 4MiB。透明通道绝不铺平。编码路由只看解码出的 alpha 事实:透明输入编码为 WebP(effort 0),非透明输入编码为 JPEG,共用 85/75/60 质量阶梯;全部档位都超过目标时保留最小产物,见取代本节的[按 alpha 路由的质量阶梯记录](../bug-fix/2026-08-24-alpha-routed-image-quality-ladders.zh.md)。处于全部规范化限制内的干净、单帧、8-bit sRGB/sRGBA PNG、JPEG 或 WebP 按字节原样直通,并保留内容寻址去重。GIF、动图、元数据、方向、16-bit PNG 和不兼容色彩空间都会触发转换。源图和转换输出各完整解码一次;输出的格式、尺寸、位深、色彩空间和透明通道事实通过校验后,其摘要才会进入引用。 批量准入在发布任何成员前,为每张图片各准备并验证一次规范化附件。校验失败不会开始写入。发布直接使用这些已准备字节,因此大批次不会在提交时重复完整解码和编码。之后发生的存储失败不会返回部分引用;按现有存储规则,已经发布的不可变对象可能保持不可达。 ### 确定性请求版本 -`AttachmentStore.readImageRequest` 按路由拥有的总像素和编码字节预算派生请求版本。缩放公式为 `min(1, sqrt(maxPixels / (width * height)))`,不会放大小图,随后向预算内取整,确保编码光栅不超过总像素上限。DeepSeek V4 Flash Vision Exp 默认使用总像素 640,000 和原始编码字节 1MiB。其 catalog 只使用一个 `imagePixelBudget` 字段:正整数选择确切总像素预算,`low` 选择总像素 512×512,省略时使用路由默认值。2048×1024 规范化附件在这个硬上限下会投影为 1130×565。请求编码使用相同的分类分支:低色数输入先尝试 PNG,只有不带 alpha 通道时才使用 palette,随后依次尝试质量 85、80 的 WebP;其他透明输入依次尝试质量 85、80 的 WebP;其他非透明输入依次尝试质量 85、80 的 JPEG。只有前一结果超过 1MiB 时才执行下一个候选;两个质量档都超限后才缩小尺寸。普通 agent 轮次、直接 `ctx.llm.stream` 调用、压缩和其他辅助流都使用同一派生过程。 +`AttachmentStore.readImageRequest` 按路由拥有的总像素和编码字节预算派生请求版本。缩放公式为 `min(1, sqrt(maxPixels / (width * height)))`,不会放大小图,随后向预算内取整,确保编码光栅不超过总像素上限。DeepSeek V4 Flash Vision Exp 默认使用总像素 640,000 和原始编码字节目标 1MiB。其 catalog 只使用一个 `imagePixelBudget` 字段:正整数选择确切总像素预算,`low` 选择总像素 512×512,省略时使用路由默认值。2048×1024 规范化附件在这个硬上限下会投影为 1130×565。请求编码与规范化共用同一套 alpha 路由和 85/75/60 质量阶梯,按需执行;没有任何档位达到目标时保留最小产物(见[按 alpha 路由的质量阶梯记录](../bug-fix/2026-08-24-alpha-routed-image-quality-ladders.zh.md))。普通 agent 轮次、直接 `ctx.llm.stream` 调用、压缩和其他辅助流都使用同一派生过程。 -`variantId` 和缓存路径覆盖规范化附件 ID、变换策略版本、路由像素和字节预算及固定编码参数。新缓存条目在发布前会完整解码。缓存命中只探测文件头,校验格式、8-bit sRGB/sRGBA、尺寸、透明通道和字节上限,不会再次完整解码光栅;不匹配时会重新生成。因此,同一策略下的 DeepSeek Files 和 pi-ai 内联 base64 使用相同的确定性字节。内联计量使用派生字节经过 base64 膨胀后的长度,不使用规范化附件字节数。同一进程内相同 `variantId` 的调用共享一次变换和缓存写入。每个调用方可以取消自己的等待;只有全部等待方都取消时,共享变换才会中止。调用方对单数 `readImageRequest` 使用 `Promise.all` 保持结果顺序。本地实现通过一个 FIFO 限流器运行规范化和请求变换,`imageCompressionConcurrency` 的可配置范围为 1 至 8,默认值为 2。全部规范化附件准备完成后,批次仍按顺序发布。 +`variantId` 和缓存路径覆盖规范化附件 ID、变换策略版本、路由像素和字节预算及固定编码参数。新缓存条目在发布前会完整解码。缓存命中只探测文件头,校验格式、8-bit sRGB/sRGBA、尺寸和透明通道,不会再次完整解码光栅;不匹配时会重新生成。因此,同一策略下的 DeepSeek Files 和 pi-ai 内联 base64 使用相同的确定性字节。内联计量使用派生字节经过 base64 膨胀后的长度,不使用规范化附件字节数。同一进程内相同 `variantId` 的调用共享一次变换和缓存写入。每个调用方可以取消自己的等待;只有全部等待方都取消时,共享变换才会中止。调用方对单数 `readImageRequest` 使用 `Promise.all` 保持结果顺序。本地实现通过一个 FIFO 限流器运行规范化和请求变换,`imageCompressionConcurrency` 的可配置范围为 1 至 8,默认值为 2。全部规范化附件准备完成后,批次仍按顺序发布。 请求大小 offload 是确定性的从旧到新投影。读取附件前,每条路由先以 `min(附件字节数, 请求版本字节上限)` 作为保守上界,移除超出预算的最旧前缀。系统只读取并转换保留的附件,因此已省略的缺失或损坏对象不会阻塞请求。第二次投影使用确切派生长度,但不会重新加入已省略图片。DeepSeek 默认上限为 128MiB 和 600 张引用图片。被移除前缀会越过连续的 64MiB 字节边界,并按 20 张图片数量步长递增,因此 129 张 1MiB 图片会移除最旧的 65 张并保留 64MiB;持久历史超过 192MiB 前,该前缀保持不变。Pi-ai 保留可配置的 base64 请求上限。每张省略图片都会变成逐图占位文本,保留自己的身份和本次工具执行环境解析出的访问方式,嵌套工具结果图片也使用相同规则;追加式会话历史继续保留原始引用。 @@ -62,7 +62,7 @@ Status: implemented ## Verification -包测试会生成 16-bit RGB 和 RGBA PNG fixture,验证 8-bit 转换与干净 8-bit 字节直通、字节压力下保留透明通道、区分高频和普通照片与低色数图形、首个候选合规后停止编码、正方形和宽屏 640,000 像素投影、请求字节不超过 1MiB、相同变体与上传 singleflight 且不会共享取消、变换并发上限、缓存与上传身份、跳过已保守 offload 的历史附件读取、批量只准备一次、Files 响应不一致、进入刷新余量时不查询远端并更新 ID、单个 ID、多个 ID 和模糊失效响应只恢复一次、文件解析失败后回退到有界全内联请求、删除配额文件前完成分页、规范化提供方诊断、纯文本投影,以及普通请求与压缩共享请求字节。无需密钥的组装快照覆盖真实工具 schema 和图片请求路径。使用凭据的测试只使用内置 `deepseek-official` 路由及其已配置端点,不使用自定义提供方条目。 +包测试会生成 16-bit RGB 和 RGBA PNG fixture,验证 8-bit 转换与干净 8-bit 字节直通、字节压力下保留透明通道、首个候选合规后停止编码、字节目标不可达时保留最小阶梯产物、正方形和宽屏 640,000 像素投影、相同变体与上传 singleflight 且不会共享取消、变换并发上限、缓存与上传身份、跳过已保守 offload 的历史附件读取、批量只准备一次、Files 响应不一致、进入刷新余量时不查询远端并更新 ID、单个 ID、多个 ID 和模糊失效响应只恢复一次、文件解析失败后回退到有界全内联请求、删除配额文件前完成分页、规范化提供方诊断、纯文本投影,以及普通请求与压缩共享请求字节。无需密钥的组装快照覆盖真实工具 schema 和图片请求路径。使用凭据的测试只使用内置 `deepseek-official` 路由及其已配置端点,不使用自定义提供方条目。 ## Consequences diff --git a/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.i18n.yaml b/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.i18n.yaml index d2eb7495d9..534ca917cc 100644 --- a/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.i18n.yaml +++ b/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.md -2026-08-13-python-minimal-model-visible-snapshot.md: 37c76be93fb4f18fa99ceec7c15d20e572f2dfb3 -2026-08-13-python-minimal-model-visible-snapshot.zh.md: 5c2b0dd5fcd6ec5ea3a68eb884e19b0917bbe0be +2026-08-13-python-minimal-model-visible-snapshot.md: 1cef57c440ddd2209628016ab550174b448587bc +2026-08-13-python-minimal-model-visible-snapshot.zh.md: fbbb3bd6f4c3edadb61a02eebbae3dfaa97cb8e7 diff --git a/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.md b/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.md index 37c76be93f..1cef57c440 100644 --- a/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.md +++ b/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.md @@ -6,15 +6,13 @@ English | [中文](2026-08-13-python-minimal-model-visible-snapshot.zh.md) ## Problem -The Python lane never compared what the minimal composition actually shows the model. Dynamic runtime context reaches history as a user message, so the mock model's assertion that system-role messages equal the deployment persona could not see it, and the advanced executable snapshot replaces each request header's assembled system prompt with a token and each tool schema with its name. The sandbox-policy runtime-context message therefore rode along in the checked-in [minimal composition](../../../../examples/python-sdk-agent/minimal.cordis.yml) while `python-runtime` stayed green, and any plugin that adds a system section, a tool, or another context message could do the same. +The Python lane needs an exact record of what the standalone minimal profile shows the model. Functional tool assertions prove execution but do not reveal an added system section, tool description, or user-role context message, while the advanced executable snapshot replaces each request header's assembled system prompt with a token and each tool schema with its name. ## Decision -The `sdk-minimal` scenario in [the packaged-runtime smoke](../../../../scripts/smoke-python-runtime.py) records `scripts/snapshots/python-sdk-single-exe/minimal/model-visible.json`: for every model request of the turn, the advertised tool schemas verbatim and the message list. System and user messages keep their full text with the scenario's temporary directory tokenized; assistant and tool messages keep only call identity, because their PTY and filesystem text differs across the platforms the expected output replays on. +The `sdk-minimal` scenario in [the packaged-runtime smoke](../../../../scripts/smoke-python-runtime.py) boots the shipped profile and records `scripts/snapshots/python-sdk-single-exe/minimal/model-visible.json`: for every model request of the turn, the advertised tool schemas verbatim and the message list. System and user messages keep their full text with the scenario's temporary directory tokenized; assistant and tool messages keep only call identity, because their PTY and filesystem text differs across replay platforms. The profile omits dynamic runtime context, so every message it emits is compared. -One model-visible message is excluded: the agent loop's dynamic runtime-context snapshot. The same composition emits it on macOS and not on Linux, which the required lane runs, so no single expected output can carry it. That difference is a defect in its own right ([#2488](https://github.com/deepseek-harness/deepseek-harness/issues/2488)) — this expected output covers every other model-visible message rather than waiting for it. - -The mock model no longer asserts the minimal scenario's tools and system prompts — the snapshot owns that surface and reports a complete diff instead of the first mismatch. Snapshot comparison takes its directory and file set as arguments, so the `minimal` and `advanced` expected outputs use one implementation, and `--update-snapshots` accepts `sdk-minimal`. +The snapshot, rather than inline mock-model assertions, owns the minimal scenario's tools and system prompts and reports their complete diff. Snapshot comparison takes its directory and file set as arguments, so the `minimal` and `advanced` expected outputs use one implementation, and `--update-snapshots` accepts `sdk-minimal`. ## Alternatives considered @@ -22,12 +20,12 @@ The mock model no longer asserts the minimal scenario's tools and system prompts **Extend the mock model's inline assertions.** Every new model-visible contribution would need another hand-written expectation, and a failure names one mismatch rather than the whole surface. Tool descriptions would also be duplicated from the composition into the script. -**Rely on the TypeScript SDK snapshot.** Its `persistent-tools` scenario pins the same composition's system prompt, tool schemas, and runtime context, but through replayed model responses and a source or `lib` runtime, in a different required job. It cannot show what the deployed executable's closure assembles for a Python caller. +**Rely on the TypeScript SDK snapshot.** Its `persistent-tools` scenario pins a similar two-tool composition through replayed model responses and a source or `lib` runtime, in a different required job. It cannot show what the deployed executable's shipped profile assembles for a Python caller. ## Consequences A change to the minimal composition's model-visible surface — a system section, a tool, a tool description, or an added user message — now fails `python-runtime` with the exact diff, and landing it means rerunning `--scenario sdk-minimal --update-snapshots` and reviewing that diff. The minimal composition's tool descriptions become reviewed expected output. -Assistant and tool message text is no longer compared, and the runtime-context snapshot is not compared at all. The scenario's own assertions continue to own persistent-shell state, editor output, and the final response; [#2488](https://github.com/deepseek-harness/deepseek-harness/issues/2488) owns the excluded message until its platform difference is resolved. +Assistant and tool message text is not compared. The scenario's own assertions continue to own persistent-shell state, editor output, and the final response; the snapshot owns every model-visible message the profile emits. [AGENTS.md](../../../../AGENTS.md) and [the testing policy](../../../../docs/testing.md) now name both SDKs as independent projections of the agent loop, session lifecycle, and `SessionEventMap`, so a change to any of those carries updating both expected outputs rather than only the one a contributor happens to run. diff --git a/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.zh.md b/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.zh.md index 5c2b0dd5fc..fbbb3bd6f4 100644 --- a/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.zh.md +++ b/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.zh.md @@ -6,15 +6,13 @@ Status: implemented ## 问题 -Python 通道从未比对极简组合实际展示给模型的内容。动态运行时上下文以 user 消息进入历史,因此 mock 模型"system 角色消息等于部署 persona"的断言看不见它;而进阶可执行文件快照会把每个请求头中已组装的系统提示词换成占位符、把每个工具 schema 换成其名称。于是 sandbox-policy 的运行时上下文消息一直搭车留在签入的[极简组合](../../../../examples/python-sdk-agent/minimal.cordis.yml)里,而 `python-runtime` 始终是绿的;任何新增系统分段、工具或其他上下文消息的插件都能照此蒙混过关。 +Python 通道需要精确记录独立极简 profile 实际展示给模型的内容。功能性工具断言可以证明执行,但无法发现新增系统分段、工具描述或 user 角色上下文消息;而进阶可执行文件快照会把每个请求头中已组装的系统提示词换成占位符,并把每个工具 schema 换成其名称。 ## 决策 -[打包运行时冒烟测试](../../../../scripts/smoke-python-runtime.py)的 `sdk-minimal` 场景会录制 `scripts/snapshots/python-sdk-single-exe/minimal/model-visible.json`:对该回合的每个模型请求,逐字记录对外公布的工具 schema 与消息列表。system 与 user 消息保留全文,仅将场景的临时目录替换为占位符;assistant 与 tool 消息只保留调用标识,因为它们的 PTY 与文件系统文本在期望输出需要重放的各平台上并不相同。 +[打包运行时冒烟测试](../../../../scripts/smoke-python-runtime.py)的 `sdk-minimal` 场景会启动随附 profile,并录制 `scripts/snapshots/python-sdk-single-exe/minimal/model-visible.json`:对该回合的每个模型请求,逐字记录对外公布的工具 schema 与消息列表。system 与 user 消息保留全文,仅将场景的临时目录替换为占位符;assistant 与 tool 消息只保留调用标识,因为它们的 PTY 与文件系统文本在各回放平台上并不相同。该 profile 省略动态运行时上下文,因此它发出的每条消息都会参与比对。 -有一条模型可见消息被排除在外:agent loop 的动态运行时上下文快照。同一组合在 macOS 上会发出它,在必需车道所用的 Linux 上不会,因此任何单一期望输出都无法承载它。该差异本身就是缺陷([#2488](https://github.com/deepseek-harness/deepseek-harness/issues/2488))——这份期望输出覆盖其余全部模型可见消息,而不是等它先被修复。 - -mock 模型不再断言极简场景的工具与系统提示词——该面由快照拥有,并给出完整差异而非首个不匹配项。快照比对以目录与文件集合为参数,因此 `minimal` 与 `advanced` 两份期望输出共用一套实现,且 `--update-snapshots` 接受 `sdk-minimal`。 +极简场景的工具与系统提示词由快照拥有,而不是由 mock 模型内联断言;快照会给出其完整差异。快照比对以目录与文件集合为参数,因此 `minimal` 与 `advanced` 两份期望输出共用一套实现,且 `--update-snapshots` 接受 `sdk-minimal`。 ## 曾考虑的替代方案 @@ -22,12 +20,12 @@ mock 模型不再断言极简场景的工具与系统提示词——该面由快 **扩展 mock 模型中的内联断言。** 每新增一项模型可见贡献都要再手写一条期望,且失败只会指出一处不匹配而非整个面。工具描述还会从组合复制进脚本,形成重复。 -**依赖 TypeScript SDK 快照。** 其 `persistent-tools` 场景固定了同一组合的系统提示词、工具 schema 与运行时上下文,但走的是重放的模型响应与 source 或 `lib` 运行时,且位于另一个必需任务中。它无法体现已部署可执行文件的闭包为 Python 调用方组装出什么。 +**依赖 TypeScript SDK 快照。** 其 `persistent-tools` 场景通过重放模型响应与 source 或 `lib` 运行时固定一套相似的双工具组合,且位于另一个必需任务中。它无法体现已部署可执行文件的随附 profile 为 Python 调用方组装出什么。 ## 后果 极简组合模型可见面的改动——系统分段、工具、工具描述或新增的 user 消息——现在会让 `python-runtime` 带着精确差异失败;要让它落地,就必须重新运行 `--scenario sdk-minimal --update-snapshots` 并审阅该差异。极简组合的工具描述由此成为经过审阅的期望输出。 -assistant 与 tool 消息文本不再参与比对,运行时上下文快照则完全不参与比对。持久 shell 状态、编辑器输出与最终响应仍由该场景自身的断言拥有;被排除的那条消息由 [#2488](https://github.com/deepseek-harness/deepseek-harness/issues/2488) 负责,直到其平台差异得到解决。 +assistant 与 tool 消息文本不参与比对。持久 shell 状态、编辑器输出与最终响应仍由该场景自身的断言拥有;快照负责该 profile 发出的每条模型可见消息。 [AGENTS.md](../../../../AGENTS.md) 与[测试政策](../../../../docs/testing.zh.md)现已点明两个 SDK 都是 agent loop、会话生命周期与 `SessionEventMap` 的独立投影,因此改动其中任何一项都要连带更新两侧的期望输出,而不只是贡献者恰好会运行的那一侧。 diff --git a/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.i18n.yaml b/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.i18n.yaml index 93a20e387f..635b5df7af 100644 --- a/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.i18n.yaml +++ b/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.md -2026-08-23-installed-python-wheel-black-box-ci.md: f2b5bd0edeb02a5d72e8010c62c3cfb59ee95c5d -2026-08-23-installed-python-wheel-black-box-ci.zh.md: fb0f5fb2da676f1a24a2630bd45f005f46a3095e +2026-08-23-installed-python-wheel-black-box-ci.md: a2fd4134d7bff0e74aa2d1afc3590e9cdd90809e +2026-08-23-installed-python-wheel-black-box-ci.zh.md: 203440ed0f6bbe84257474dd69400a64bc480cd3 diff --git a/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.md b/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.md index f2b5bd0ede..a2fd4134d7 100644 --- a/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.md +++ b/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.md @@ -18,7 +18,7 @@ The black-box harness rejects a non-venv process, repository-relative working di ### Keyless behavior -Every target runs the complete packaged-runtime scenario set after installation. A local SSE model keeps outputs deterministic while the public SDK exercises the default configuration, an external complete configuration, persistent PTY and editor behavior, worker-thread code and workflow execution, ripgrep-backed search, external stdio MCP discovery and execution, model-visible and durable snapshots, JSONL/Zstandard persistence, direct JSON-RPC, and shutdown. A restart snapshot launches two complete SDK runtime processes against one persistence root and pins their isolated model histories, high-level results, and separate durable logs. The installed run replaces the source-SDK pre-wheel run; the executable and wheel are tested together once rather than maintaining two behavior inventories. +Every target runs the complete packaged-runtime scenario set after installation. A local SSE model keeps outputs deterministic while the public SDK exercises the default SDK profile, ordered patch overlays, external bundle installation through `dsh plugin`, persistent PTY and editor behavior, worker-thread code and workflow execution, ripgrep-backed search, external stdio MCP discovery and execution, model-visible and durable snapshots, Zstandard persistence, direct JSON-RPC, and shutdown. A restart snapshot launches two complete SDK runtime processes against one persistence root and pins their isolated model histories, high-level results, and separate durable logs. The installed run replaces the source-SDK pre-wheel run; the executable and wheel are tested together once rather than maintaining two behavior inventories. Linux additionally retains its manylinux 2.28 clean-install smoke and GLIBC checks. macOS retains deployment-target and native helper checks. These platform constraints supplement the common black-box behavior rather than substituting for it. @@ -34,7 +34,7 @@ The pull-request `python-runtime` job calls the reusable builder for Linux x64, ## Existing decisions and supersession -This decision supersedes the single-target topology in the archived [required Python runtime pull-request validation](../../archived/testing/2026-08-12-required-python-runtime-pull-request-ci.md) while retaining its requirement that the real executable, snapshots, wheels, and clean installation meet before merge. The [single-file Python SDK runtime distribution](../architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md) remains authoritative for SEA packaging, the closed dependency set, native sidecars, wheel tags, and release artifacts. +This decision supersedes the single-target topology in the archived [required Python runtime pull-request validation](../../archived/testing/2026-08-12-required-python-runtime-pull-request-ci.md) while retaining its requirement that the real executable, snapshots, wheels, and clean installation meet before merge. The [Python SDK dsh profile runtime](../architecture/2026-08-23-python-sdk-dsh-profile-runtime.md) owns the launched application and customization surface; the [single-file Python SDK runtime distribution](../architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md) remains authoritative for SEA packaging, native sidecars, wheel tags, and release artifacts. ## Alternatives considered diff --git a/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md b/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md index fb0f5fb2da..203440ed0f 100644 --- a/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md +++ b/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md @@ -18,7 +18,7 @@ Python SDK 单元测试驱动 fake peer,而打包运行时工作流可以在 ### Keyless 行为 -每个目标都会在安装后运行完整的打包运行时场景。一个本地 SSE mock 模型提供确定性输出,公开 SDK 则覆盖默认配置、外部完整配置、持久 PTY 与 editor 行为、worker thread 代码与 workflow 执行、基于 ripgrep 的搜索、外部 stdio MCP 发现与执行、模型可见及持久化快照、JSONL/Zstandard 持久化、直接 JSON-RPC 与关闭。Restart 快照针对同一持久化根目录启动两个完整 SDK 运行时进程,并固定其彼此隔离的模型历史、高层结果与独立持久日志。安装后运行取代 wheel 构建前的源码 SDK 运行,因此可执行文件与 wheel 包共同接受一次验证,而不是维护两套行为清单。 +每个目标都会在安装后运行完整的打包运行时场景。一个本地 SSE mock 模型提供确定性输出,公开 SDK 则覆盖默认 SDK profile、有序 patch overlay、通过 `dsh plugin` 安装外部 bundle、持久 PTY 与 editor 行为、worker thread 代码与 workflow 执行、基于 ripgrep 的搜索、外部 stdio MCP 发现与执行、模型可见及持久化快照、Zstandard 持久化、直接 JSON-RPC 与关闭。Restart 快照针对同一持久化根目录启动两个完整 SDK 运行时进程,并固定其彼此隔离的模型历史、高层结果与独立持久日志。安装后运行取代 wheel 构建前的源码 SDK 运行,因此可执行文件与 wheel 包共同接受一次验证,而不是维护两套行为清单。 Linux 另外保留 manylinux 2.28 干净安装冒烟测试与 GLIBC 检查。macOS 保留部署目标与原生 helper 检查。这些平台约束补充共同黑盒行为,不能替代它。 @@ -34,7 +34,7 @@ Fork 与 Dependabot 拉取请求永远不会获得仓库密钥。它们的原生 ## Existing decisions and supersession -本决策取代已归档的[必需 Python 运行时拉取请求验证](../../archived/testing/2026-08-12-required-python-runtime-pull-request-ci.md)中的单目标拓扑,同时保留真实可执行文件、快照、wheel 包与干净安装必须在合并前相遇的要求。[单文件 Python SDK 运行时 distribution](../architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md)仍负责 SEA 打包、封闭依赖集合、原生 sidecar、wheel 包标签与发布产物。 +本决策取代已归档的[必需 Python 运行时拉取请求验证](../../archived/testing/2026-08-12-required-python-runtime-pull-request-ci.md)中的单目标拓扑,同时保留真实可执行文件、快照、wheel 包与干净安装必须在合并前相遇的要求。[Python SDK dsh profile 运行时](../architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md)负责启动应用与自定义接口;[单文件 Python SDK 运行时 distribution](../architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md)继续负责 SEA 打包、原生 sidecar、wheel 包标签与发布产物。 ## Alternatives considered diff --git a/.github/workflows/build-exe-for-python-sdk.yml b/.github/workflows/build-exe-for-python-sdk.yml index b9ba5e148e..04501d5deb 100644 --- a/.github/workflows/build-exe-for-python-sdk.yml +++ b/.github/workflows/build-exe-for-python-sdk.yml @@ -235,7 +235,7 @@ jobs: run: | set -euo pipefail platform="${TARGET#node24-}" - exe="$PWD/dist-exe/dsh-jsonrpc-agent-pkg-$platform" + exe="$PWD/dist-exe/deepseek-harness-sdk-runtime-$platform" [ -x "$exe" ] || { echo "::error::$exe missing or not executable"; exit 1; } case "$platform" in linux-x64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_x86_64.whl ;; diff --git a/.gitignore b/.gitignore index 70c355e391..d33bbc8a4e 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,7 @@ tmp/ .idea mise.toml dist-exe/ -python/sdk-runtime/src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-* +python/sdk-runtime/src/deepseek_harness_runtime/runtime/deepseek-harness-sdk-runtime-* python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/ python/**/__pycache__/ python/**/.pytest_cache/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cf5a093f69..faa9402c42 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -38,7 +38,7 @@ sdk-wheel: - pnpm install --frozen-lockfile - pnpm run verify-runtime-closure - pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets="$PKG_TARGET" - - EXE="$PWD/dist-exe/dsh-jsonrpc-agent-pkg-$PLATFORM" + - EXE="$PWD/dist-exe/deepseek-harness-sdk-runtime-$PLATFORM" - test -x "$EXE" - uv run --python 3.10 --group test --project python/sdk python scripts/smoke-python-runtime.py --scenario all --exe "$EXE" - python scripts/build-python-release.py --package runtime --tag "$CI_COMMIT_TAG" --platform "$PLATFORM" --runtime-exe "$EXE" --output-dir "release/$PLATFORM" diff --git a/AGENTS.md b/AGENTS.md index 3fa1ce77e4..fa9515604b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ DeepSeek Harness is an all-plugin agent harness on vendored Cordis. Read [docs/a ## Application launch -Node apps launch only through `dsh` profiles; application-package bins, demos, and SDK argv escape hatches are forbidden. The private Python runtime is the sole temporary exception. [Architecture](docs/architecture.md#application-launch) owns scope and deferred artifact rename; `pnpm run verify-application-entrypoints` enforces it. +Supported Node applications launch only through `dsh` profiles; application-package bins, demos, and public SDK argv escape hatches are forbidden. [Architecture](docs/architecture.md#application-launch) owns the launch set; `pnpm run verify-application-entrypoints` enforces it. ## Repository layout @@ -46,7 +46,7 @@ packages/ @deepseek-ai/dsh- workspaces at packages/// acp/ automation-only Agent Client Protocol server interaction/ approval/interaction capabilities, permission, commands, ask-user boot/ shared profile/application boot glue - sdk/ JSON-RPC protocol, server, TypeScript client, and private Python carrier + sdk/ JSON-RPC protocol, server, and TypeScript client examples/ reusable demo bundles (agent-spine) experimental/ private prototypes excluded from official releases support/ dev/test infrastructure diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index f2637fe699..2b2a3019c8 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -88,6 +88,7 @@ External packages that a workspace package resolves at runtime. The tier covers | [`react`](https://github.com/facebook/react) | MIT | | [`react-dom`](https://github.com/facebook/react) | MIT | | [`readable-stream`](https://github.com/nodejs/readable-stream) | MIT | +| [`resolve.exports`](https://github.com/lukeed/resolve.exports) | MIT | | [`sharp`](https://github.com/lovell/sharp) | Apache-2.0 | | [`shiki`](https://github.com/shikijs/shiki) | MIT | | [`supports-color`](https://github.com/chalk/supports-color) | MIT | diff --git a/apps/cli/README.i18n.yaml b/apps/cli/README.i18n.yaml index 977871533e..f73476f371 100644 --- a/apps/cli/README.i18n.yaml +++ b/apps/cli/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write apps/cli/README.md -README.md: d59b8264093dafb0160893c942371a4daa942c14 -README.zh.md: 6a209f5ad64b38c138ae1712a05ed9e840d9a74f +README.md: ff0efb03d8a5d6da747f1b5bc0d05361a6c8a567 +README.zh.md: 8e9c122afa87525c99e8045f220b96b4511c04fe diff --git a/apps/cli/README.md b/apps/cli/README.md index d59b826409..ff0efb03d8 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -The `dsh` command is the sole supported Node application launcher: profiles are ordered stacks of plugin-bundle patch layers under the user's own overrides. SDK and ACP are profiles, not separate public bins. [`src/args.ts`](src/args.ts) owns the command grammar, and [`src/bin.ts`](src/bin.ts) loads only the selected runner. Invalid commands, options from another mode, configuration errors, and boot failures exit nonzero. +The `dsh` command is the sole supported Node application launcher: profiles are ordered stacks of plugin-bundle patch layers under the user's own overrides. SDK and ACP are profiles, not separate public bins. The Python runtime wheel packages this same command; the SDK defaults to `sdk`, and the minimal example selects `sdk-minimal`. [`src/args.ts`](src/args.ts) owns the command grammar, and [`src/bin.ts`](src/bin.ts) loads only the selected runner. Invalid commands, options from another mode, configuration errors, and boot failures exit nonzero. ## Entry modes @@ -12,10 +12,11 @@ The `dsh` command is the sole supported Node application launcher: profiles are | `dsh --profile acp` | Serve automation clients over ACP stdio until disconnect. | | `dsh --profile headless "job"` | Run one fresh persisted session, print the final answer, and exit. | | `dsh --profile sdk` | Serve SDK clients over JSON-RPC stdio until shutdown or disconnect. | +| `dsh --profile sdk-minimal` | Serve SDK clients with the standalone minimal agent tree. | | `dsh web` | Alias of `--profile web`. | | `dsh plugin --profile ` | Manage a profile's plugins by forwarding to pnpm in the profile directory. | -The invoking directory is the default workspace root. The `web`, `headless`, `sdk`, and `acp` profiles auto-initialize on first use from shipped templates; any other profile must be created through `dsh plugin`. +The invoking directory is the default workspace root. The `web`, `headless`, `sdk`, `sdk-minimal`, and `acp` profiles auto-initialize on first use from shipped templates; any other profile must be created through `dsh plugin`. ## App arguments @@ -38,7 +39,7 @@ The tree composes over an empty root: - then the profile's `cordis.patch.yml`, then the home-level `$DSH_HOME/cordis.patch.yml` - then `--patch` overlays -Bundles named in `dsh.profile.bundles` resolve from the dsh installation first (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`, `@deepseek-ai/dsh-sdk-app`, `@deepseek-ai/dsh-acp-app`), then from the profile's own `node_modules`, where pnpm installs out-of-tree plugins. +Bundles named in `dsh.profile.bundles` resolve from the dsh installation first (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`, `@deepseek-ai/dsh-sdk-app`, `@deepseek-ai/dsh-sdk-minimal`, `@deepseek-ai/dsh-acp-app`), then from the profile's own `node_modules`, where pnpm installs out-of-tree plugins. Use `--dump-default-config` and `--dump-config` to inspect the composed tree without booting it. diff --git a/apps/cli/README.zh.md b/apps/cli/README.zh.md index 6a209f5ad6..8e9c122afa 100644 --- a/apps/cli/README.zh.md +++ b/apps/cli/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -`dsh` 是唯一受支持的 Node 应用启动器;profile 由多个插件组合包 patch 层按顺序叠加而成,其上再应用用户自己的覆盖配置。SDK 与 ACP 都是 profile,而不是独立的公开 bin。[`src/args.ts`](src/args.ts) 负责命令语法,[`src/bin.ts`](src/bin.ts) 只加载选中的运行器。无效命令、来自其他模式的选项、配置错误和启动失败都会以非零状态退出。 +`dsh` 是唯一受支持的 Node 应用启动器;profile 由多个插件组合包 patch 层按顺序叠加而成,其上再应用用户自己的覆盖配置。SDK 与 ACP 都是 profile,而不是独立的公开 bin。Python 运行时 wheel 会打包同一个命令;SDK 默认使用 `sdk`,极简示例选择 `sdk-minimal`。[`src/args.ts`](src/args.ts) 负责命令语法,[`src/bin.ts`](src/bin.ts) 只加载选中的运行器。无效命令、来自其他模式的选项、配置错误和启动失败都会以非零状态退出。 ## 入口模式 @@ -12,10 +12,11 @@ | `dsh --profile acp` | 通过 ACP stdio 为自动化 client 提供服务,直至断开连接。 | | `dsh --profile headless "job"` | 运行一个全新的持久化会话,打印最终答案并退出。 | | `dsh --profile sdk` | 通过 JSON-RPC stdio 为 SDK client 提供服务,直至关闭或断开连接。 | +| `dsh --profile sdk-minimal` | 以独立极简 agent 配置树为 SDK client 提供服务。 | | `dsh web` | `--profile web` 的别名。 | | `dsh plugin --profile ` | 通过在 profile 目录中转发给 pnpm 来管理该 profile 的插件。 | -运行命令时所在的目录将作为默认 workspace 根目录。`web`、`headless`、`sdk` 和 `acp` profile 在首次使用时会从随附模板自动初始化;其他任何 profile 都必须通过 `dsh plugin` 创建。 +运行命令时所在的目录将作为默认 workspace 根目录。`web`、`headless`、`sdk`、`sdk-minimal` 和 `acp` profile 在首次使用时会从随附模板自动初始化;其他任何 profile 都必须通过 `dsh plugin` 创建。 ## 应用参数 @@ -40,7 +41,7 @@ profile 目录包含一个 `package.json`,其中记录树外插件依赖,以 - profile 自身的 `cordis.patch.yml`,然后是 home 级的 `$DSH_HOME/cordis.patch.yml` - `--patch` 指定的覆盖层 -`dsh.profile.bundles` 中列出的组合包先从 dsh 安装目录解析(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`、`@deepseek-ai/dsh-sdk-app`、`@deepseek-ai/dsh-acp-app`),再从 profile 自身的 `node_modules` 解析;pnpm 会将树外插件安装到该目录。 +`dsh.profile.bundles` 中列出的组合包先从 dsh 安装目录解析(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`、`@deepseek-ai/dsh-sdk-app`、`@deepseek-ai/dsh-sdk-minimal`、`@deepseek-ai/dsh-acp-app`),再从 profile 自身的 `node_modules` 解析;pnpm 会将树外插件安装到该目录。 使用 `--dump-default-config` 和 `--dump-config` 可在不启动的情况下检查组合后的配置树。 diff --git a/apps/cli/composition.md b/apps/cli/composition.md index 9e119a6100..9ec9a9a22d 100644 --- a/apps/cli/composition.md +++ b/apps/cli/composition.md @@ -3,7 +3,7 @@ # DSH Base Composition -The dsh-base bundle patch every profile applies first; mode bundles (dsh-web-app, dsh-headless) and the user's profile layer patch over it. +The dsh-base bundle patch shared by the web, headless, sdk, and acp profiles; their mode bundles and user layers patch over it, while sdk-minimal owns a separate standalone tree. ```mermaid flowchart LR diff --git a/apps/cli/package.json b/apps/cli/package.json index 49f7fc84d1..72e81a8208 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -58,6 +58,7 @@ "@deepseek-ai/dsh-session-projection": "workspace:^", "@deepseek-ai/dsh-session-reference": "workspace:^", "@deepseek-ai/dsh-sdk-app": "workspace:^", + "@deepseek-ai/dsh-sdk-minimal": "workspace:^", "@deepseek-ai/dsh-time-context": "workspace:^", "@deepseek-ai/dsh-skill": "workspace:^", "@deepseek-ai/dsh-skill-filesystem": "workspace:^", diff --git a/apps/cli/reference/README.i18n.yaml b/apps/cli/reference/README.i18n.yaml index 5ed7b32789..96e7b4c7b0 100644 --- a/apps/cli/reference/README.i18n.yaml +++ b/apps/cli/reference/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write apps/cli/reference/README.md -README.md: 0f407afa3b06d144681550d5096bf96c498e6451 -README.zh.md: bf4dc4ca9f49c1801d108234411123de459c0444 +README.md: 33de399dc4b2b8e67ed24ed046fcc0da2ff0a7ac +README.zh.md: 0f20245f318e31159780d29cca955fc85a5b5481 diff --git a/apps/cli/reference/README.md b/apps/cli/reference/README.md index 0f407afa3b..33de399dc4 100644 --- a/apps/cli/reference/README.md +++ b/apps/cli/reference/README.md @@ -8,9 +8,9 @@ This reference defines the profile, web-alias, plugin-management, and config-dum `dsh --profile ` boots the profile at `$DSH_HOME/profiles/`. The effective tree is composed over an empty root by applying, in order: each bundle patch named in the profile manifest's `dsh.profile.bundles` list, the profile's own `cordis.patch.yml`, the home-level `$DSH_HOME/cordis.patch.yml` (machine-local preferences shared by every profile, so it outranks the per-profile layer), and each `--patch ` overlay in argv order. Later layers win per row; a patch replaces the targeted row's complete `config` value rather than deep-merging keys, and may insert new rows. `dsh.profile.patchReload` selects `live` patch-file watching or `startup` one-time loading; omission defaults a custom profile to `live`. A parse, schema, resolution, or plugin boot failure is reported and exits nonzero. SIGINT and SIGTERM dispose the mounted root before exit. -Bundle names resolve from the dsh installation first, then from the profile directory. In-box bundles (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`, `@deepseek-ai/dsh-sdk-app`, `@deepseek-ai/dsh-acp-app`) therefore always come from the same installation as the running `dsh`; out-of-tree bundles come from the profile's pnpm-managed `node_modules`. A bare plugin `name` in any patch row resolves through the profile directory's Node parent-walk, which reaches the maintained installation fallback `$DSH_HOME/profiles/node_modules` (one symlink per package the installation's app and bundles depend on, healed on every launch). +Bundle names resolve from the dsh installation first, then from the profile directory. In-box bundles (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`, `@deepseek-ai/dsh-sdk-app`, `@deepseek-ai/dsh-sdk-minimal`, `@deepseek-ai/dsh-acp-app`) therefore always come from the same installation as the running `dsh`; out-of-tree bundles come from the profile's pnpm-managed `node_modules`. A bare plugin `name` in any patch row resolves through the profile directory's Node parent walk, which reaches the maintained installation fallback `$DSH_HOME/profiles/node_modules`. Plain Node installations place one healed symlink there per dependency-closure package. A pkg executable instead places a real ESM proxy that mirrors explicit exports and re-exports the virtual package URL, because operating-system symlinks cannot enter pkg's `/snapshot` filesystem. -The `web`, `headless`, `sdk`, and `acp` profiles auto-initialize from shipped templates on first use (`web`: base + web-app with live patches; `headless`: base + headless with startup-only patches; `sdk`: base + sdk-app with startup-only patches; `acp`: base + acp-app with startup-only patches). Any other missing profile fails loud with a hint to run `dsh plugin --profile add `. +The `web`, `headless`, `sdk`, `sdk-minimal`, and `acp` profiles auto-initialize from shipped templates on first use (`web`: base + web-app with live patches; `headless`: base + headless with startup-only patches; `sdk`: base + sdk-app with startup-only patches; `sdk-minimal`: its standalone bundle with startup-only patches; `acp`: base + acp-app with startup-only patches). Any other missing profile fails loud with a hint to run `dsh plugin --profile add `. ### App arguments @@ -27,6 +27,7 @@ The shipped apps own these command lines: | `web` | `--host`, `--port`, repeatable `--trusted-host`, `--no-open` | | `headless` | the task text, as the positional argument | | `sdk` | no options; stdio carries the JSON-RPC protocol | +| `sdk-minimal` | no options; stdio carries the same JSON-RPC protocol | | `acp` | no options; stdio carries Agent Client Protocol | A one-shot task (`dsh --profile headless "run the tests"`) creates one fresh persisted Agent through the core registry, submits the task, waits for quiescence, and flushes the Session before deriving the last non-empty assistant text and final `turn/end` reason from its durable interval. It prints the text on stdout and exits 0 for `completed`, else 1. An invocation with no task is a usage error from that app. The shipped headless profile mounts no ApiProxy, Host, HTTP server, Web runtime, or browser client; a successful run writes nothing to stderr and opens no listening port. @@ -80,9 +81,9 @@ The production Web runner needs built package and frontend artifacts (`pnpm run Process shutdown gives the plugin tree up to five seconds to dispose. The first `SIGINT`/`SIGTERM` starts that graceful drain — `SIGTERM` is a supervisor's ordinary stop request and exits 0 on every surface, `SIGINT` reports 130; a second signal forces immediate exit. If one-shot normal completion is already stuck in disposal, the first `Ctrl+C` is the escalation and exits immediately instead of being swallowed. -All modes treat the invoking directory as the default workspace root, load applicable `AGENTS.md` or `CLAUDE.md` instructions with a 65,536-byte render budget, and use an in-memory SQLite session content index. A `patchReload: live` profile watches valid edits of both `cordis.patch.yml` layers (profile and home) and reapplies them transactionally; a `startup` profile applies them once. A one-shot surface exits through its bounded shutdown, which disposes any live watchers. +The base-backed modes treat the invoking directory as the default workspace root, load applicable `AGENTS.md` or `CLAUDE.md` instructions with a 65,536-byte render budget, and use an in-memory SQLite session content index. The standalone `sdk-minimal` profile uses the invoking directory as its local filesystem and sandbox-policy root but intentionally omits instruction discovery and SQLite. A `patchReload: live` profile watches valid edits of both `cordis.patch.yml` layers (profile and home) and reapplies them transactionally; a `startup` profile applies them once. A one-shot surface exits through its bounded shutdown, which disposes any live watchers. -New sessions default to the `workspace-write` permission preset. Bash and filesystem mutations are restricted to the session workspace and platform temporary roots; reads and network access are not confined, while process visibility depends on the selected sandbox backend — bwrap runs commands in a private PID namespace that hides host processes, and Landlock and Seatbelt leave host process visibility unchanged. `DSH_PERMISSION_MODE` changes the process fallback. Stored General-settings permissions affect later Web sessions, not an already-open one. +New sessions in base-backed profiles default to the `workspace-write` permission preset. Bash and filesystem mutations are restricted to the session workspace and platform temporary roots; reads and network access are not confined, while process visibility depends on the selected sandbox backend — bwrap runs commands in a private PID namespace that hides host processes, and Landlock and Seatbelt leave host process visibility unchanged. `DSH_PERMISSION_MODE` changes the process fallback. Stored General-settings permissions affect later Web sessions, not an already-open one. The standalone `sdk-minimal` tree instead pins `danger-full-access` and mounts no approval or permission-settings service. `DSH_TOOLS_MODE` selects `native`, `code`, or `both` for the process; another value fails at boot. The shipped `minimal` agent preset keeps that deployment presentation, fixes the complete system prompt to `You are a helpful software engineer assistant.`, and composes only persistent `bash` plus `str_replace_editor`. Select 极简模式 when creating a Web session; every other prompt section and model-facing plugin remains absent from that agent while the shared browser, workspace, persistence, sandbox, and permission host stays in place. diff --git a/apps/cli/reference/README.zh.md b/apps/cli/reference/README.zh.md index bf4dc4ca9f..0f20245f31 100644 --- a/apps/cli/reference/README.zh.md +++ b/apps/cli/reference/README.zh.md @@ -8,9 +8,9 @@ `dsh --profile ` 启动位于 `$DSH_HOME/profiles/` 的 profile。生效配置树以空根节点为起点,依次叠加 profile manifest(元数据清单)的 `dsh.profile.bundles` 列表中指定的各组合包 patch、profile 自身的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml`(这是各 profile 共享的机器本地偏好,因此优先于逐 profile 配置层),以及按 argv 顺序指定的各个 `--patch ` 覆盖层。对同一配置行,后应用的层优先。patch 会替换目标行的整个 `config` 值,而不是深度合并其中的键;patch 也可以插入新行。`dsh.profile.patchReload` 可选择 `live` patch 文件监视或 `startup` 单次加载;自定义 profile 省略该值时默认使用 `live`。配置解析、schema 校验、模块解析或插件启动失败时,系统会报告错误并以非零状态退出。收到 SIGINT 或 SIGTERM 时,挂载的根节点会先 dispose(资源释放)再退出。 -组合包名称先从 dsh 安装目录解析,再从 profile 目录解析。因此,内置组合包(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`、`@deepseek-ai/dsh-sdk-app`、`@deepseek-ai/dsh-acp-app`)始终来自当前运行的 `dsh` 所属的安装;树外组合包则来自 profile 中由 pnpm 管理的 `node_modules`。patch 行中的裸插件 `name` 会从 profile 目录开始,按照 Node 的模块解析规则逐级向父目录查找,直至由 dsh 维护的安装后备目录 `$DSH_HOME/profiles/node_modules`。该目录为 dsh 安装中的应用和组合包所依赖的每个包各维护一个符号链接,并在每次启动时修复这些链接。 +组合包名称先从 dsh 安装目录解析,再从 profile 目录解析。因此,内置组合包(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`、`@deepseek-ai/dsh-sdk-app`、`@deepseek-ai/dsh-sdk-minimal`、`@deepseek-ai/dsh-acp-app`)始终来自当前运行的 `dsh` 所属的安装;树外组合包则来自 profile 中由 pnpm 管理的 `node_modules`。patch 行中的裸插件 `name` 会从 profile 目录开始,按照 Node 的模块解析规则逐级向父目录查找,直至由 dsh 维护的安装后备目录 `$DSH_HOME/profiles/node_modules`。普通 Node 安装会为依赖闭包中的每个包放置并修复一个符号链接。pkg 可执行程序则放置真实 ESM 代理,镜像显式 exports 并重新导出虚拟包 URL,因为操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统。 -`web`、`headless`、`sdk` 和 `acp` profile 首次使用时会从随附模板自动初始化(`web`:base + web-app,实时应用 patch;`headless`:base + headless,只在启动时应用 patch;`sdk`:base + sdk-app,只在启动时应用 patch;`acp`:base + acp-app,只在启动时应用 patch)。其他缺失的 profile 会显式报错,并提示运行 `dsh plugin --profile add `。 +`web`、`headless`、`sdk`、`sdk-minimal` 和 `acp` profile 首次使用时会从随附模板自动初始化(`web`:base + web-app,实时应用 patch;`headless`:base + headless,只在启动时应用 patch;`sdk`:base + sdk-app,只在启动时应用 patch;`sdk-minimal`:独立组合包,只在启动时应用 patch;`acp`:base + acp-app,只在启动时应用 patch)。其他缺失的 profile 会显式报错,并提示运行 `dsh plugin --profile add `。 ### 应用参数 @@ -27,6 +27,7 @@ | `web` | `--host`、`--port`、可重复的 `--trusted-host`、`--no-open` | | `headless` | 任务文本,作为位置参数 | | `sdk` | 无选项;stdio 携带 JSON-RPC 协议 | +| `sdk-minimal` | 无选项;stdio 携带相同的 JSON-RPC 协议 | | `acp` | 无选项;stdio 携带 Agent Client Protocol | 一次性任务(`dsh --profile headless "run the tests"`)通过核心注册表创建一个全新的持久化 Agent(智能体),提交任务、等待完全停稳并对会话执行 flush,再从其持久化事件区间中推导最后一个非空 assistant 文本与最终 `turn/end` 原因。它在 stdout 打印文本,并在原因为 `completed` 时以 0 退出,否则以 1 退出。没有任务的调用是该应用的用法错误。随附 headless profile 不挂载 ApiProxy、Host、HTTP 服务器、Web 运行时或浏览器客户端;成功运行不会向 stderr 写入任何内容,也不会打开监听端口。 @@ -80,9 +81,9 @@ dsh web --help 进程关闭时,插件树最多有 5 秒完成 dispose。首次收到 `SIGINT` 或 `SIGTERM` 时会开始优雅排空:`SIGTERM` 是监督进程发出的常规停止请求,在所有运行模式下都以 0 退出;`SIGINT` 则报告 130。第二次收到信号时会立即强制退出。如果一次性运行在正常结束时已经卡在 dispose 阶段,第一次按下 `Ctrl+C` 就会直接升级为强制退出,而不会被忽略。 -所有模式都将运行命令时所在的目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md` 或 `CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。`patchReload: live` profile 会监视 profile 与 home 两个 `cordis.patch.yml` 配置层的有效变更,并以事务方式重新应用;`startup` profile 则只应用一次。一次性运行模式通过有界关闭流程退出,该流程会 dispose(资源释放)所有实时监视器。 +基于 base 的模式都将运行命令时所在的目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md` 或 `CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。独立的 `sdk-minimal` profile 把运行命令时所在的目录作为本地文件系统与沙箱策略根目录,但刻意省略指令发现与 SQLite。`patchReload: live` profile 会监视 profile 与 home 两个 `cordis.patch.yml` 配置层的有效变更,并以事务方式重新应用;`startup` profile 则只应用一次。一次性运行模式通过有界关闭流程退出,该流程会 dispose(资源释放)所有实时监视器。 -新会话默认使用 `workspace-write` 权限预设。Bash 和文件系统修改仅限于会话 workspace 与平台临时根目录;读取和网络访问不受限制,进程可见性则取决于所选沙箱后端——bwrap 在私有 PID 命名空间中运行命令并隐藏宿主进程,Landlock 与 Seatbelt 保持宿主进程可见性不变。`DSH_PERMISSION_MODE` 更改进程后备值。General settings 中存储的权限影响后续 Web 会话,不改变已打开的会话。 +基于 base 的 profile 中,新会话默认使用 `workspace-write` 权限预设。Bash 和文件系统修改仅限于会话 workspace 与平台临时根目录;读取和网络访问不受限制,进程可见性则取决于所选沙箱后端——bwrap 在私有 PID 命名空间中运行命令并隐藏宿主进程,Landlock 与 Seatbelt 保持宿主进程可见性不变。`DSH_PERMISSION_MODE` 更改进程后备值。General settings 中存储的权限影响后续 Web 会话,不改变已打开的会话。独立的 `sdk-minimal` 配置树则固定为 `danger-full-access`,且不挂载 approval 或权限 settings 服务。 `DSH_TOOLS_MODE` 为进程选择 `native`、`code` 或 `both`;其他值会导致启动失败。随附的 `minimal` agent preset 会保留该部署的呈现方式,将完整系统提示词固定为 `You are a helpful software engineer assistant.`,并且仅组合持久 `bash` 和 `str_replace_editor`。创建 Web 会话时请选择极简模式;该 agent 不包含任何其他提示词段落或面向模型的插件,而共享的浏览器、workspace、持久化、沙箱与权限宿主保持不变。 diff --git a/apps/cli/src/bin.ts b/apps/cli/src/bin.ts index 321849f2d9..9e7c600c8d 100644 --- a/apps/cli/src/bin.ts +++ b/apps/cli/src/bin.ts @@ -41,7 +41,7 @@ switch (invocation.mode) { } case 'dump-config': { const { runDumpConfig } = await import('./dump-config.ts') - runDumpConfig(invocation.profile, invocation.defaultOnly, invocation.patches) + await runDumpConfig(invocation.profile, invocation.defaultOnly, invocation.patches) break } default: diff --git a/apps/cli/src/dump-config.ts b/apps/cli/src/dump-config.ts index 1754eb4efd..dc2089e7c8 100644 --- a/apps/cli/src/dump-config.ts +++ b/apps/cli/src/dump-config.ts @@ -26,9 +26,10 @@ const NAME = 'dsh' * (the recovery diagnostic for a broken `cordis.patch.yml`, which is then * never parsed). * @param patches - `--patch` overlay paths, in argv order. + * @returns settlement after the profile is healed and the dump is written. */ -export function runDumpConfig(profile: string, defaultOnly: boolean, patches: readonly string[]): void { - const loaded = prepareProfile(profile, !defaultOnly) +export async function runDumpConfig(profile: string, defaultOnly: boolean, patches: readonly string[]): Promise { + const loaded = await prepareProfile(profile, !defaultOnly) const layers: ConfigDumpLayer[] = loaded.layers.map(layer => ({ label: layer.packageName, patches: layer.patches, diff --git a/apps/cli/src/profile-boot.ts b/apps/cli/src/profile-boot.ts index 68c66a8e34..30058a380f 100644 --- a/apps/cli/src/profile-boot.ts +++ b/apps/cli/src/profile-boot.ts @@ -115,8 +115,8 @@ export function resolveTelemetryPatch(disabledEnv: string | undefined, hasRow: b * @param userLayer - `false` skips parsing `cordis.patch.yml` (the default dump). * @returns the loaded profile. */ -export function prepareProfile(name: string, userLayer = true): Profile { - healProfilesModuleFallback(INSTALL_ANCHOR) +export async function prepareProfile(name: string, userLayer = true): Promise { + await healProfilesModuleFallback(INSTALL_ANCHOR) const profile = loadProfile(NAME, name, INSTALL_ANCHOR, undefined, { userLayer }) writeFileSync(join(profile.dir, PROFILE_ROOT_FILENAME), PROFILE_ROOT_CONFIG) return profile @@ -145,8 +145,8 @@ function allPatches(composed: ComposedProfile): PatchOptions[] { /** * Load `name` and compose its effective patch stack: bundle layers in - * `dsh.profile.bundles` order (the base bundle gates the shell stacks by - * platform on its own rows), the profile's user layer, the home-level user + * `dsh.profile.bundles` order (a base-backed profile gets the base bundle's + * platform-gated shell rows), the profile's user layer, the home-level user * layer (`$DSH_HOME/cordis.patch.yml` — machine-local preferences that apply * to every profile, so it outranks the per-profile layer), `--patch` overlays, * then the telemetry switch. @@ -154,11 +154,11 @@ function allPatches(composed: ComposedProfile): PatchOptions[] { * @param patchFiles - `--patch` overlay paths, in argv order. * @returns the profile and its patch layers. */ -function composeProfile( +async function composeProfile( name: string, patchFiles: readonly string[], -): ComposedProfile { - const profile = prepareProfile(name) +): Promise { + const profile = await prepareProfile(name) const homePatches = loadOptionalPatches(NAME, homePatchPath()) ?? [] const overlays = patchFiles.flatMap(file => loadOverlayPatches(NAME, resolve(file))) const bundlePatches = profile.layers.flatMap(layer => layer.patches) @@ -207,7 +207,7 @@ function suppressShutdownError(ctx: Context, signal: AbortSignal, error: unknown * @returns the settled root context and the shutdown controller. */ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Context; shutdown: ProcessShutdown }> { - const composed = composeProfile(options.profile, options.patchFiles) + const composed = await composeProfile(options.profile, options.patchFiles) const app: { current?: Context } = {} const appReady = createAppReady() const shutdown = createProcessShutdown(async () => { await app.current?.fiber.dispose() }) diff --git a/apps/cli/tests/built-bin.e2e.ts b/apps/cli/tests/built-bin.e2e.ts index 2fffff6700..397e960075 100644 --- a/apps/cli/tests/built-bin.e2e.ts +++ b/apps/cli/tests/built-bin.e2e.ts @@ -12,7 +12,9 @@ import { type SessionNotification, } from '@agentclientprotocol/sdk' import { startMockLlmServer } from '@deepseek-ai/dsh-llm-mock-server' +import { entryListSchema } from '@deepseek-ai/cordis-plugin-include' import { execa } from 'execa' +import * as yaml from 'js-yaml' import { afterEach, beforeEach, describe, expect, it } from 'vitest' /** Published-entry acceptance for argument errors, profile lifecycle, and boot-free config dumps. */ @@ -924,6 +926,37 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', expect(stdout).not.toMatch(/name: '@deepseek-ai\/dsh-client-/) }, 30_000) + it('prints the exact standalone sdk-minimal tree without dsh-base', async () => { + const { stdout, code, stderr } = await runBuiltBin( + ['--profile', 'sdk-minimal', '--dump-default-config'], + { DSH_HOME: home }, + ) + expect(code).toBe(0) + expect(stderr).toBe('') + const rows = yaml.load(stdout, { schema: entryListSchema }) as Array<{ id?: string; name?: string }> + expect(rows.map(row => [row.id, row.name])).toEqual([ + ['sdk-app-startup', '@deepseek-ai/dsh-sdk-app'], + ['sdk-jsonrpc-server', '@deepseek-ai/dsh-sdk-jsonrpc-server'], + ['deepseek-llm-api-extensions', '@deepseek-ai/dsh-deepseek-llm-api-extensions'], + ['session-log-deepseek', '@deepseek-ai/dsh-session-log-deepseek'], + ['plugin-package-inventory-deepseek', '@deepseek-ai/dsh-plugin-package-inventory-deepseek'], + ['llm-deepseek', '@deepseek-ai/dsh-llm-deepseek'], + ['sandbox', '@deepseek-ai/dsh-sandbox-local'], + ['sandbox-policy', '@deepseek-ai/dsh-sandbox-policy'], + ['subprocess', '@deepseek-ai/dsh-subprocess-local'], + ['pty', '@deepseek-ai/dsh-terminal'], + ['terminal-bash', '@deepseek-ai/dsh-terminal-bash'], + ['fs-local', '@deepseek-ai/dsh-fs-local'], + ['agent-spine', '@deepseek-ai/dsh-agent-spine-demo'], + ['persistent-bash', '@deepseek-ai/dsh-tool-bash-persistent'], + ['str-replace-editor', '@deepseek-ai/dsh-tool-str-replace-editor'], + ['sessions', '@deepseek-ai/dsh-session-persistence-jsonl'], + ]) + expect(stdout).toContain('# == @deepseek-ai/dsh-sdk-minimal') + expect(stdout).not.toContain('@deepseek-ai/dsh-base') + expect(stdout).not.toContain('@deepseek-ai/dsh-web-app') + }, 30_000) + it('composes the profile user layer and a --patch overlay in order', async () => { // Auto-init the web profile first, then write its user layer. const init = await runBuiltBin(['--profile', 'web', '--dump-default-config'], { DSH_HOME: home }) diff --git a/apps/cli/tests/profile-hmr.spec.ts b/apps/cli/tests/profile-hmr.spec.ts index 6fed867d92..306bfe1458 100644 --- a/apps/cli/tests/profile-hmr.spec.ts +++ b/apps/cli/tests/profile-hmr.spec.ts @@ -9,7 +9,7 @@ import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include' const REPOSITORY_ROOT = fileURLToPath(new URL('../../../', import.meta.url)) /** Load one shipped bundle patch through the same parser as profile boot. */ -function bundle(name: 'acp-app' | 'base' | 'headless' | 'sdk-app' | 'web-app'): PatchOptions[] { +function bundle(name: 'acp-app' | 'base' | 'headless' | 'sdk-app' | 'sdk-minimal' | 'web-app'): PatchOptions[] { return loadOverlayPatches('profile-hmr test', join(REPOSITORY_ROOT, 'packages', 'bundle', name, 'cordis.patch.yml')) } @@ -39,4 +39,8 @@ describe('profile module-HMR policy', () => { config: { root: ['.'] }, }) }) + + it('keeps the standalone sdk-minimal tree free of module HMR', () => { + expect(composeEntries([bundle('sdk-minimal')]).find(entry => entry.id === 'hmr')).toBeUndefined() + }) }) diff --git a/apps/cli/tests/web-agent-presets.e2e.ts b/apps/cli/tests/web-agent-presets.e2e.ts index 1dc66b2f08..6af58a3a37 100644 --- a/apps/cli/tests/web-agent-presets.e2e.ts +++ b/apps/cli/tests/web-agent-presets.e2e.ts @@ -11,6 +11,7 @@ import type { Agent } from '@deepseek-ai/dsh-agent' import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' import { settingsNamespace } from '@deepseek-ai/dsh-settings' +import { SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-tool-subagent/model-selection-settings' import { resolveSessionPreset, SETTINGS_NAMESPACE, SHIPPED_PRESET_ROOT } from '@deepseek-ai/dsh-agent-presets' import { applyChildComposition, childSessionMeta } from '@deepseek-ai/dsh-subagent' import { CallId } from '@deepseek-ai/dsh-llm' @@ -107,7 +108,7 @@ async function bootWeb( // upward walk. The flat fallback the preset boot maintains is what makes // them resolvable — the same mechanism, not a test-only shim. const home = dirname(settingsFile) - healProfilesModuleFallback(INSTALL_ANCHOR, home) + await healProfilesModuleFallback(INSTALL_ANCHOR, home) const profileDir = join(home, 'profiles', 'spec') await mkdir(profileDir, { recursive: true }) // Product Bundles are installed into the Profile, not the dsh app. Model @@ -240,6 +241,34 @@ describe('the shipped Web composition', () => { } }) + it('applies the default-off subagent model-selection preference only to new sessions', async () => { + await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, { enabled: false }) + const disabled = await ctx.agents.create({ + sessionId: SessionId('preset-model-selection-disabled'), + setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined), + }) + await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, { enabled: true }) + const enabled = await ctx.agents.create({ + sessionId: SessionId('preset-model-selection-enabled'), + setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined), + }) + try { + expect(toolNames(ctx, disabled.agent)).not.toContain('list_subagent_models') + expect(toolParameterNames(ctx, disabled.agent, 'subagent')).not.toEqual(expect.arrayContaining([ + 'model', 'provider', 'reasoning_effort', + ])) + expect(toolNames(ctx, enabled.agent)).toContain('list_subagent_models') + expect(toolParameterNames(ctx, enabled.agent, 'subagent')).toEqual(expect.arrayContaining([ + 'model', 'provider', 'reasoning_effort', + ])) + expect(toolNames(ctx, disabled.agent)).not.toContain('list_subagent_models') + } finally { + await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, { enabled: false }) + await enabled.dispose() + await disabled.dispose() + } + }) + it('composes the exact RL prompt and two tools from `minimal`', async () => { const handle = await ctx.agents.create({ sessionId: SessionId('preset-minimal'), diff --git a/apps/web/tests/live-interactions.e2e.ts b/apps/web/tests/live-interactions.e2e.ts index 85a7a57e62..0586a900c2 100644 --- a/apps/web/tests/live-interactions.e2e.ts +++ b/apps/web/tests/live-interactions.e2e.ts @@ -1,6 +1,7 @@ -// Web e2e scenarios: live-turn interactions — cancellation, error surfacing, -// transient-retry recovery, and retry exhaustion, all through the real -// composition and wire. The model adapter is dsh-llm-replay with override +// Web e2e scenarios: live-turn interactions — running-draft submission, +// cancellation, error surfacing, transient-retry recovery, and retry +// exhaustion, all through the real composition and wire. The model adapter is +// dsh-llm-replay with override // sidecars: `hang` (+ a readyFile marker) makes mid-stream cancel // deterministic by construction, `throw` entries express provider failures by // stable code, and `{ patches }` augmentation injects transient throws before @@ -29,11 +30,12 @@ import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './suppor const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/live-interactions', import.meta.url)) const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl') -// One golden pins the stable mid-turn loading state; the other four capture -// what the user is left looking at after cancel, after a non-retryable failure, -// after retry recovery, and after retry exhaustion. +// One golden pins the empty mid-turn loading state, one pins the sendable draft +// state, and the other four capture what remains after cancel, after a +// non-retryable failure, after retry recovery, and after retry exhaustion. const CANCEL_EXPECTED = join(SNAPSHOT_DIR, 'cancel.expected.md') const LOADING_EXPECTED = join(SNAPSHOT_DIR, 'loading.expected.md') +const RUNNING_DRAFT_EXPECTED = join(SNAPSHOT_DIR, 'running-draft.expected.md') const ERROR_EXPECTED = join(SNAPSHOT_DIR, 'error-auth.expected.md') const RETRY_EXPECTED = join(SNAPSHOT_DIR, 'retry.expected.md') const RETRY_EXHAUSTED_EXPECTED = join(SNAPSHOT_DIR, 'retry-exhausted.expected.md') @@ -44,6 +46,7 @@ const AUTH_PROVIDER_MESSAGE = 'Authentication Fails, Your api key: sk-preview-se // patch. Kept deliberately tool-free so the derived script is exactly one // model call. const PROMPT = 'Reply with a one-sentence description of event sourcing, then stop.' +const RUNNING_DRAFT = 'Queue this follow-up while the current turn is running.' /** turn/end reasons observed, in order. */ function turnEndReasons(events: SessionEvent[]): string[] { @@ -147,6 +150,22 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => { ).toBe(true) const loadingSnapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold!.workspaceCwd) await compareOrRefreshGolden(LOADING_EXPECTED, loadingSnapshot, MODE) + + const input = page.locator('textarea').first() + await input.fill(RUNNING_DRAFT) + const send = page.getByRole('button', { name: 'Send message', exact: true }) + await send.waitFor({ timeout: 10_000 }) + expect(await page.getByRole('button', { name: 'Stop generating', exact: true }).count()).toBe(0) + const runningDraftSnapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold!.workspaceCwd) + await compareOrRefreshGolden(RUNNING_DRAFT_EXPECTED, runningDraftSnapshot, MODE) + await send.click() + await expect.poll(() => input.inputValue(), { timeout: 10_000 }).toBe('') + const queuedRow = page.locator('[data-queue-dock]').getByRole('listitem').filter({ hasText: RUNNING_DRAFT }) + await queuedRow.waitFor({ timeout: 10_000 }) + await page.getByRole('button', { name: 'Stop generating', exact: true }).waitFor({ timeout: 10_000 }) + await queuedRow.getByRole('button', { name: 'Remove queued message' }).click() + await expect.poll(() => queuedRow.count(), { timeout: 10_000 }).toBe(0) + await page.getByRole('button', { name: 'Stop generating' }).click() await settled expect(turnEndReasons(sessionEvents).at(-1)).toBe('aborted') @@ -281,8 +300,8 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => { it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => { await assertFixtureInventory(SNAPSHOT_DIR, [ - 'session.jsonl', 'cancel.expected.md', 'loading.expected.md', 'error-auth.expected.md', 'retry.expected.md', - 'retry-exhausted.expected.md', + 'session.jsonl', 'cancel.expected.md', 'loading.expected.md', 'running-draft.expected.md', + 'error-auth.expected.md', 'retry.expected.md', 'retry-exhausted.expected.md', ]) }) }) diff --git a/apps/web/tests/scaffold.ts b/apps/web/tests/scaffold.ts index 51ae01105f..ec30bc1e0c 100644 --- a/apps/web/tests/scaffold.ts +++ b/apps/web/tests/scaffold.ts @@ -519,7 +519,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise/profiles. - healProfilesModuleFallback(INSTALL_ANCHOR, harnessHome) + await healProfilesModuleFallback(INSTALL_ANCHOR, harnessHome) const profileDir = join(harnessHome, 'profiles', 'scaffold') await mkdir(profileDir, { recursive: true }) const rootConfig = join(profileDir, 'cordis.yml') diff --git a/apps/web/tests/snapshots/live-interactions/running-draft.expected.md b/apps/web/tests/snapshots/live-interactions/running-draft.expected.md new file mode 100644 index 0000000000..d4e15c0498 --- /dev/null +++ b/apps/web/tests/snapshots/live-interactions/running-draft.expected.md @@ -0,0 +1,28 @@ +- banner: + - navigation "Session hierarchy": + - button "Reply with a one-sentence description" [disabled] + - img + - text: Standard mode + - button "Session log": + - text: Session log + - img + - tablist: + - tab "Chat" [selected] + - tab "Trajectory" +- text: Reply with a one-sentence description of event sourcing, then stop. {{clock}} +- button "Copy": + - img +- button "Context injection @deepseek-ai/dsh-system-prompt": + - img + - img + - text: Context injection @deepseek-ai/dsh-system-prompt +- paragraph: partial +- status: Deep diving... +- textbox "Message the agent": Queue this follow-up while the current turn is running. +- button "Commands": + - img +- 'button "Access mode, current: Workspace Write"': Workspace Write +- button "Select model, current DeepSeek-V4-Flash": + - text: DeepSeek-V4-Flash + - img +- button "Send message" diff --git a/apps/web/tests/snapshots/models-settings/configured.expected.md b/apps/web/tests/snapshots/models-settings/configured.expected.md index 3c3be0922c..6ac6d76796 100644 --- a/apps/web/tests/snapshots/models-settings/configured.expected.md +++ b/apps/web/tests/snapshots/models-settings/configured.expected.md @@ -19,6 +19,10 @@ - text: 关闭 - heading "模型" [level=2] - paragraph: 填入各提供方的 API 密钥即可使用其模型。 + - region "Subagent 自选模型": + - heading "Subagent 自选模型" [level=3] + - paragraph: 允许新会话为 subagent 选择提供方、模型和推理强度。运行中的会话不会改变。 + - switch "允许 subagent 自选模型" - status: 已保存 minimax-cn。 - list: - listitem: diff --git a/apps/web/tests/snapshots/models-settings/declared-edit.expected.md b/apps/web/tests/snapshots/models-settings/declared-edit.expected.md index 1d538bcfe4..2f7d4a3a30 100644 --- a/apps/web/tests/snapshots/models-settings/declared-edit.expected.md +++ b/apps/web/tests/snapshots/models-settings/declared-edit.expected.md @@ -19,6 +19,10 @@ - text: 关闭 - heading "模型" [level=2] - paragraph: 填入各提供方的 API 密钥即可使用其模型。 + - region "Subagent 自选模型": + - heading "Subagent 自选模型" [level=3] + - paragraph: 允许新会话为 subagent 选择提供方、模型和推理强度。运行中的会话不会改变。 + - switch "允许 subagent 自选模型" - list: - listitem: - text: minimax-cn diff --git a/apps/web/tests/snapshots/models-settings/declared.expected.md b/apps/web/tests/snapshots/models-settings/declared.expected.md index df48328fd3..bb129bf2ea 100644 --- a/apps/web/tests/snapshots/models-settings/declared.expected.md +++ b/apps/web/tests/snapshots/models-settings/declared.expected.md @@ -19,6 +19,10 @@ - text: 关闭 - heading "模型" [level=2] - paragraph: 填入各提供方的 API 密钥即可使用其模型。 + - region "Subagent 自选模型": + - heading "Subagent 自选模型" [level=3] + - paragraph: 允许新会话为 subagent 选择提供方、模型和推理强度。运行中的会话不会改变。 + - switch "允许 subagent 自选模型" - list: - listitem: - text: minimax-cn diff --git a/apps/web/tests/snapshots/models-settings/empty.expected.md b/apps/web/tests/snapshots/models-settings/empty.expected.md index 54bf1db3c3..dfeb637b5f 100644 --- a/apps/web/tests/snapshots/models-settings/empty.expected.md +++ b/apps/web/tests/snapshots/models-settings/empty.expected.md @@ -19,6 +19,10 @@ - text: 关闭 - heading "模型" [level=2] - paragraph: 填入各提供方的 API 密钥即可使用其模型。 + - region "Subagent 自选模型": + - heading "Subagent 自选模型" [level=3] + - paragraph: 允许新会话为 subagent 选择提供方、模型和推理强度。运行中的会话不会改变。 + - switch "允许 subagent 自选模型" - list - text: 提供方 - combobox "提供方": diff --git a/apps/web/tests/snapshots/onboarding-deepseek-config/models.expected.md b/apps/web/tests/snapshots/onboarding-deepseek-config/models.expected.md index a302932e65..020f70d095 100644 --- a/apps/web/tests/snapshots/onboarding-deepseek-config/models.expected.md +++ b/apps/web/tests/snapshots/onboarding-deepseek-config/models.expected.md @@ -19,6 +19,10 @@ - text: 关闭 - heading "模型" [level=2] - paragraph: 填入各提供方的 API 密钥即可使用其模型。 + - region "Subagent 自选模型": + - heading "Subagent 自选模型" [level=3] + - paragraph: 允许新会话为 subagent 选择提供方、模型和推理强度。运行中的会话不会改变。 + - switch "允许 subagent 自选模型" - list: - listitem: - text: DeepSeek diff --git a/apps/web/tests/snapshots/onboarding-usable-provider/dismissed.expected.md b/apps/web/tests/snapshots/onboarding-usable-provider/dismissed.expected.md index 496443b057..73c66388f3 100644 --- a/apps/web/tests/snapshots/onboarding-usable-provider/dismissed.expected.md +++ b/apps/web/tests/snapshots/onboarding-usable-provider/dismissed.expected.md @@ -19,6 +19,10 @@ - text: 关闭 - heading "模型" [level=2] - paragraph: 填入各提供方的 API 密钥即可使用其模型。 + - region "Subagent 自选模型": + - heading "Subagent 自选模型" [level=3] + - paragraph: 允许新会话为 subagent 选择提供方、模型和推理强度。运行中的会话不会改变。 + - switch "允许 subagent 自选模型" - list: - listitem: - text: DeepSeek diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index 7f2bc49362..82e57fd2f7 100644 --- a/docs/architecture.i18n.yaml +++ b/docs/architecture.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/architecture.md -architecture.md: e37f2321377242803fb89f0a2258682e6c52801c -architecture.zh.md: 69abbcdf52654ecbce6549d25ee089b937228123 +architecture.md: add615c252948adab8db7dec7059f94b8f45e52c +architecture.zh.md: 48baf14d6a29e5ef77e6e47f4d9fa9adc4e9e748 diff --git a/docs/architecture.md b/docs/architecture.md index e37f232137..add615c252 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -16,17 +16,17 @@ There is no privileged core to patch: you extend dsh by mounting a plugin beside A running `dsh` is a plugin tree composed at boot from ordered layers. -A **profile** is a named composition stored in the Harness home. It lists the bundles it stacks, holds any out-of-tree plugins it installs, and keeps the user's own `cordis.patch.yml`. `web`, `headless`, `sdk`, and `acp` ship as templates. +A **profile** is a named composition stored in the Harness home. It lists the bundles it stacks, holds any out-of-tree plugins it installs, and keeps the user's own `cordis.patch.yml`. `web`, `headless`, `sdk`, `sdk-minimal`, and `acp` ship as templates. A **bundle** is a distribution format for Cordis config rows and the code they mount, so whatever it inserts stays patchable by the layers above it. Each declares itself in its own `package.json` under a `dsh` field: `dsh.profile` lists a profile's bundles, and `dsh.bundle` points at a bundle's patch file. -[`dsh-base`](../packages/bundle/base/README.md) is the first layer of every profile: model adapters, tools, persistence, sandbox and approval policy, settings, credentials, telemetry. [`dsh-web-app`](../packages/bundle/web-app/README.md) adds the browser application, [`dsh-headless`](../packages/bundle/headless/README.md) adds a one-shot runner with no server, [`dsh-sdk-app`](../packages/bundle/sdk-app/README.md) adds the SDK JSON-RPC server, and [`dsh-acp-app`](../packages/bundle/acp-app/README.md) adds the automation-only ACP server. +[`dsh-base`](../packages/bundle/base/README.md) is the shared first layer of the `web`, `headless`, `sdk`, and `acp` profiles: model adapters, tools, persistence, sandbox and approval policy, settings, credentials, telemetry. [`dsh-web-app`](../packages/bundle/web-app/README.md) adds the browser application, [`dsh-headless`](../packages/bundle/headless/README.md) adds a one-shot runner with no server, [`dsh-sdk-app`](../packages/bundle/sdk-app/README.md) adds the SDK JSON-RPC server, and [`dsh-acp-app`](../packages/bundle/acp-app/README.md) adds the automation-only ACP server. [`dsh-sdk-minimal`](../packages/bundle/sdk-minimal/README.md) is the deliberate exception: one bundle owns its complete explicit SDK tree and does not apply `dsh-base`. Layers apply to an empty entry list in this order: each bundle in the profile's listed order, then the profile's `cordis.patch.yml`, then the home-level one, then any `--patch` overlay. A patch targets a row by id and replaces its whole config, or inserts new rows. -Custom profiles default to live patch reload. The shipped `web` profile is live; `headless`, `sdk`, and `acp` apply all layers once at startup because replacing a one-shot or stdio application's dependencies after it owns work would invalidate that lifecycle. +Custom profiles default to live patch reload. The shipped `web` profile is live; `headless`, `sdk`, `sdk-minimal`, and `acp` apply all layers once at startup because replacing a one-shot or stdio application's dependencies after it owns work would invalidate that lifecycle. To see the tree your machine actually boots: @@ -40,11 +40,11 @@ Composition mechanics are in [app-boot](../packages/boot/app-boot/README.md#prof ## Application launch -Every supported Node application starts at the `dsh` CLI with a named profile. The shipped applications are `dsh web` (the deliberate alias for `--profile web`), `dsh --profile headless`, `dsh --profile sdk`, and `dsh --profile acp`. The TypeScript SDK resolves its same-version `dsh` dependency and selects `sdk`; custom plugin composition remains a profile plus ordered patch files, not another executable or inline application tree. +Every supported Node application starts at the `dsh` CLI with a named profile. The shipped applications are `dsh web` (the deliberate alias for `--profile web`), `dsh --profile headless`, `dsh --profile sdk`, `dsh --profile sdk-minimal`, and `dsh --profile acp`. The TypeScript SDK resolves its same-version `dsh` dependency and selects `sdk`; custom plugin composition remains a profile plus ordered patch files, not another executable or inline application tree. `sdk-minimal` is a repository-owned standalone bundle behind the same launcher, not a caller-supplied Cordis tree. Vendored CLIs, build-only and test-only executables, direct in-process plugin mounting, and the private browser WebWorker preview are not Harness application launchers. [`verify-application-entrypoints`](../scripts/verify-application-entrypoints.ts) keeps every package bin, executable source, and root demo in an explicit class and rejects a Node application path that bypasses `dsh`. -The packaged Python SDK runtime is the sole temporary application exception. Its private [`dsh-sdk-python-runtime`](../packages/sdk/python-runtime/README.md) carrier and `dsh-sdk-python-runtime-closure` deploy manifest preserve the current Python API, wire, default `cordis.yml`, environment variables, wheel names, `dsh-jsonrpc-agent-pkg--` executables, sidecars, and platform set. A later Python migration will launch `dsh --profile sdk`, delete the private direct-config carrier, and then rename that executable family to `deepseek-harness-sdk-runtime--`. +The Python SDK follows the same application architecture. Its runtime wheel packages the normal `dsh` CLI as `deepseek-harness-sdk-runtime--`, and the client launches `dsh --profile sdk` with an explicit Harness home by default. The minimal example selects the shipped `sdk-minimal` profile. Python exposes profile selection and ordered patch files rather than a complete Cordis tree; persistent external plugins are installed through `dsh plugin`. The removed private direct-config carrier has no compatibility bin or fallback parser. ## Core packages diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index 69abbcdf52..48baf14d6a 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -16,17 +16,17 @@ 运行中的 `dsh` 是一棵插件树,由启动时按序叠加的各层组合而成。 -**profile** 是存放在 Harness home 中的具名组装。它列出自己叠放的组合包,存放自己安装的树外插件,并保存用户自己的 `cordis.patch.yml`。`web`、`headless`、`sdk` 和 `acp` 作为模板随发行版交付。 +**profile** 是存放在 Harness home 中的具名组装。它列出自己叠放的组合包,存放自己安装的树外插件,并保存用户自己的 `cordis.patch.yml`。`web`、`headless`、`sdk`、`sdk-minimal` 和 `acp` 作为模板随发行版交付。 **组合包**是 Cordis 配置项及其挂载代码的分发格式,因此它插入的内容始终可被其上各层 patch。 两者都在各自的 `package.json` 中通过 `dsh` 字段声明自己:`dsh.profile` 列出一个 profile 的组合包,`dsh.bundle` 指向一个组合包的 patch 文件。 -[`dsh-base`](../packages/bundle/base/README.zh.md) 是每个 profile 的第一层:模型适配器、工具、持久化、沙箱与审批策略、设置、凭据、遥测。[`dsh-web-app`](../packages/bundle/web-app/README.zh.md) 增加浏览器应用,[`dsh-headless`](../packages/bundle/headless/README.zh.md) 增加不带服务器的一次性运行器,[`dsh-sdk-app`](../packages/bundle/sdk-app/README.zh.md) 增加 SDK JSON-RPC 服务器,[`dsh-acp-app`](../packages/bundle/acp-app/README.zh.md) 增加仅用于自动化的 ACP 服务器。 +[`dsh-base`](../packages/bundle/base/README.zh.md) 是 `web`、`headless`、`sdk` 与 `acp` profile 的共享第一层:模型适配器、工具、持久化、沙箱与审批策略、设置、凭据、遥测。[`dsh-web-app`](../packages/bundle/web-app/README.zh.md) 增加浏览器应用,[`dsh-headless`](../packages/bundle/headless/README.zh.md) 增加不带服务器的一次性运行器,[`dsh-sdk-app`](../packages/bundle/sdk-app/README.zh.md) 增加 SDK JSON-RPC 服务器,[`dsh-acp-app`](../packages/bundle/acp-app/README.zh.md) 增加仅用于自动化的 ACP 服务器。[`dsh-sdk-minimal`](../packages/bundle/sdk-minimal/README.zh.md) 是刻意保留的例外:一个组合包拥有完整的显式 SDK 配置树,不应用 `dsh-base`。 各层按此顺序应用在空条目列表之上:先按 profile 列出的顺序应用每个组合包,然后是 profile 的 `cordis.patch.yml`,然后是 home 级的那份,最后是任意 `--patch` overlay。一条 patch 按 id 定位某个条目并替换其整个 config,或插入新条目。 -自定义 profile 默认实时重载 patch。随附的 `web` profile 使用实时重载;`headless`、`sdk` 和 `acp` 则只在启动时应用一次所有配置层,因为一次性应用或 stdio 应用拥有工作之后,替换其依赖会破坏该生命周期。 +自定义 profile 默认实时重载 patch。随附的 `web` profile 使用实时重载;`headless`、`sdk`、`sdk-minimal` 和 `acp` 则只在启动时应用一次所有配置层,因为一次性应用或 stdio 应用拥有工作之后,替换其依赖会破坏该生命周期。 要查看你的机器实际启动的配置树: @@ -40,11 +40,11 @@ dsh --profile web --dump-config ## 应用启动 -所有受支持的 Node 应用都从 `dsh` CLI 与具名 profile 启动。随附应用是 `dsh web`(刻意为 `--profile web` 保留的别名)、`dsh --profile headless`、`dsh --profile sdk` 与 `dsh --profile acp`。TypeScript SDK 会解析其同版本 `dsh` 依赖并选择 `sdk`;自定义插件组合继续由 profile 与有序 patch 文件表达,而不是另一个可执行文件或内联应用树。 +所有受支持的 Node 应用都从 `dsh` CLI 与具名 profile 启动。随附应用是 `dsh web`(刻意为 `--profile web` 保留的别名)、`dsh --profile headless`、`dsh --profile sdk`、`dsh --profile sdk-minimal` 与 `dsh --profile acp`。TypeScript SDK 会解析其同版本 `dsh` 依赖并选择 `sdk`;自定义插件组合继续由 profile 与有序 patch 文件表达,而不是另一个可执行文件或内联应用树。`sdk-minimal` 是位于同一 launcher 后的仓库自有独立组合包,而不是由调用方提供的 Cordis 配置树。 Vendored CLI、仅用于构建和测试的可执行文件、进程内直接挂载插件以及私有浏览器 WebWorker 预览都不属于 Harness 应用启动器。[`verify-application-entrypoints`](../scripts/verify-application-entrypoints.ts)将每个包 bin、可执行源码与根 demo 归入显式类别,并拒绝任何绕过 `dsh` 的 Node 应用路径。 -打包后的 Python SDK 运行时是唯一的临时应用例外。其私有 [`dsh-sdk-python-runtime`](../packages/sdk/python-runtime/README.zh.md) 载体与 `dsh-sdk-python-runtime-closure` 部署 manifest 保持当前 Python API、协议格式、默认 `cordis.yml`、环境变量、wheel 包名称、`dsh-jsonrpc-agent-pkg--` 可执行文件、伴随文件及平台集合不变。后续 Python 迁移会改为启动 `dsh --profile sdk`、删除私有直读配置载体,然后把该可执行文件族重命名为 `deepseek-harness-sdk-runtime--`。 +Python SDK 遵循相同的应用架构。其运行时 wheel 把普通 `dsh` CLI 打包为 `deepseek-harness-sdk-runtime--`,客户端默认以显式 Harness home 启动 `dsh --profile sdk`。极简示例选择随附的 `sdk-minimal` profile。Python 暴露 profile 选择与有序 patch 文件,而不是完整 Cordis 树;持久外部插件通过 `dsh plugin` 安装。已删除的私有直读配置载体没有兼容 bin 或回退 parser。 ## 核心包 diff --git a/docs/capability-seams.i18n.yaml b/docs/capability-seams.i18n.yaml index 406b4015a0..d7e1c70e5a 100644 --- a/docs/capability-seams.i18n.yaml +++ b/docs/capability-seams.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/capability-seams.md -capability-seams.md: 75f050f329e709e5c88bffbe0d3bc2072d4286de -capability-seams.zh.md: 25fa48c67e406b03677debba44eff5d49fd3c626 +capability-seams.md: 0994b186f7daa6afbff0f1484216e55fc79170ff +capability-seams.zh.md: 48aa0a5353bba8d56f63abfcfc4cd2c601569cb6 diff --git a/docs/capability-seams.md b/docs/capability-seams.md index 75f050f329..0994b186f7 100644 --- a/docs/capability-seams.md +++ b/docs/capability-seams.md @@ -56,6 +56,8 @@ flowchart LR pkg_settings["settings"] svc_settings["ctx.settings
User-settings seam"] pkg_settings_file["settings-file"] + pkg_tool_subagent["tool-subagent"] + svc_subagentModelSelection["ctx.subagentModelSelection
Subagent model-selection preference"] pkg_credentials["credentials"] svc_credentials["ctx.credentials
Credential seam"] pkg_credentials_local["credentials-local"] @@ -94,7 +96,6 @@ flowchart LR pkg_tool_ask_user["tool-ask-user"] pkg_tool_cordis["tool-cordis"] pkg_tool_skill["tool-skill"] - pkg_tool_subagent["tool-subagent"] pkg_tool_todo["tool-todo"] pkg_user_questions["user-questions"] svc_userQuestions["ctx.userQuestions
Human question/answer seam"] @@ -310,6 +311,7 @@ flowchart LR pkg_terminal --> svc_terminals pkg_terminal_bash --> svc_terminals pkg_token_meter --> svc_tokenMeter + pkg_tool_subagent --> svc_subagentModelSelection pkg_tools --> svc_tools pkg_typert_registry --> svc_typert pkg_user_questions --> svc_userQuestions @@ -401,6 +403,7 @@ flowchart LR svc_storage --> pkg_storage_domain svc_storageDomain --> pkg_message_feedback svc_storageDomain --> pkg_workspace + svc_subagentModelSelection --> pkg_tool_subagent svc_subagents --> pkg_tool_ralph svc_subagents --> pkg_tool_subagent svc_subagents --> pkg_tool_subagent_control @@ -458,6 +461,7 @@ flowchart LR | `ctx.typertGateway` | `core` | [`api-gateway`](../packages/api/gateway) | - | - | - | Associates generated Remote descriptors with live Cordis services, resolves registered identities, and exposes unary calls through the shared Connection RPC carrier. | | `ctx.sessionPersistence` | `seam` | [`session-persistence`](../packages/session/session-persistence) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | [`agent-loop`](../packages/core/agent-loop), [`tool-bash`](../packages/shell/tool-bash), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`message-feedback`](../packages/feedback/message-feedback) | - | Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time. | | `ctx.settings` | `seam` | [`settings`](../packages/settings/settings) | [`settings-file`](../packages/settings/settings-file) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), `apiproxy` | - | Plugins register namespace schemas and resolve layered values; providers store the raw document. The LLM adapters register their entry config as the composition base under the user section; the web gateway serves redacted layered descriptors and writes the user layer. | +| `ctx.subagentModelSelection` | `core` | [`tool-subagent`](../packages/subagent/tool-subagent) | - | [`tool-subagent`](../packages/subagent/tool-subagent) | - | Owns the default-off settings namespace that Agent-scoped delegation tools sample when composing a new top-level Session. | | `ctx.credentials` | `seam` | [`credentials`](../packages/credentials/credentials) | [`credentials-local`](../packages/credentials/credentials-local) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), `apiproxy` | - | Configuration carries references to secrets; providers own the values. Consumers resolve per operation, so a rotated credential reaches the very next request; the web gateway exposes value-free views and write-only storage. | | `ctx.authorization` | `seam` | [`authorization`](../packages/credentials/authorization) | - | [`llm-pi-ai`](../packages/llm/llm-pi-ai) | - | Flows are registered by the plugin that knows how to obtain one credential and keyed by the record they write; the seam owns the conversation and the one-attempt-per-key lifecycle, never the protocol. | | `ctx.sessionTelemetry` | `seam` | [`session-telemetry`](../packages/session/session-telemetry) | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | - | - | The seam captures, redacts, and hands session records to one backend; nothing else consumes the service — its output leaves the process. | diff --git a/docs/capability-seams.zh.md b/docs/capability-seams.zh.md index 25fa48c67e..48aa0a5353 100644 --- a/docs/capability-seams.zh.md +++ b/docs/capability-seams.zh.md @@ -58,6 +58,8 @@ flowchart LR pkg_settings["settings"] svc_settings["ctx.settings
User-settings seam"] pkg_settings_file["settings-file"] + pkg_tool_subagent["tool-subagent"] + svc_subagentModelSelection["ctx.subagentModelSelection
Subagent model-selection preference"] pkg_credentials["credentials"] svc_credentials["ctx.credentials
Credential seam"] pkg_credentials_local["credentials-local"] @@ -96,7 +98,6 @@ flowchart LR pkg_tool_ask_user["tool-ask-user"] pkg_tool_cordis["tool-cordis"] pkg_tool_skill["tool-skill"] - pkg_tool_subagent["tool-subagent"] pkg_tool_todo["tool-todo"] pkg_user_questions["user-questions"] svc_userQuestions["ctx.userQuestions
Human question/answer seam"] @@ -312,6 +313,7 @@ flowchart LR pkg_terminal --> svc_terminals pkg_terminal_bash --> svc_terminals pkg_token_meter --> svc_tokenMeter + pkg_tool_subagent --> svc_subagentModelSelection pkg_tools --> svc_tools pkg_typert_registry --> svc_typert pkg_user_questions --> svc_userQuestions @@ -403,6 +405,7 @@ flowchart LR svc_storage --> pkg_storage_domain svc_storageDomain --> pkg_message_feedback svc_storageDomain --> pkg_workspace + svc_subagentModelSelection --> pkg_tool_subagent svc_subagents --> pkg_tool_ralph svc_subagents --> pkg_tool_subagent svc_subagents --> pkg_tool_subagent_control @@ -460,6 +463,7 @@ flowchart LR | `ctx.typertGateway` | `core` | [`api-gateway`](../packages/api/gateway) | - | - | - | 将生成的 Remote 描述符与实时 Cordis 服务关联,解析已注册的身份,并通过共享的 Connection RPC 载体提供一元调用。 | | `ctx.sessionPersistence` | `seam` | [`session-persistence`](../packages/session/session-persistence) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | [`agent-loop`](../packages/core/agent-loop), [`tool-bash`](../packages/shell/tool-bash), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`message-feedback`](../packages/feedback/message-feedback) | - | 各后端持久化同一套 SessionEvent 词汇;应用在组合时选择后端。 | | `ctx.settings` | `seam` | [`settings`](../packages/settings/settings) | [`settings-file`](../packages/settings/settings-file) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), `apiproxy` | - | 插件注册命名空间 schema 并解析分层值;提供方存储原始文档。LLM(大语言模型)适配器在用户分区下将其入口配置注册为组合基础;Web 网关提供经过脱敏的分层描述符,并写入用户层。 | +| `ctx.subagentModelSelection` | `core` | [`tool-subagent`](../packages/subagent/tool-subagent) | - | [`tool-subagent`](../packages/subagent/tool-subagent) | - | 拥有默认关闭的设置命名空间;Agent 作用域的委派工具会在组合新顶层 Session 时读取它。 | | `ctx.credentials` | `seam` | [`credentials`](../packages/credentials/credentials) | [`credentials-local`](../packages/credentials/credentials-local) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), `apiproxy` | - | 配置携带对机密信息的引用;提供方拥有实际值。消费方按操作解析,因此轮换后的凭据会在紧接着的下一次请求中生效;Web 网关提供不含实际值的视图和只写存储。 | | `ctx.authorization` | `seam` | [`authorization`](../packages/credentials/authorization) | - | [`llm-pi-ai`](../packages/llm/llm-pi-ai) | - | flow 由知道如何取得某份凭据的插件注册,并以其写入的记录为键;seam 拥有这段对话与"每个键同时只跑一次尝试"的生命周期,而非协议本身。 | | `ctx.sessionTelemetry` | `seam` | [`session-telemetry`](../packages/session/session-telemetry) | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | - | - | 该 seam 捕获会话记录、进行脱敏并交给一个后端;没有其他组件消费该服务,其输出会离开当前进程。 | diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 24599c8a61..616048088b 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/config-catalog.md -config-catalog.md: fea204a6f581c35aeb24a9fcbeceafb13c70c971 -config-catalog.zh.md: 5a77f246a1c8d20e50b8eae439781c9eaa562eb7 +config-catalog.md: 9230b8de99a2e1d084a29a2fc92b3d445f59e5e7 +config-catalog.zh.md: d89fee530dec759a429ccf39f6972272955f2865 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index fea204a6f5..9230b8de99 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -166,9 +166,10 @@ Source: [`packages/preset/agent-presets/src/preset.ts:52`](../packages/preset/ag * Bundle config: each field forwarded verbatim to the child that owns it — * `agents` to the agent loop (an app that pre-creates no agents, like the ACP * bridge, simply omits it), `includeHarnessIdentity`, `includeRuntimeContext`, - * `persona`, and `toolOrder` to the system-prompt plugin (the fixed opener, - * dynamic-context policy, deployment persona, and explicit model-facing tool - * order), the `tools` object to the tool registry (its presentation `mode`), + * `persona`, `personaComplete`, and `toolOrder` to the system-prompt plugin + * (the fixed opener, dynamic-context policy, deployment persona completeness, + * and explicit model-facing tool order), the `tools` object to the tool + * registry (its presentation `mode`), * `dshHome` to bash environment and local skill discovery, `sessionTitle` to * the fallback title service, `skills` to the * skill registry/local provider/tool consumer, `workspaceContext` to the @@ -196,6 +197,8 @@ export interface Config { includeRuntimeContext?: SystemPromptConfig['includeRuntimeContext'] /** The deployment persona (see dsh-system-prompt's `Config`). */ persona?: SystemPromptConfig['persona'] + /** Whether the deployment persona is the complete system prompt. */ + personaComplete?: SystemPromptConfig['personaComplete'] /** The explicit model-facing tool order (see dsh-system-prompt's `Config`). */ toolOrder?: SystemPromptConfig['toolOrder'] /** The tool registry's config — its presentation `mode` (see dsh-tools' `Config`). */ @@ -247,7 +250,7 @@ export interface GoalConfig { Depends on: [`AgentLoopConfig`](#deepseek-aidsh-agent-loop) · [`GoalDomainConfig`](#deepseek-aidsh-goal) · [`InvariantConfig`](#deepseek-aidsh-invariants) · [`JobsConfig`](#deepseek-aidsh-jobs-local) · [`SessionTitleConfig`](#deepseek-aidsh-session-title) · [`SkillFileSystem`](../packages/skill/skill-filesystem/src/index.ts) · [`SkillRegistryConfig`](#deepseek-aidsh-skill) · [`SystemPromptConfig`](#deepseek-aidsh-system-prompt) · [`toolBash`](../packages/shell/tool-bash/src/index.ts) · [`toolGoal`](../packages/goal/tool-goal/src/index.ts) · [`toolJobs`](../packages/jobs/tool-jobs/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools) · [`toolSkill`](../packages/skill/tool-skill/src/index.ts) · [`workspaceContext`](../packages/context/agent-instructions/src/index.ts) -Source: [`packages/examples/agent-spine-demo/src/index.ts:92`](../packages/examples/agent-spine-demo/src/index.ts) +Source: [`packages/examples/agent-spine-demo/src/index.ts:93`](../packages/examples/agent-spine-demo/src/index.ts) @@ -308,16 +311,21 @@ export interface Config { maxImagePixels?: number /** Maximum intrinsic width and maximum intrinsic height accepted for one submitted image. Default: 8192px. */ maxImageDimension?: number - /** Long-edge pixel cap of the stored provider-independent normalized image. */ + /** Total-pixel budget of the stored provider-independent normalized image. */ + normalizedImageMaxPixels?: number + /** Long-edge pixel cap of the stored provider-independent normalized image, applied after the total-pixel budget. */ normalizedImageMaxDimension?: number - /** Encoded-byte safety cap of the stored provider-independent normalized image. */ + /** + * Encoded-byte target of the stored provider-independent normalized image; + * the smallest quality-ladder output is kept when no quality fits. + */ normalizedImageMaxBytes?: number /** Maximum simultaneous normalization or request-image transformations in this service instance. */ imageCompressionConcurrency?: number } ``` -Source: [`packages/attachment/attachment-local/src/index.ts:51`](../packages/attachment/attachment-local/src/index.ts) +Source: [`packages/attachment/attachment-local/src/index.ts:55`](../packages/attachment/attachment-local/src/index.ts) @@ -943,14 +951,14 @@ export interface DeepSeekCatalogModel { inputModalities?: ModelModality[] /** Total-pixel budget for one deterministic request preview, or the 512-by-512 `low` preset. */ imagePixelBudget?: number | 'low' - /** Encoded-byte cap for one deterministic request preview. */ + /** Encoded-byte target for one deterministic request preview; the smallest quality-ladder output is used when no quality fits. */ imageMaxBytes?: number } ``` Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) -Source: [`packages/llm/llm-deepseek/src/index.ts:107`](../packages/llm/llm-deepseek/src/index.ts) +Source: [`packages/llm/llm-deepseek/src/index.ts:117`](../packages/llm/llm-deepseek/src/index.ts) @@ -1054,7 +1062,10 @@ export interface PiAiProviderProfile { maxRequestImageBytes?: number /** Total-pixel budget for each deterministic inline request version. */ requestImagePixelBudget?: number - /** Raw encoded-byte cap for each deterministic inline request version. */ + /** + * Raw encoded-byte target for each deterministic inline request version; + * the smallest quality-ladder output is used when no quality fits. + */ requestImageMaxBytes?: number /** Provider-owned model-request retry policy; omission uses normal mode with five retries. */ retryPolicy?: RetryPolicyConfig @@ -1213,7 +1224,7 @@ export type PiAiThinkingFormat = NonNullable @@ -1670,6 +1681,22 @@ Depends on: [`SandboxMode`](subsystems/sandbox.md) Source: [`packages/sandbox/sandbox-policy/src/index.ts:67`](../packages/sandbox/sandbox-policy/src/index.ts) + + +## `@deepseek-ai/dsh-sdk-app` + +Requires: `cmdlineArgs` + +```ts config-catalog +/** SDK stdio startup configuration. */ +export interface Config { + /** Profile name rendered in help and diagnostics (default `sdk`). */ + profile?: string +} +``` + +Source: [`packages/bundle/sdk-app/src/index.ts:23`](../packages/bundle/sdk-app/src/index.ts) + ## `@deepseek-ai/dsh-sdk-jsonrpc-server` @@ -1681,6 +1708,13 @@ Requires: `agents` export interface JsonRpcConfig { /** Report max-token turn/subagent termination as a successful SDK result. */ maxTokensAsSuccess?: boolean + /** Per-root-agent model-facing tool filter; an allow list excludes later unnamed global tools. */ + toolFilter?: { + /** Global tool names that remain visible. */ + allow?: string[] + /** Global tool names removed from visibility. */ + deny?: string[] + } /** Transport input override; production uses `process.stdin`. */ input?: Readable /** Transport output override; production uses `process.stdout`. */ @@ -2417,6 +2451,8 @@ export interface Config { * `deployment:persona` shadows it; `{{variable}}` references are strict. */ persona?: string + /** Treat the deployment persona as the complete system prompt (default false). */ + personaComplete?: boolean /** * Model-facing tool names in order, with {@link TOOL_ORDER_REST} exactly once. * Invalid fields fail at load and unknown names fail at assembly; known names @@ -2816,6 +2852,14 @@ export interface Config { * a distinct name. */ toolName?: string + /** Let the model discover and select the child LLM route (default false). */ + enableModelSelection?: boolean + /** + * Sample the Host `subagent-model-selection` user setting for each new + * top-level session and inherit that decision in its child sessions. Mutually + * exclusive with `enableModelSelection`. + */ + modelSelectionSettings?: boolean /** * Expose `run_in_background` (default true). Disabled instances omit the * parameter and reject forced background calls. @@ -2863,7 +2907,7 @@ export interface Config { Depends on: [`AgentOptions`](subsystems/core.md) -Source: [`packages/subagent/tool-subagent/src/index.ts:29`](../packages/subagent/tool-subagent/src/index.ts) +Source: [`packages/subagent/tool-subagent/src/index.ts:48`](../packages/subagent/tool-subagent/src/index.ts) @@ -3321,7 +3365,6 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co - `@deepseek-ai/dsh-llm` ([`packages/llm/llm/src/index.ts`](../packages/llm/llm/src/index.ts)) - `@deepseek-ai/dsh-lsp` ([`packages/lsp/lsp/src/index.ts`](../packages/lsp/lsp/src/index.ts)) - `@deepseek-ai/dsh-schedule` — requires `agents` · `sessions` · `tools` · `sessionPersistence` ([`packages/schedule/schedule/src/index.ts`](../packages/schedule/schedule/src/index.ts)) -- `@deepseek-ai/dsh-sdk-app` — requires `cmdlineArgs` ([`packages/bundle/sdk-app/src/index.ts`](../packages/bundle/sdk-app/src/index.ts)) - `@deepseek-ai/dsh-session` ([`packages/core/session/src/index.ts`](../packages/core/session/src/index.ts)) - `@deepseek-ai/dsh-session-checkpoint-policy` — requires `llm` · `sessionPersistence` · `sessions` · `tools` ([`packages/session/session-checkpoint-policy/src/index.ts`](../packages/session/session-checkpoint-policy/src/index.ts)) - `@deepseek-ai/dsh-session-log-export` — requires `commands` ([`packages/session-query/session-log-export/src/index.ts`](../packages/session-query/session-log-export/src/index.ts)) @@ -3391,8 +3434,8 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them. - `@deepseek-ai/dsh-sandbox-windows-acl` ([`packages/sandbox/sandbox-windows-acl/src/index.ts`](../packages/sandbox/sandbox-windows-acl/src/index.ts)) - `@deepseek-ai/dsh-scope` ([`packages/core/scope/src/index.ts`](../packages/core/scope/src/index.ts)) - `@deepseek-ai/dsh-sdk-client` ([`packages/sdk/client/src/index.ts`](../packages/sdk/client/src/index.ts)) +- `@deepseek-ai/dsh-sdk-minimal` ([`packages/bundle/sdk-minimal/src/index.ts`](../packages/bundle/sdk-minimal/src/index.ts)) - `@deepseek-ai/dsh-sdk-protocol` ([`packages/sdk/protocol/src/index.ts`](../packages/sdk/protocol/src/index.ts)) -- `@deepseek-ai/dsh-sdk-python-runtime` ([`packages/sdk/python-runtime/src/index.ts`](../packages/sdk/python-runtime/src/index.ts)) - `@deepseek-ai/dsh-session-telemetry` ([`packages/session/session-telemetry/src/index.ts`](../packages/session/session-telemetry/src/index.ts)) - `@deepseek-ai/dsh-session-title-llm` ([`packages/session/session-title-llm/src/index.ts`](../packages/session/session-title-llm/src/index.ts)) - `@deepseek-ai/dsh-subagent-in-process-driver` ([`packages/subagent/subagent-in-process-driver/src/index.ts`](../packages/subagent/subagent-in-process-driver/src/index.ts)) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 5a77f246a1..d89fee530d 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -168,9 +168,10 @@ export type PresetTrust = 'system' | 'user' * Bundle config: each field forwarded verbatim to the child that owns it — * `agents` to the agent loop (an app that pre-creates no agents, like the ACP * bridge, simply omits it), `includeHarnessIdentity`, `includeRuntimeContext`, - * `persona`, and `toolOrder` to the system-prompt plugin (the fixed opener, - * dynamic-context policy, deployment persona, and explicit model-facing tool - * order), the `tools` object to the tool registry (its presentation `mode`), + * `persona`, `personaComplete`, and `toolOrder` to the system-prompt plugin + * (the fixed opener, dynamic-context policy, deployment persona completeness, + * and explicit model-facing tool order), the `tools` object to the tool + * registry (its presentation `mode`), * `dshHome` to bash environment and local skill discovery, `sessionTitle` to * the fallback title service, `skills` to the * skill registry/local provider/tool consumer, `workspaceContext` to the @@ -198,6 +199,8 @@ export interface Config { includeRuntimeContext?: SystemPromptConfig['includeRuntimeContext'] /** The deployment persona (see dsh-system-prompt's `Config`). */ persona?: SystemPromptConfig['persona'] + /** Whether the deployment persona is the complete system prompt. */ + personaComplete?: SystemPromptConfig['personaComplete'] /** The explicit model-facing tool order (see dsh-system-prompt's `Config`). */ toolOrder?: SystemPromptConfig['toolOrder'] /** The tool registry's config — its presentation `mode` (see dsh-tools' `Config`). */ @@ -249,7 +252,7 @@ export interface GoalConfig { 依赖:[`AgentLoopConfig`](#deepseek-aidsh-agent-loop) · [`GoalDomainConfig`](#deepseek-aidsh-goal) · [`InvariantConfig`](#deepseek-aidsh-invariants) · [`JobsConfig`](#deepseek-aidsh-jobs-local) · [`SessionTitleConfig`](#deepseek-aidsh-session-title) · [`SkillFileSystem`](../packages/skill/skill-filesystem/src/index.ts) · [`SkillRegistryConfig`](#deepseek-aidsh-skill) · [`SystemPromptConfig`](#deepseek-aidsh-system-prompt) · [`toolBash`](../packages/shell/tool-bash/src/index.ts) · [`toolGoal`](../packages/goal/tool-goal/src/index.ts) · [`toolJobs`](../packages/jobs/tool-jobs/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools) · [`toolSkill`](../packages/skill/tool-skill/src/index.ts) · [`workspaceContext`](../packages/context/agent-instructions/src/index.ts) -来源:[`packages/examples/agent-spine-demo/src/index.ts:92`](../packages/examples/agent-spine-demo/src/index.ts) +来源:[`packages/examples/agent-spine-demo/src/index.ts:93`](../packages/examples/agent-spine-demo/src/index.ts) @@ -310,16 +313,21 @@ export interface Config { maxImagePixels?: number /** Maximum intrinsic width and maximum intrinsic height accepted for one submitted image. Default: 8192px. */ maxImageDimension?: number - /** Long-edge pixel cap of the stored provider-independent normalized image. */ + /** Total-pixel budget of the stored provider-independent normalized image. */ + normalizedImageMaxPixels?: number + /** Long-edge pixel cap of the stored provider-independent normalized image, applied after the total-pixel budget. */ normalizedImageMaxDimension?: number - /** Encoded-byte safety cap of the stored provider-independent normalized image. */ + /** + * Encoded-byte target of the stored provider-independent normalized image; + * the smallest quality-ladder output is kept when no quality fits. + */ normalizedImageMaxBytes?: number /** Maximum simultaneous normalization or request-image transformations in this service instance. */ imageCompressionConcurrency?: number } ``` -来源:[`packages/attachment/attachment-local/src/index.ts:51`](../packages/attachment/attachment-local/src/index.ts) +来源:[`packages/attachment/attachment-local/src/index.ts:55`](../packages/attachment/attachment-local/src/index.ts) @@ -945,7 +953,7 @@ export interface DeepSeekCatalogModel { inputModalities?: ModelModality[] /** Total-pixel budget for one deterministic request preview, or the 512-by-512 `low` preset. */ imagePixelBudget?: number | 'low' - /** Encoded-byte cap for one deterministic request preview. */ + /** Encoded-byte target for one deterministic request preview; the smallest quality-ladder output is used when no quality fits. */ imageMaxBytes?: number } ``` @@ -1056,7 +1064,10 @@ export interface PiAiProviderProfile { maxRequestImageBytes?: number /** Total-pixel budget for each deterministic inline request version. */ requestImagePixelBudget?: number - /** Raw encoded-byte cap for each deterministic inline request version. */ + /** + * Raw encoded-byte target for each deterministic inline request version; + * the smallest quality-ladder output is used when no quality fits. + */ requestImageMaxBytes?: number /** Provider-owned model-request retry policy; omission uses normal mode with five retries. */ retryPolicy?: RetryPolicyConfig @@ -1672,6 +1683,22 @@ export interface Config { 来源:[`packages/sandbox/sandbox-policy/src/index.ts:67`](../packages/sandbox/sandbox-policy/src/index.ts) + + +## `@deepseek-ai/dsh-sdk-app` + +需要:`cmdlineArgs` + +```ts config-catalog +/** SDK stdio startup configuration. */ +export interface Config { + /** Profile name rendered in help and diagnostics (default `sdk`). */ + profile?: string +} +``` + +来源:[`packages/bundle/sdk-app/src/index.ts:23`](../packages/bundle/sdk-app/src/index.ts) + ## `@deepseek-ai/dsh-sdk-jsonrpc-server` @@ -1683,6 +1710,13 @@ export interface Config { export interface JsonRpcConfig { /** Report max-token turn/subagent termination as a successful SDK result. */ maxTokensAsSuccess?: boolean + /** Per-root-agent model-facing tool filter; an allow list excludes later unnamed global tools. */ + toolFilter?: { + /** Global tool names that remain visible. */ + allow?: string[] + /** Global tool names removed from visibility. */ + deny?: string[] + } /** Transport input override; production uses `process.stdin`. */ input?: Readable /** Transport output override; production uses `process.stdout`. */ @@ -1694,7 +1728,7 @@ export interface JsonRpcConfig { 依赖:`Readable`(`node:stream`)· `Writable`(`node:stream`) -来源:[`packages/sdk/server/src/index.ts:29`](../packages/sdk/server/src/index.ts) +来源:[`packages/sdk/server/src/index.ts:25`](../packages/sdk/server/src/index.ts) @@ -2419,6 +2453,8 @@ export interface Config { * `deployment:persona` shadows it; `{{variable}}` references are strict. */ persona?: string + /** Treat the deployment persona as the complete system prompt (default false). */ + personaComplete?: boolean /** * Model-facing tool names in order, with {@link TOOL_ORDER_REST} exactly once. * Invalid fields fail at load and unknown names fail at assembly; known names @@ -2818,6 +2854,14 @@ export interface Config { * a distinct name. */ toolName?: string + /** Let the model discover and select the child LLM route (default false). */ + enableModelSelection?: boolean + /** + * Sample the Host `subagent-model-selection` user setting for each new + * top-level session and inherit that decision in its child sessions. Mutually + * exclusive with `enableModelSelection`. + */ + modelSelectionSettings?: boolean /** * Expose `run_in_background` (default true). Disabled instances omit the * parameter and reject forced background calls. @@ -2865,7 +2909,7 @@ export interface Config { 依赖:[`AgentOptions`](subsystems/core.zh.md) -来源:[`packages/subagent/tool-subagent/src/index.ts:29`](../packages/subagent/tool-subagent/src/index.ts) +来源:[`packages/subagent/tool-subagent/src/index.ts:48`](../packages/subagent/tool-subagent/src/index.ts) @@ -3323,7 +3367,6 @@ export interface Config { - `@deepseek-ai/dsh-llm`([`packages/llm/llm/src/index.ts`](../packages/llm/llm/src/index.ts)) - `@deepseek-ai/dsh-lsp`([`packages/lsp/lsp/src/index.ts`](../packages/lsp/lsp/src/index.ts)) - `@deepseek-ai/dsh-schedule` — 需要 `agents` · `sessions` · `tools` · `sessionPersistence`([`packages/schedule/schedule/src/index.ts`](../packages/schedule/schedule/src/index.ts)) -- `@deepseek-ai/dsh-sdk-app` — 需要 `cmdlineArgs`([`packages/bundle/sdk-app/src/index.ts`](../packages/bundle/sdk-app/src/index.ts)) - `@deepseek-ai/dsh-session`([`packages/core/session/src/index.ts`](../packages/core/session/src/index.ts)) - `@deepseek-ai/dsh-session-checkpoint-policy` — 需要 `llm` · `sessionPersistence` · `sessions` · `tools`([`packages/session/session-checkpoint-policy/src/index.ts`](../packages/session/session-checkpoint-policy/src/index.ts)) - `@deepseek-ai/dsh-session-log-export` — 需要 `commands`([`packages/session-query/session-log-export/src/index.ts`](../packages/session-query/session-log-export/src/index.ts)) @@ -3392,8 +3435,8 @@ export interface Config { - `@deepseek-ai/dsh-sandbox-windows-acl`([`packages/sandbox/sandbox-windows-acl/src/index.ts`](../packages/sandbox/sandbox-windows-acl/src/index.ts)) - `@deepseek-ai/dsh-scope`([`packages/core/scope/src/index.ts`](../packages/core/scope/src/index.ts)) - `@deepseek-ai/dsh-sdk-client`([`packages/sdk/client/src/index.ts`](../packages/sdk/client/src/index.ts)) +- `@deepseek-ai/dsh-sdk-minimal`([`packages/bundle/sdk-minimal/src/index.ts`](../packages/bundle/sdk-minimal/src/index.ts)) - `@deepseek-ai/dsh-sdk-protocol`([`packages/sdk/protocol/src/index.ts`](../packages/sdk/protocol/src/index.ts)) -- `@deepseek-ai/dsh-sdk-python-runtime`([`packages/sdk/python-runtime/src/index.ts`](../packages/sdk/python-runtime/src/index.ts)) - `@deepseek-ai/dsh-session-telemetry`([`packages/session/session-telemetry/src/index.ts`](../packages/session/session-telemetry/src/index.ts)) - `@deepseek-ai/dsh-session-title-llm`([`packages/session/session-title-llm/src/index.ts`](../packages/session/session-title-llm/src/index.ts)) - `@deepseek-ai/dsh-subagent-in-process-driver`([`packages/subagent/subagent-in-process-driver/src/index.ts`](../packages/subagent/subagent-in-process-driver/src/index.ts)) diff --git a/docs/event-producer-consumer.i18n.yaml b/docs/event-producer-consumer.i18n.yaml index c9e637910e..33c536f50a 100644 --- a/docs/event-producer-consumer.i18n.yaml +++ b/docs/event-producer-consumer.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/event-producer-consumer.md -event-producer-consumer.md: 2be5a84969b9f14823abf90cf289a0a41e48dd11 -event-producer-consumer.zh.md: 5bbae1be5d03c3e443d36093ce60dbf7e4b07971 +event-producer-consumer.md: 4ab1275e9309e150504d6a3bdd80792bb1a49ea1 +event-producer-consumer.zh.md: 8d8a401bfdccc74d5774ce5ce7ceac2c548f6c72 diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 2be5a84969..4ab1275e93 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -9,18 +9,18 @@ This matrix shows which packages dispatch each harness-owned event and which pac | --- | --- | --- | --- | --- | | `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:183`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | | `agent-preset/selected` | `emit` | [`packages/preset/agent-presets/src/types.ts:13`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `remotes` | -| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `tool-agent-team` | -| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), `tool-agent-team` | -| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) | -| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) | -| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:205`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) | -| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:186`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) | -| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill) | -| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:244`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) | -| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:260`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-retry`](../packages/llm/llm-retry) | -| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:217`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) | -| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:178`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` | -| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:278`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) | +| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:161`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:170`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:292`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) | +| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:199`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) | +| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:207`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) | +| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:188`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) | +| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:233`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) | +| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:246`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) | +| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:262`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-retry`](../packages/llm/llm-retry) | +| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:219`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) | +| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:180`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` | +| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) | | `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:444`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:424`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:451`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | @@ -52,13 +52,13 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:48`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `remotes` | | `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:35`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) | | `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:297`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - | -| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:164`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), `server`, [`subagent`](../packages/subagent/subagent) | -| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:138`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:144`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:155`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`subagent`](../packages/subagent/subagent) | +| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:165`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), `server`, [`subagent`](../packages/subagent/subagent) | +| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:139`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:145`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:156`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`subagent`](../packages/subagent/subagent) | | `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`system-prompt`](../packages/core/system-prompt) | | `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - | -| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:207`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`emit`) | - | +| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:207`](../packages/core/tools/src/index.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`), [`tools`](../packages/core/tools) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | | `tools/code-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:189`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) | | `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:163`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` | | `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:175`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index 5bbae1be5d..8d8a401bfd 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -11,13 +11,13 @@ | --- | --- | --- | --- | --- | | `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:183`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | | `agent-preset/selected` | `emit` | [`packages/preset/agent-presets/src/types.ts:13`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `remotes` | -| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `tool-agent-team` | -| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), `tool-agent-team` | +| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:161`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:170`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | | `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) | | `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) | | `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:205`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) | | `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:186`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) | -| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill) | +| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) | | `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:244`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) | | `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:260`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-retry`](../packages/llm/llm-retry) | | `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:217`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) | @@ -54,13 +54,13 @@ | `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:48`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `remotes` | | `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:35`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) | | `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:297`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - | -| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:164`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), `server`, [`subagent`](../packages/subagent/subagent) | -| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:138`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:144`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:155`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`subagent`](../packages/subagent/subagent) | +| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:165`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), `server`, [`subagent`](../packages/subagent/subagent) | +| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:139`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:145`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:156`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`subagent`](../packages/subagent/subagent) | | `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`system-prompt`](../packages/core/system-prompt) | | `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - | -| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:207`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`emit`) | - | +| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:207`](../packages/core/tools/src/index.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`), [`tools`](../packages/core/tools) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | | `tools/code-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:189`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) | | `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:163`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` | | `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:175`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) | diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index 8ba88343ec..a467f4aea6 100644 --- a/docs/module-graph.i18n.yaml +++ b/docs/module-graph.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/module-graph.md -module-graph.md: aeb35195f3a095b9de694f164dc1111acf5c8197 -module-graph.zh.md: ef3bc3fbce9cad37542eeea6adf936dd70e6581b +module-graph.md: 9d192404f4949bcf6e5c4222f0b898cc52362bd8 +module-graph.zh.md: bbdfd5642238644939fd08c02b9786310bad63e2 diff --git a/docs/module-graph.md b/docs/module-graph.md index aeb35195f3..9d192404f4 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -123,6 +123,7 @@ flowchart TD pkg_base["base"] pkg_headless["headless"] pkg_sdk_app["sdk-app"] + pkg_sdk_minimal["sdk-minimal"] pkg_web_app["web-app"] end subgraph group_client["packages/client"] @@ -275,7 +276,6 @@ flowchart TD pkg_sdk_client["sdk-client"] pkg_sdk_jsonrpc_server["sdk-jsonrpc-server"] pkg_sdk_protocol["sdk-protocol"] - pkg_sdk_python_runtime["sdk-python-runtime"] end subgraph group_session["packages/session"] pkg_session_checkpoint_policy["session-checkpoint-policy"] @@ -367,6 +367,7 @@ flowchart TD pkg_acp_app --> pkg_invariants pkg_base --> pkg_invariants pkg_sdk_app --> pkg_invariants + pkg_sdk_minimal --> pkg_invariants pkg_client_store --> pkg_invariants pkg_client_ui_primitives --> pkg_invariants pkg_client_ui_renderer --> pkg_invariants @@ -381,7 +382,6 @@ flowchart TD pkg_host_directory_picker_native --> pkg_invariants pkg_host_webserver --> pkg_invariants pkg_sandbox_windows_acl --> pkg_invariants - pkg_sdk_python_runtime --> pkg_invariants pkg_storage --> pkg_invariants pkg_subprocess --> pkg_invariants pkg_win32_process --> pkg_invariants @@ -570,14 +570,6 @@ flowchart TD pkg_jobs --> pkg_brand pkg_jobs --> pkg_invariants pkg_jobs --> pkg_session - pkg_agent_presets --> pkg_agent - pkg_agent_presets --> pkg_atomic_write - pkg_agent_presets --> pkg_home_paths - pkg_agent_presets --> pkg_invariants - pkg_agent_presets --> pkg_scope - pkg_agent_presets --> pkg_session - pkg_agent_presets --> pkg_settings - pkg_agent_presets --> pkg_system_prompt pkg_sandbox_local --> pkg_invariants pkg_sandbox_local --> pkg_llm pkg_sandbox_local --> pkg_sandbox @@ -655,11 +647,6 @@ flowchart TD pkg_llm_pi_ai --> pkg_llm pkg_llm_pi_ai --> pkg_settings pkg_llm_pi_ai --> pkg_timeout - pkg_plugin_package_inventory_deepseek --> pkg_agent - pkg_plugin_package_inventory_deepseek --> pkg_agent_presets - pkg_plugin_package_inventory_deepseek --> pkg_deepseek_llm_api_extensions - pkg_plugin_package_inventory_deepseek --> pkg_invariants - pkg_plugin_package_inventory_deepseek --> pkg_session pkg_tools --> pkg_agent pkg_tools --> pkg_code_runtime pkg_tools --> pkg_invariants @@ -710,8 +697,6 @@ flowchart TD pkg_command_feedback --> pkg_invariants pkg_command_feedback --> pkg_session pkg_command_feedback --> pkg_session_telemetry - pkg_host_apiproxy --> pkg_agent_presets - pkg_host_apiproxy --> pkg_invariants pkg_permission_presets --> pkg_commands pkg_permission_presets --> pkg_invariants pkg_permission_presets --> pkg_sandbox @@ -812,21 +797,6 @@ flowchart TD pkg_tool_skill --> pkg_llm pkg_tool_skill --> pkg_skill pkg_tool_skill --> pkg_tools - pkg_subagent --> pkg_agent - pkg_subagent --> pkg_agent_presets - pkg_subagent --> pkg_brand - pkg_subagent --> pkg_invariants - pkg_subagent --> pkg_jobs - pkg_subagent --> pkg_llm - pkg_subagent --> pkg_sandbox - pkg_subagent --> pkg_sandbox_policy - pkg_subagent --> pkg_scope - pkg_subagent --> pkg_session - pkg_subagent --> pkg_session_persistence - pkg_subagent --> pkg_session_projection - pkg_subagent --> pkg_session_projection_cache - pkg_subagent --> pkg_tools - pkg_subagent --> pkg_user_approval pkg_tool_web --> pkg_invariants pkg_tool_web --> pkg_llm pkg_tool_web --> pkg_system_prompt @@ -874,10 +844,6 @@ flowchart TD pkg_file_reference_local --> pkg_invariants pkg_file_reference_local --> pkg_system_prompt pkg_file_reference_local --> pkg_tools - pkg_experimental_webworker_runtime --> pkg_client_modules - pkg_experimental_webworker_runtime --> pkg_host_apiproxy - pkg_experimental_webworker_runtime --> pkg_host_webserver - pkg_experimental_webworker_runtime --> pkg_invariants pkg_cordis_host_runner --> pkg_agent pkg_cordis_host_runner --> pkg_brand pkg_cordis_host_runner --> pkg_invariants @@ -917,6 +883,15 @@ flowchart TD pkg_mcp_client --> pkg_subprocess pkg_mcp_client --> pkg_timeout pkg_mcp_client --> pkg_tools + pkg_agent_presets --> pkg_agent + pkg_agent_presets --> pkg_atomic_write + pkg_agent_presets --> pkg_home_paths + pkg_agent_presets --> pkg_invariants + pkg_agent_presets --> pkg_scope + pkg_agent_presets --> pkg_session + pkg_agent_presets --> pkg_settings + pkg_agent_presets --> pkg_system_prompt + pkg_agent_presets --> pkg_tools pkg_schedule --> pkg_agent pkg_schedule --> pkg_brand pkg_schedule --> pkg_invariants @@ -990,6 +965,89 @@ flowchart TD pkg_llm_replay --> pkg_invariants pkg_llm_replay --> pkg_llm pkg_llm_replay --> pkg_session + pkg_tool_workflow --> pkg_agent + pkg_tool_workflow --> pkg_invariants + pkg_tool_workflow --> pkg_llm + pkg_tool_workflow --> pkg_session + pkg_tool_workflow --> pkg_system_prompt + pkg_tool_workflow --> pkg_tools + pkg_tool_workflow --> pkg_workflow + pkg_plugin_package_inventory_deepseek --> pkg_agent + pkg_plugin_package_inventory_deepseek --> pkg_agent_presets + pkg_plugin_package_inventory_deepseek --> pkg_deepseek_llm_api_extensions + pkg_plugin_package_inventory_deepseek --> pkg_invariants + pkg_plugin_package_inventory_deepseek --> pkg_session + pkg_subagent --> pkg_agent + pkg_subagent --> pkg_agent_presets + pkg_subagent --> pkg_brand + pkg_subagent --> pkg_invariants + pkg_subagent --> pkg_jobs + pkg_subagent --> pkg_llm + pkg_subagent --> pkg_sandbox + pkg_subagent --> pkg_sandbox_policy + pkg_subagent --> pkg_scope + pkg_subagent --> pkg_session + pkg_subagent --> pkg_session_persistence + pkg_subagent --> pkg_session_projection + pkg_subagent --> pkg_session_projection_cache + pkg_subagent --> pkg_tools + pkg_subagent --> pkg_user_approval + pkg_session_query --> pkg_brand + pkg_session_query --> pkg_invariants + pkg_session_query --> pkg_llm + pkg_session_query --> pkg_session + pkg_session_query --> pkg_session_persistence + pkg_session_query --> pkg_session_title + pkg_session_query --> pkg_tool_todo + pkg_acp --> pkg_agent + pkg_acp --> pkg_attachment + pkg_acp --> pkg_invariants + pkg_acp --> pkg_llm + pkg_acp --> pkg_mcp_client + pkg_acp --> pkg_session + pkg_acp --> pkg_session_persistence + pkg_acp --> pkg_token_meter + pkg_acp --> pkg_user_approval + pkg_web_app --> pkg_invariants + pkg_web_app --> pkg_shell_env + pkg_web_app --> pkg_system_prompt + pkg_compaction_tool_result_pruner --> pkg_compaction + pkg_compaction_tool_result_pruner --> pkg_invariants + pkg_compaction_tool_result_pruner --> pkg_llm + pkg_compaction_tool_result_pruner --> pkg_session + pkg_compaction_tool_result_pruner --> pkg_token_meter + pkg_tool_cordis --> pkg_agent + pkg_tool_cordis --> pkg_cordis_host_runner + pkg_tool_cordis --> pkg_invariants + pkg_tool_cordis --> pkg_llm + pkg_tool_cordis --> pkg_scope + pkg_tool_cordis --> pkg_session + pkg_tool_cordis --> pkg_system_prompt + pkg_tool_cordis --> pkg_tools + pkg_host_apiproxy --> pkg_agent_presets + pkg_host_apiproxy --> pkg_invariants + pkg_tool_bash --> pkg_agent + pkg_tool_bash --> pkg_invariants + pkg_tool_bash --> pkg_jobs + pkg_tool_bash --> pkg_llm + pkg_tool_bash --> pkg_sandbox + pkg_tool_bash --> pkg_sandbox_policy + pkg_tool_bash --> pkg_shell + pkg_tool_bash --> pkg_shell_env + pkg_tool_bash --> pkg_system_prompt + pkg_tool_bash --> pkg_tools + pkg_tool_bash --> pkg_user_approval + pkg_tool_pwsh --> pkg_agent + pkg_tool_pwsh --> pkg_invariants + pkg_tool_pwsh --> pkg_jobs + pkg_tool_pwsh --> pkg_llm + pkg_tool_pwsh --> pkg_sandbox + pkg_tool_pwsh --> pkg_sandbox_policy + pkg_tool_pwsh --> pkg_shell + pkg_tool_pwsh --> pkg_shell_env + pkg_tool_pwsh --> pkg_system_prompt + pkg_tool_pwsh --> pkg_tools + pkg_tool_pwsh --> pkg_user_approval pkg_webhook --> pkg_agent pkg_webhook --> pkg_agent_default_model pkg_webhook --> pkg_agent_presets @@ -1000,13 +1058,6 @@ flowchart TD pkg_webhook --> pkg_session pkg_webhook --> pkg_session_title pkg_webhook --> pkg_workspace - pkg_tool_workflow --> pkg_agent - pkg_tool_workflow --> pkg_invariants - pkg_tool_workflow --> pkg_llm - pkg_tool_workflow --> pkg_session - pkg_tool_workflow --> pkg_system_prompt - pkg_tool_workflow --> pkg_tools - pkg_tool_workflow --> pkg_workflow pkg_subagent_acp --> pkg_agent pkg_subagent_acp --> pkg_invariants pkg_subagent_acp --> pkg_llm @@ -1037,6 +1088,9 @@ flowchart TD pkg_tool_subagent --> pkg_invariants pkg_tool_subagent --> pkg_jobs pkg_tool_subagent --> pkg_llm + pkg_tool_subagent --> pkg_scope + pkg_tool_subagent --> pkg_session + pkg_tool_subagent --> pkg_settings pkg_tool_subagent --> pkg_subagent pkg_tool_subagent --> pkg_system_prompt pkg_tool_subagent --> pkg_tools @@ -1058,107 +1112,6 @@ flowchart TD pkg_hooks_claude_code --> pkg_session_persistence pkg_hooks_claude_code --> pkg_subagent pkg_hooks_claude_code --> pkg_tools - pkg_session_query --> pkg_brand - pkg_session_query --> pkg_invariants - pkg_session_query --> pkg_llm - pkg_session_query --> pkg_session - pkg_session_query --> pkg_session_persistence - pkg_session_query --> pkg_session_title - pkg_session_query --> pkg_tool_todo - pkg_acp --> pkg_agent - pkg_acp --> pkg_attachment - pkg_acp --> pkg_invariants - pkg_acp --> pkg_llm - pkg_acp --> pkg_mcp_client - pkg_acp --> pkg_session - pkg_acp --> pkg_session_persistence - pkg_acp --> pkg_token_meter - pkg_acp --> pkg_user_approval - pkg_web_app --> pkg_invariants - pkg_web_app --> pkg_shell_env - pkg_web_app --> pkg_system_prompt - pkg_client_connection --> pkg_attachment - pkg_client_connection --> pkg_commands - pkg_client_connection --> pkg_host_apiproxy - pkg_client_connection --> pkg_host_webserver - pkg_client_connection --> pkg_invariants - pkg_client_connection --> pkg_llm - pkg_client_connection --> pkg_session - pkg_client_connection --> pkg_tool_todo - pkg_compaction_tool_result_pruner --> pkg_compaction - pkg_compaction_tool_result_pruner --> pkg_invariants - pkg_compaction_tool_result_pruner --> pkg_llm - pkg_compaction_tool_result_pruner --> pkg_session - pkg_compaction_tool_result_pruner --> pkg_token_meter - pkg_experimental_agent_team --> pkg_agent - pkg_experimental_agent_team --> pkg_brand - pkg_experimental_agent_team --> pkg_invariants - pkg_experimental_agent_team --> pkg_llm - pkg_experimental_agent_team --> pkg_session - pkg_experimental_agent_team --> pkg_session_persistence - pkg_experimental_agent_team --> pkg_subagent - pkg_tool_cordis --> pkg_agent - pkg_tool_cordis --> pkg_cordis_host_runner - pkg_tool_cordis --> pkg_invariants - pkg_tool_cordis --> pkg_llm - pkg_tool_cordis --> pkg_scope - pkg_tool_cordis --> pkg_session - pkg_tool_cordis --> pkg_system_prompt - pkg_tool_cordis --> pkg_tools - pkg_sdk_protocol --> pkg_invariants - pkg_sdk_protocol --> pkg_llm - pkg_sdk_protocol --> pkg_session - pkg_sdk_protocol --> pkg_subagent - pkg_tool_bash --> pkg_agent - pkg_tool_bash --> pkg_invariants - pkg_tool_bash --> pkg_jobs - pkg_tool_bash --> pkg_llm - pkg_tool_bash --> pkg_sandbox - pkg_tool_bash --> pkg_sandbox_policy - pkg_tool_bash --> pkg_shell - pkg_tool_bash --> pkg_shell_env - pkg_tool_bash --> pkg_system_prompt - pkg_tool_bash --> pkg_tools - pkg_tool_bash --> pkg_user_approval - pkg_tool_pwsh --> pkg_agent - pkg_tool_pwsh --> pkg_invariants - pkg_tool_pwsh --> pkg_jobs - pkg_tool_pwsh --> pkg_llm - pkg_tool_pwsh --> pkg_sandbox - pkg_tool_pwsh --> pkg_sandbox_policy - pkg_tool_pwsh --> pkg_shell - pkg_tool_pwsh --> pkg_shell_env - pkg_tool_pwsh --> pkg_system_prompt - pkg_tool_pwsh --> pkg_tools - pkg_tool_pwsh --> pkg_user_approval - pkg_webhook_github --> pkg_credentials - pkg_webhook_github --> pkg_host_webserver - pkg_webhook_github --> pkg_invariants - pkg_webhook_github --> pkg_session - pkg_webhook_github --> pkg_webhook - pkg_tool_ralph --> pkg_agent - pkg_tool_ralph --> pkg_invariants - pkg_tool_ralph --> pkg_llm - pkg_tool_ralph --> pkg_subagent - pkg_tool_ralph --> pkg_system_prompt - pkg_tool_ralph --> pkg_tools - pkg_tool_ralph --> pkg_workflow - pkg_workflow_worker_thread --> pkg_agent - pkg_workflow_worker_thread --> pkg_brand - pkg_workflow_worker_thread --> pkg_invariants - pkg_workflow_worker_thread --> pkg_llm - pkg_workflow_worker_thread --> pkg_session - pkg_workflow_worker_thread --> pkg_subagent - pkg_workflow_worker_thread --> pkg_tools - pkg_workflow_worker_thread --> pkg_workflow - pkg_subagent_fork_in_process --> pkg_agent - pkg_subagent_fork_in_process --> pkg_invariants - pkg_subagent_fork_in_process --> pkg_session - pkg_subagent_fork_in_process --> pkg_subagent - pkg_subagent_fork_in_process --> pkg_subagent_in_process_driver - pkg_subagent_spawn_in_process --> pkg_invariants - pkg_subagent_spawn_in_process --> pkg_subagent - pkg_subagent_spawn_in_process --> pkg_subagent_in_process_driver pkg_session_query_sqlite --> pkg_invariants pkg_session_query_sqlite --> pkg_session pkg_session_query_sqlite --> pkg_session_persistence @@ -1170,11 +1123,14 @@ flowchart TD pkg_tool_session_query --> pkg_system_prompt pkg_tool_session_query --> pkg_timeout pkg_tool_session_query --> pkg_tools - pkg_api_gateway --> pkg_brand - pkg_api_gateway --> pkg_client_connection - pkg_api_gateway --> pkg_host_webserver - pkg_api_gateway --> pkg_invariants - pkg_api_gateway --> pkg_typert_registry + pkg_client_connection --> pkg_attachment + pkg_client_connection --> pkg_commands + pkg_client_connection --> pkg_host_apiproxy + pkg_client_connection --> pkg_host_webserver + pkg_client_connection --> pkg_invariants + pkg_client_connection --> pkg_llm + pkg_client_connection --> pkg_session + pkg_client_connection --> pkg_tool_todo pkg_compaction_basic --> pkg_agent pkg_compaction_basic --> pkg_commands pkg_compaction_basic --> pkg_compaction @@ -1213,6 +1169,54 @@ flowchart TD pkg_agent_spine_demo --> pkg_tool_jobs pkg_agent_spine_demo --> pkg_tool_skill pkg_agent_spine_demo --> pkg_tools + pkg_experimental_agent_team --> pkg_agent + pkg_experimental_agent_team --> pkg_brand + pkg_experimental_agent_team --> pkg_invariants + pkg_experimental_agent_team --> pkg_llm + pkg_experimental_agent_team --> pkg_session + pkg_experimental_agent_team --> pkg_session_persistence + pkg_experimental_agent_team --> pkg_subagent + pkg_experimental_webworker_runtime --> pkg_client_modules + pkg_experimental_webworker_runtime --> pkg_host_apiproxy + pkg_experimental_webworker_runtime --> pkg_host_webserver + pkg_experimental_webworker_runtime --> pkg_invariants + pkg_sdk_protocol --> pkg_invariants + pkg_sdk_protocol --> pkg_llm + pkg_sdk_protocol --> pkg_session + pkg_sdk_protocol --> pkg_subagent + pkg_webhook_github --> pkg_credentials + pkg_webhook_github --> pkg_host_webserver + pkg_webhook_github --> pkg_invariants + pkg_webhook_github --> pkg_session + pkg_webhook_github --> pkg_webhook + pkg_tool_ralph --> pkg_agent + pkg_tool_ralph --> pkg_invariants + pkg_tool_ralph --> pkg_llm + pkg_tool_ralph --> pkg_subagent + pkg_tool_ralph --> pkg_system_prompt + pkg_tool_ralph --> pkg_tools + pkg_tool_ralph --> pkg_workflow + pkg_workflow_worker_thread --> pkg_agent + pkg_workflow_worker_thread --> pkg_brand + pkg_workflow_worker_thread --> pkg_invariants + pkg_workflow_worker_thread --> pkg_llm + pkg_workflow_worker_thread --> pkg_session + pkg_workflow_worker_thread --> pkg_subagent + pkg_workflow_worker_thread --> pkg_tools + pkg_workflow_worker_thread --> pkg_workflow + pkg_subagent_fork_in_process --> pkg_agent + pkg_subagent_fork_in_process --> pkg_invariants + pkg_subagent_fork_in_process --> pkg_session + pkg_subagent_fork_in_process --> pkg_subagent + pkg_subagent_fork_in_process --> pkg_subagent_in_process_driver + pkg_subagent_spawn_in_process --> pkg_invariants + pkg_subagent_spawn_in_process --> pkg_subagent + pkg_subagent_spawn_in_process --> pkg_subagent_in_process_driver + pkg_api_gateway --> pkg_brand + pkg_api_gateway --> pkg_client_connection + pkg_api_gateway --> pkg_host_webserver + pkg_api_gateway --> pkg_invariants + pkg_api_gateway --> pkg_typert_registry pkg_experimental_tool_agent_team --> pkg_agent pkg_experimental_tool_agent_team --> pkg_experimental_agent_team pkg_experimental_tool_agent_team --> pkg_invariants @@ -1658,6 +1662,7 @@ flowchart TD | [`acp-app`](../packages/bundle/acp-app) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`base`](../packages/bundle/base) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`sdk-app`](../packages/bundle/sdk-app) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants) | +| [`sdk-minimal`](../packages/bundle/sdk-minimal) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-store`](../packages/client/store) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-primitives`](../packages/client/ui-primitives) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-renderer`](../packages/client/ui-renderer) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) | @@ -1672,7 +1677,6 @@ flowchart TD | [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`host-webserver`](../packages/host/webserver) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | [`invariants`](../packages/runtime-diagnostics/invariants) | -| [`sdk-python-runtime`](../packages/sdk/python-runtime) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`storage`](../packages/storage/storage) | `storage` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`subprocess`](../packages/subprocess/subprocess) | `subprocess` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`win32-process`](../packages/subprocess/win32-process) | `subprocess` | [`invariants`](../packages/runtime-diagnostics/invariants) | @@ -1731,7 +1735,6 @@ flowchart TD | [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) | | [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) | | [`jobs`](../packages/jobs/jobs) | `jobs` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | -| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`agent`](../packages/core/agent), [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt) | | [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) | | [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) | | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) | @@ -1747,7 +1750,6 @@ flowchart TD | [`workspace`](../packages/workspace/workspace) | `workspace` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage`](../packages/storage/storage), [`storage-domain`](../packages/storage/storage-domain) | | [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`atomic-write`](../packages/util/atomic-write), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`credentials`](../packages/credentials/credentials), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) | | [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`attachment`](../packages/attachment/attachment), [`authorization`](../packages/credentials/authorization), [`credentials`](../packages/credentials/credentials), [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) | -| [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | `llm` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | | [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/interaction/user-approval) | | [`command-goal`](../packages/goal/command-goal) | `goal` | [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm) | | [`goal-round-driver`](../packages/goal/goal-round-driver) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | @@ -1760,7 +1762,6 @@ flowchart TD | [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`shell`](../packages/shell/shell) | | [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry) | -| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`permission-presets`](../packages/interaction/permission-presets) | `interaction` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`shell`](../packages/shell/shell), [`user-approval`](../packages/interaction/user-approval) | | [`jobs-local`](../packages/jobs/jobs-local) | `jobs` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`scope`](../packages/core/scope), [`timeout`](../packages/util/timeout) | | [`lsp-stdio`](../packages/lsp/lsp-stdio) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | @@ -1777,7 +1778,6 @@ flowchart TD | [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | | [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) | | [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) | -| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | [`tool-web`](../packages/web/tool-web) | `web` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) | | [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`tools`](../packages/core/tools) | | [`tool-todo`](../packages/todo/tool-todo) | `todo` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) | @@ -1786,7 +1786,6 @@ flowchart TD | [`command-compact`](../packages/compaction/command-compact) | `compaction` | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`agent-instructions`](../packages/context/agent-instructions) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) | | [`file-reference-local`](../packages/context/file-reference-local) | `context` | [`agent`](../packages/core/agent), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-modules`](../packages/client/modules), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) | `extensions` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) | | [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder) | `guard` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`tools`](../packages/core/tools) | | [`tool-call-timeout-policy`](../packages/guard/timeout-policy) | `guard` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | @@ -1794,6 +1793,7 @@ flowchart TD | [`tool-jobs`](../packages/jobs/tool-jobs) | `jobs` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`tool-lsp`](../packages/lsp/tool-lsp) | `lsp` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | | [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | +| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`agent`](../packages/core/agent), [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) | | [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy) | `session` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) | | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | `session` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`command-feedback`](../packages/feedback/command-feedback), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry) | @@ -1807,37 +1807,41 @@ flowchart TD | [`tool-terminal`](../packages/terminal/tool-terminal) | `terminal` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`system-prompt`](../packages/core/system-prompt), [`terminal`](../packages/terminal/terminal), [`tools`](../packages/core/tools) | | [`agent-loop-testkit`](../packages/test-support/agent-loop-testkit) | `test-support` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`llm-replay`](../packages/test-support/llm-replay) | `test-support` | [`compaction`](../packages/compaction/compaction), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | -| [`webhook`](../packages/webhook/webhook) | `webhook` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) | | [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | +| [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | `llm` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | +| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | +| [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-title`](../packages/session/session-title), [`tool-todo`](../packages/todo/tool-todo) | +| [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`mcp-client`](../packages/mcp/mcp-client), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`token-meter`](../packages/llm/token-meter), [`user-approval`](../packages/interaction/user-approval) | +| [`web-app`](../packages/bundle/web-app) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt) | +| [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | +| [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | +| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`invariants`](../packages/runtime-diagnostics/invariants) | +| [`tool-bash`](../packages/shell/tool-bash) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | +| [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | +| [`webhook`](../packages/webhook/webhook) | `webhook` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) | | [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | +| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | | [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`hooks-claude-code`](../packages/hooks/hooks-claude-code) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | -| [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-title`](../packages/session/session-title), [`tool-todo`](../packages/todo/tool-todo) | -| [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`mcp-client`](../packages/mcp/mcp-client), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`token-meter`](../packages/llm/token-meter), [`user-approval`](../packages/interaction/user-approval) | -| [`web-app`](../packages/bundle/web-app) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt) | +| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) | +| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | | [`client-connection`](../packages/client/connection) | `client` | [`attachment`](../packages/attachment/attachment), [`commands`](../packages/interaction/commands), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tool-todo`](../packages/todo/tool-todo) | -| [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | +| [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | +| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`typert-protocol`](../packages/typert/protocol) | +| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs-local`](../packages/jobs/jobs-local), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`shell-env`](../packages/shell/shell-env), [`skill`](../packages/skill/skill), [`skill-filesystem`](../packages/skill/skill-filesystem), [`system-prompt`](../packages/core/system-prompt), [`tool-bash`](../packages/shell/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-skill`](../packages/skill/tool-skill), [`tools`](../packages/core/tools) | | [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent) | -| [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | +| [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-modules`](../packages/client/modules), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | -| [`tool-bash`](../packages/shell/tool-bash) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | -| [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | [`webhook-github`](../packages/webhook/webhook-github) | `webhook` | [`credentials`](../packages/credentials/credentials), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`webhook`](../packages/webhook/webhook) | | [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`workflow-worker-thread`](../packages/workflow/workflow-worker-thread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | -| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) | -| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | | [`api-gateway`](../packages/api/gateway) | `api` | [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-registry`](../packages/typert/registry) | -| [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | -| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`typert-protocol`](../packages/typert/protocol) | -| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs-local`](../packages/jobs/jobs-local), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`shell-env`](../packages/shell/shell-env), [`skill`](../packages/skill/skill), [`skill-filesystem`](../packages/skill/skill-filesystem), [`system-prompt`](../packages/core/system-prompt), [`tool-bash`](../packages/shell/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-skill`](../packages/skill/tool-skill), [`tools`](../packages/core/tools) | | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | `experimental` | [`agent`](../packages/core/agent), [`experimental-agent-team`](../packages/experimental/agent-team), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) | | [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index ef3bc3fbce..bbdfd56422 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -125,6 +125,7 @@ flowchart TD pkg_base["base"] pkg_headless["headless"] pkg_sdk_app["sdk-app"] + pkg_sdk_minimal["sdk-minimal"] pkg_web_app["web-app"] end subgraph group_client["packages/client"] @@ -277,7 +278,6 @@ flowchart TD pkg_sdk_client["sdk-client"] pkg_sdk_jsonrpc_server["sdk-jsonrpc-server"] pkg_sdk_protocol["sdk-protocol"] - pkg_sdk_python_runtime["sdk-python-runtime"] end subgraph group_session["packages/session"] pkg_session_checkpoint_policy["session-checkpoint-policy"] @@ -369,6 +369,7 @@ flowchart TD pkg_acp_app --> pkg_invariants pkg_base --> pkg_invariants pkg_sdk_app --> pkg_invariants + pkg_sdk_minimal --> pkg_invariants pkg_client_store --> pkg_invariants pkg_client_ui_primitives --> pkg_invariants pkg_client_ui_renderer --> pkg_invariants @@ -383,7 +384,6 @@ flowchart TD pkg_host_directory_picker_native --> pkg_invariants pkg_host_webserver --> pkg_invariants pkg_sandbox_windows_acl --> pkg_invariants - pkg_sdk_python_runtime --> pkg_invariants pkg_storage --> pkg_invariants pkg_subprocess --> pkg_invariants pkg_win32_process --> pkg_invariants @@ -572,14 +572,6 @@ flowchart TD pkg_jobs --> pkg_brand pkg_jobs --> pkg_invariants pkg_jobs --> pkg_session - pkg_agent_presets --> pkg_agent - pkg_agent_presets --> pkg_atomic_write - pkg_agent_presets --> pkg_home_paths - pkg_agent_presets --> pkg_invariants - pkg_agent_presets --> pkg_scope - pkg_agent_presets --> pkg_session - pkg_agent_presets --> pkg_settings - pkg_agent_presets --> pkg_system_prompt pkg_sandbox_local --> pkg_invariants pkg_sandbox_local --> pkg_llm pkg_sandbox_local --> pkg_sandbox @@ -657,11 +649,6 @@ flowchart TD pkg_llm_pi_ai --> pkg_llm pkg_llm_pi_ai --> pkg_settings pkg_llm_pi_ai --> pkg_timeout - pkg_plugin_package_inventory_deepseek --> pkg_agent - pkg_plugin_package_inventory_deepseek --> pkg_agent_presets - pkg_plugin_package_inventory_deepseek --> pkg_deepseek_llm_api_extensions - pkg_plugin_package_inventory_deepseek --> pkg_invariants - pkg_plugin_package_inventory_deepseek --> pkg_session pkg_tools --> pkg_agent pkg_tools --> pkg_code_runtime pkg_tools --> pkg_invariants @@ -712,8 +699,6 @@ flowchart TD pkg_command_feedback --> pkg_invariants pkg_command_feedback --> pkg_session pkg_command_feedback --> pkg_session_telemetry - pkg_host_apiproxy --> pkg_agent_presets - pkg_host_apiproxy --> pkg_invariants pkg_permission_presets --> pkg_commands pkg_permission_presets --> pkg_invariants pkg_permission_presets --> pkg_sandbox @@ -814,21 +799,6 @@ flowchart TD pkg_tool_skill --> pkg_llm pkg_tool_skill --> pkg_skill pkg_tool_skill --> pkg_tools - pkg_subagent --> pkg_agent - pkg_subagent --> pkg_agent_presets - pkg_subagent --> pkg_brand - pkg_subagent --> pkg_invariants - pkg_subagent --> pkg_jobs - pkg_subagent --> pkg_llm - pkg_subagent --> pkg_sandbox - pkg_subagent --> pkg_sandbox_policy - pkg_subagent --> pkg_scope - pkg_subagent --> pkg_session - pkg_subagent --> pkg_session_persistence - pkg_subagent --> pkg_session_projection - pkg_subagent --> pkg_session_projection_cache - pkg_subagent --> pkg_tools - pkg_subagent --> pkg_user_approval pkg_tool_web --> pkg_invariants pkg_tool_web --> pkg_llm pkg_tool_web --> pkg_system_prompt @@ -876,10 +846,6 @@ flowchart TD pkg_file_reference_local --> pkg_invariants pkg_file_reference_local --> pkg_system_prompt pkg_file_reference_local --> pkg_tools - pkg_experimental_webworker_runtime --> pkg_client_modules - pkg_experimental_webworker_runtime --> pkg_host_apiproxy - pkg_experimental_webworker_runtime --> pkg_host_webserver - pkg_experimental_webworker_runtime --> pkg_invariants pkg_cordis_host_runner --> pkg_agent pkg_cordis_host_runner --> pkg_brand pkg_cordis_host_runner --> pkg_invariants @@ -919,6 +885,15 @@ flowchart TD pkg_mcp_client --> pkg_subprocess pkg_mcp_client --> pkg_timeout pkg_mcp_client --> pkg_tools + pkg_agent_presets --> pkg_agent + pkg_agent_presets --> pkg_atomic_write + pkg_agent_presets --> pkg_home_paths + pkg_agent_presets --> pkg_invariants + pkg_agent_presets --> pkg_scope + pkg_agent_presets --> pkg_session + pkg_agent_presets --> pkg_settings + pkg_agent_presets --> pkg_system_prompt + pkg_agent_presets --> pkg_tools pkg_schedule --> pkg_agent pkg_schedule --> pkg_brand pkg_schedule --> pkg_invariants @@ -992,6 +967,89 @@ flowchart TD pkg_llm_replay --> pkg_invariants pkg_llm_replay --> pkg_llm pkg_llm_replay --> pkg_session + pkg_tool_workflow --> pkg_agent + pkg_tool_workflow --> pkg_invariants + pkg_tool_workflow --> pkg_llm + pkg_tool_workflow --> pkg_session + pkg_tool_workflow --> pkg_system_prompt + pkg_tool_workflow --> pkg_tools + pkg_tool_workflow --> pkg_workflow + pkg_plugin_package_inventory_deepseek --> pkg_agent + pkg_plugin_package_inventory_deepseek --> pkg_agent_presets + pkg_plugin_package_inventory_deepseek --> pkg_deepseek_llm_api_extensions + pkg_plugin_package_inventory_deepseek --> pkg_invariants + pkg_plugin_package_inventory_deepseek --> pkg_session + pkg_subagent --> pkg_agent + pkg_subagent --> pkg_agent_presets + pkg_subagent --> pkg_brand + pkg_subagent --> pkg_invariants + pkg_subagent --> pkg_jobs + pkg_subagent --> pkg_llm + pkg_subagent --> pkg_sandbox + pkg_subagent --> pkg_sandbox_policy + pkg_subagent --> pkg_scope + pkg_subagent --> pkg_session + pkg_subagent --> pkg_session_persistence + pkg_subagent --> pkg_session_projection + pkg_subagent --> pkg_session_projection_cache + pkg_subagent --> pkg_tools + pkg_subagent --> pkg_user_approval + pkg_session_query --> pkg_brand + pkg_session_query --> pkg_invariants + pkg_session_query --> pkg_llm + pkg_session_query --> pkg_session + pkg_session_query --> pkg_session_persistence + pkg_session_query --> pkg_session_title + pkg_session_query --> pkg_tool_todo + pkg_acp --> pkg_agent + pkg_acp --> pkg_attachment + pkg_acp --> pkg_invariants + pkg_acp --> pkg_llm + pkg_acp --> pkg_mcp_client + pkg_acp --> pkg_session + pkg_acp --> pkg_session_persistence + pkg_acp --> pkg_token_meter + pkg_acp --> pkg_user_approval + pkg_web_app --> pkg_invariants + pkg_web_app --> pkg_shell_env + pkg_web_app --> pkg_system_prompt + pkg_compaction_tool_result_pruner --> pkg_compaction + pkg_compaction_tool_result_pruner --> pkg_invariants + pkg_compaction_tool_result_pruner --> pkg_llm + pkg_compaction_tool_result_pruner --> pkg_session + pkg_compaction_tool_result_pruner --> pkg_token_meter + pkg_tool_cordis --> pkg_agent + pkg_tool_cordis --> pkg_cordis_host_runner + pkg_tool_cordis --> pkg_invariants + pkg_tool_cordis --> pkg_llm + pkg_tool_cordis --> pkg_scope + pkg_tool_cordis --> pkg_session + pkg_tool_cordis --> pkg_system_prompt + pkg_tool_cordis --> pkg_tools + pkg_host_apiproxy --> pkg_agent_presets + pkg_host_apiproxy --> pkg_invariants + pkg_tool_bash --> pkg_agent + pkg_tool_bash --> pkg_invariants + pkg_tool_bash --> pkg_jobs + pkg_tool_bash --> pkg_llm + pkg_tool_bash --> pkg_sandbox + pkg_tool_bash --> pkg_sandbox_policy + pkg_tool_bash --> pkg_shell + pkg_tool_bash --> pkg_shell_env + pkg_tool_bash --> pkg_system_prompt + pkg_tool_bash --> pkg_tools + pkg_tool_bash --> pkg_user_approval + pkg_tool_pwsh --> pkg_agent + pkg_tool_pwsh --> pkg_invariants + pkg_tool_pwsh --> pkg_jobs + pkg_tool_pwsh --> pkg_llm + pkg_tool_pwsh --> pkg_sandbox + pkg_tool_pwsh --> pkg_sandbox_policy + pkg_tool_pwsh --> pkg_shell + pkg_tool_pwsh --> pkg_shell_env + pkg_tool_pwsh --> pkg_system_prompt + pkg_tool_pwsh --> pkg_tools + pkg_tool_pwsh --> pkg_user_approval pkg_webhook --> pkg_agent pkg_webhook --> pkg_agent_default_model pkg_webhook --> pkg_agent_presets @@ -1002,13 +1060,6 @@ flowchart TD pkg_webhook --> pkg_session pkg_webhook --> pkg_session_title pkg_webhook --> pkg_workspace - pkg_tool_workflow --> pkg_agent - pkg_tool_workflow --> pkg_invariants - pkg_tool_workflow --> pkg_llm - pkg_tool_workflow --> pkg_session - pkg_tool_workflow --> pkg_system_prompt - pkg_tool_workflow --> pkg_tools - pkg_tool_workflow --> pkg_workflow pkg_subagent_acp --> pkg_agent pkg_subagent_acp --> pkg_invariants pkg_subagent_acp --> pkg_llm @@ -1039,6 +1090,9 @@ flowchart TD pkg_tool_subagent --> pkg_invariants pkg_tool_subagent --> pkg_jobs pkg_tool_subagent --> pkg_llm + pkg_tool_subagent --> pkg_scope + pkg_tool_subagent --> pkg_session + pkg_tool_subagent --> pkg_settings pkg_tool_subagent --> pkg_subagent pkg_tool_subagent --> pkg_system_prompt pkg_tool_subagent --> pkg_tools @@ -1060,107 +1114,6 @@ flowchart TD pkg_hooks_claude_code --> pkg_session_persistence pkg_hooks_claude_code --> pkg_subagent pkg_hooks_claude_code --> pkg_tools - pkg_session_query --> pkg_brand - pkg_session_query --> pkg_invariants - pkg_session_query --> pkg_llm - pkg_session_query --> pkg_session - pkg_session_query --> pkg_session_persistence - pkg_session_query --> pkg_session_title - pkg_session_query --> pkg_tool_todo - pkg_acp --> pkg_agent - pkg_acp --> pkg_attachment - pkg_acp --> pkg_invariants - pkg_acp --> pkg_llm - pkg_acp --> pkg_mcp_client - pkg_acp --> pkg_session - pkg_acp --> pkg_session_persistence - pkg_acp --> pkg_token_meter - pkg_acp --> pkg_user_approval - pkg_web_app --> pkg_invariants - pkg_web_app --> pkg_shell_env - pkg_web_app --> pkg_system_prompt - pkg_client_connection --> pkg_attachment - pkg_client_connection --> pkg_commands - pkg_client_connection --> pkg_host_apiproxy - pkg_client_connection --> pkg_host_webserver - pkg_client_connection --> pkg_invariants - pkg_client_connection --> pkg_llm - pkg_client_connection --> pkg_session - pkg_client_connection --> pkg_tool_todo - pkg_compaction_tool_result_pruner --> pkg_compaction - pkg_compaction_tool_result_pruner --> pkg_invariants - pkg_compaction_tool_result_pruner --> pkg_llm - pkg_compaction_tool_result_pruner --> pkg_session - pkg_compaction_tool_result_pruner --> pkg_token_meter - pkg_experimental_agent_team --> pkg_agent - pkg_experimental_agent_team --> pkg_brand - pkg_experimental_agent_team --> pkg_invariants - pkg_experimental_agent_team --> pkg_llm - pkg_experimental_agent_team --> pkg_session - pkg_experimental_agent_team --> pkg_session_persistence - pkg_experimental_agent_team --> pkg_subagent - pkg_tool_cordis --> pkg_agent - pkg_tool_cordis --> pkg_cordis_host_runner - pkg_tool_cordis --> pkg_invariants - pkg_tool_cordis --> pkg_llm - pkg_tool_cordis --> pkg_scope - pkg_tool_cordis --> pkg_session - pkg_tool_cordis --> pkg_system_prompt - pkg_tool_cordis --> pkg_tools - pkg_sdk_protocol --> pkg_invariants - pkg_sdk_protocol --> pkg_llm - pkg_sdk_protocol --> pkg_session - pkg_sdk_protocol --> pkg_subagent - pkg_tool_bash --> pkg_agent - pkg_tool_bash --> pkg_invariants - pkg_tool_bash --> pkg_jobs - pkg_tool_bash --> pkg_llm - pkg_tool_bash --> pkg_sandbox - pkg_tool_bash --> pkg_sandbox_policy - pkg_tool_bash --> pkg_shell - pkg_tool_bash --> pkg_shell_env - pkg_tool_bash --> pkg_system_prompt - pkg_tool_bash --> pkg_tools - pkg_tool_bash --> pkg_user_approval - pkg_tool_pwsh --> pkg_agent - pkg_tool_pwsh --> pkg_invariants - pkg_tool_pwsh --> pkg_jobs - pkg_tool_pwsh --> pkg_llm - pkg_tool_pwsh --> pkg_sandbox - pkg_tool_pwsh --> pkg_sandbox_policy - pkg_tool_pwsh --> pkg_shell - pkg_tool_pwsh --> pkg_shell_env - pkg_tool_pwsh --> pkg_system_prompt - pkg_tool_pwsh --> pkg_tools - pkg_tool_pwsh --> pkg_user_approval - pkg_webhook_github --> pkg_credentials - pkg_webhook_github --> pkg_host_webserver - pkg_webhook_github --> pkg_invariants - pkg_webhook_github --> pkg_session - pkg_webhook_github --> pkg_webhook - pkg_tool_ralph --> pkg_agent - pkg_tool_ralph --> pkg_invariants - pkg_tool_ralph --> pkg_llm - pkg_tool_ralph --> pkg_subagent - pkg_tool_ralph --> pkg_system_prompt - pkg_tool_ralph --> pkg_tools - pkg_tool_ralph --> pkg_workflow - pkg_workflow_worker_thread --> pkg_agent - pkg_workflow_worker_thread --> pkg_brand - pkg_workflow_worker_thread --> pkg_invariants - pkg_workflow_worker_thread --> pkg_llm - pkg_workflow_worker_thread --> pkg_session - pkg_workflow_worker_thread --> pkg_subagent - pkg_workflow_worker_thread --> pkg_tools - pkg_workflow_worker_thread --> pkg_workflow - pkg_subagent_fork_in_process --> pkg_agent - pkg_subagent_fork_in_process --> pkg_invariants - pkg_subagent_fork_in_process --> pkg_session - pkg_subagent_fork_in_process --> pkg_subagent - pkg_subagent_fork_in_process --> pkg_subagent_in_process_driver - pkg_subagent_spawn_in_process --> pkg_invariants - pkg_subagent_spawn_in_process --> pkg_subagent - pkg_subagent_spawn_in_process --> pkg_subagent_in_process_driver pkg_session_query_sqlite --> pkg_invariants pkg_session_query_sqlite --> pkg_session pkg_session_query_sqlite --> pkg_session_persistence @@ -1172,11 +1125,14 @@ flowchart TD pkg_tool_session_query --> pkg_system_prompt pkg_tool_session_query --> pkg_timeout pkg_tool_session_query --> pkg_tools - pkg_api_gateway --> pkg_brand - pkg_api_gateway --> pkg_client_connection - pkg_api_gateway --> pkg_host_webserver - pkg_api_gateway --> pkg_invariants - pkg_api_gateway --> pkg_typert_registry + pkg_client_connection --> pkg_attachment + pkg_client_connection --> pkg_commands + pkg_client_connection --> pkg_host_apiproxy + pkg_client_connection --> pkg_host_webserver + pkg_client_connection --> pkg_invariants + pkg_client_connection --> pkg_llm + pkg_client_connection --> pkg_session + pkg_client_connection --> pkg_tool_todo pkg_compaction_basic --> pkg_agent pkg_compaction_basic --> pkg_commands pkg_compaction_basic --> pkg_compaction @@ -1215,6 +1171,54 @@ flowchart TD pkg_agent_spine_demo --> pkg_tool_jobs pkg_agent_spine_demo --> pkg_tool_skill pkg_agent_spine_demo --> pkg_tools + pkg_experimental_agent_team --> pkg_agent + pkg_experimental_agent_team --> pkg_brand + pkg_experimental_agent_team --> pkg_invariants + pkg_experimental_agent_team --> pkg_llm + pkg_experimental_agent_team --> pkg_session + pkg_experimental_agent_team --> pkg_session_persistence + pkg_experimental_agent_team --> pkg_subagent + pkg_experimental_webworker_runtime --> pkg_client_modules + pkg_experimental_webworker_runtime --> pkg_host_apiproxy + pkg_experimental_webworker_runtime --> pkg_host_webserver + pkg_experimental_webworker_runtime --> pkg_invariants + pkg_sdk_protocol --> pkg_invariants + pkg_sdk_protocol --> pkg_llm + pkg_sdk_protocol --> pkg_session + pkg_sdk_protocol --> pkg_subagent + pkg_webhook_github --> pkg_credentials + pkg_webhook_github --> pkg_host_webserver + pkg_webhook_github --> pkg_invariants + pkg_webhook_github --> pkg_session + pkg_webhook_github --> pkg_webhook + pkg_tool_ralph --> pkg_agent + pkg_tool_ralph --> pkg_invariants + pkg_tool_ralph --> pkg_llm + pkg_tool_ralph --> pkg_subagent + pkg_tool_ralph --> pkg_system_prompt + pkg_tool_ralph --> pkg_tools + pkg_tool_ralph --> pkg_workflow + pkg_workflow_worker_thread --> pkg_agent + pkg_workflow_worker_thread --> pkg_brand + pkg_workflow_worker_thread --> pkg_invariants + pkg_workflow_worker_thread --> pkg_llm + pkg_workflow_worker_thread --> pkg_session + pkg_workflow_worker_thread --> pkg_subagent + pkg_workflow_worker_thread --> pkg_tools + pkg_workflow_worker_thread --> pkg_workflow + pkg_subagent_fork_in_process --> pkg_agent + pkg_subagent_fork_in_process --> pkg_invariants + pkg_subagent_fork_in_process --> pkg_session + pkg_subagent_fork_in_process --> pkg_subagent + pkg_subagent_fork_in_process --> pkg_subagent_in_process_driver + pkg_subagent_spawn_in_process --> pkg_invariants + pkg_subagent_spawn_in_process --> pkg_subagent + pkg_subagent_spawn_in_process --> pkg_subagent_in_process_driver + pkg_api_gateway --> pkg_brand + pkg_api_gateway --> pkg_client_connection + pkg_api_gateway --> pkg_host_webserver + pkg_api_gateway --> pkg_invariants + pkg_api_gateway --> pkg_typert_registry pkg_experimental_tool_agent_team --> pkg_agent pkg_experimental_tool_agent_team --> pkg_experimental_agent_team pkg_experimental_tool_agent_team --> pkg_invariants @@ -1642,7 +1646,7 @@ flowchart TD pkg_client_ui_cordis --> pkg_invariants ``` -| Package | Group | Depends on | +| 包 | 分组 | 依赖 | | --- | --- | --- | | [`invariants`](../packages/runtime-diagnostics/invariants) | `runtime-diagnostics` | — | | [`atomic-write`](../packages/util/atomic-write) | `util` | [`invariants`](../packages/runtime-diagnostics/invariants) | @@ -1660,6 +1664,7 @@ flowchart TD | [`acp-app`](../packages/bundle/acp-app) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`base`](../packages/bundle/base) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`sdk-app`](../packages/bundle/sdk-app) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants) | +| [`sdk-minimal`](../packages/bundle/sdk-minimal) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-store`](../packages/client/store) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-primitives`](../packages/client/ui-primitives) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-renderer`](../packages/client/ui-renderer) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) | @@ -1674,7 +1679,6 @@ flowchart TD | [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`host-webserver`](../packages/host/webserver) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | [`invariants`](../packages/runtime-diagnostics/invariants) | -| [`sdk-python-runtime`](../packages/sdk/python-runtime) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`storage`](../packages/storage/storage) | `storage` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`subprocess`](../packages/subprocess/subprocess) | `subprocess` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`win32-process`](../packages/subprocess/win32-process) | `subprocess` | [`invariants`](../packages/runtime-diagnostics/invariants) | @@ -1733,7 +1737,6 @@ flowchart TD | [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) | | [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) | | [`jobs`](../packages/jobs/jobs) | `jobs` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | -| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`agent`](../packages/core/agent), [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt) | | [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) | | [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) | | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) | @@ -1749,7 +1752,6 @@ flowchart TD | [`workspace`](../packages/workspace/workspace) | `workspace` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage`](../packages/storage/storage), [`storage-domain`](../packages/storage/storage-domain) | | [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`atomic-write`](../packages/util/atomic-write), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`credentials`](../packages/credentials/credentials), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) | | [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`attachment`](../packages/attachment/attachment), [`authorization`](../packages/credentials/authorization), [`credentials`](../packages/credentials/credentials), [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) | -| [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | `llm` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | | [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/interaction/user-approval) | | [`command-goal`](../packages/goal/command-goal) | `goal` | [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm) | | [`goal-round-driver`](../packages/goal/goal-round-driver) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | @@ -1762,7 +1764,6 @@ flowchart TD | [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`shell`](../packages/shell/shell) | | [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry) | -| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`permission-presets`](../packages/interaction/permission-presets) | `interaction` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`shell`](../packages/shell/shell), [`user-approval`](../packages/interaction/user-approval) | | [`jobs-local`](../packages/jobs/jobs-local) | `jobs` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`scope`](../packages/core/scope), [`timeout`](../packages/util/timeout) | | [`lsp-stdio`](../packages/lsp/lsp-stdio) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | @@ -1779,7 +1780,6 @@ flowchart TD | [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | | [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) | | [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) | -| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | [`tool-web`](../packages/web/tool-web) | `web` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) | | [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`tools`](../packages/core/tools) | | [`tool-todo`](../packages/todo/tool-todo) | `todo` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) | @@ -1788,7 +1788,6 @@ flowchart TD | [`command-compact`](../packages/compaction/command-compact) | `compaction` | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`agent-instructions`](../packages/context/agent-instructions) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) | | [`file-reference-local`](../packages/context/file-reference-local) | `context` | [`agent`](../packages/core/agent), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-modules`](../packages/client/modules), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) | `extensions` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) | | [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder) | `guard` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`tools`](../packages/core/tools) | | [`tool-call-timeout-policy`](../packages/guard/timeout-policy) | `guard` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | @@ -1796,6 +1795,7 @@ flowchart TD | [`tool-jobs`](../packages/jobs/tool-jobs) | `jobs` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`tool-lsp`](../packages/lsp/tool-lsp) | `lsp` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | | [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | +| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`agent`](../packages/core/agent), [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) | | [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy) | `session` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) | | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | `session` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`command-feedback`](../packages/feedback/command-feedback), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry) | @@ -1809,37 +1809,41 @@ flowchart TD | [`tool-terminal`](../packages/terminal/tool-terminal) | `terminal` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`system-prompt`](../packages/core/system-prompt), [`terminal`](../packages/terminal/terminal), [`tools`](../packages/core/tools) | | [`agent-loop-testkit`](../packages/test-support/agent-loop-testkit) | `test-support` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`llm-replay`](../packages/test-support/llm-replay) | `test-support` | [`compaction`](../packages/compaction/compaction), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | -| [`webhook`](../packages/webhook/webhook) | `webhook` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) | | [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | +| [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | `llm` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | +| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | +| [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-title`](../packages/session/session-title), [`tool-todo`](../packages/todo/tool-todo) | +| [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`mcp-client`](../packages/mcp/mcp-client), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`token-meter`](../packages/llm/token-meter), [`user-approval`](../packages/interaction/user-approval) | +| [`web-app`](../packages/bundle/web-app) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt) | +| [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | +| [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | +| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`invariants`](../packages/runtime-diagnostics/invariants) | +| [`tool-bash`](../packages/shell/tool-bash) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | +| [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | +| [`webhook`](../packages/webhook/webhook) | `webhook` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) | | [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | +| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | | [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`hooks-claude-code`](../packages/hooks/hooks-claude-code) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | -| [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-title`](../packages/session/session-title), [`tool-todo`](../packages/todo/tool-todo) | -| [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`mcp-client`](../packages/mcp/mcp-client), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`token-meter`](../packages/llm/token-meter), [`user-approval`](../packages/interaction/user-approval) | -| [`web-app`](../packages/bundle/web-app) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt) | +| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) | +| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | | [`client-connection`](../packages/client/connection) | `client` | [`attachment`](../packages/attachment/attachment), [`commands`](../packages/interaction/commands), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tool-todo`](../packages/todo/tool-todo) | -| [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | +| [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | +| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`typert-protocol`](../packages/typert/protocol) | +| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs-local`](../packages/jobs/jobs-local), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`shell-env`](../packages/shell/shell-env), [`skill`](../packages/skill/skill), [`skill-filesystem`](../packages/skill/skill-filesystem), [`system-prompt`](../packages/core/system-prompt), [`tool-bash`](../packages/shell/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-skill`](../packages/skill/tool-skill), [`tools`](../packages/core/tools) | | [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent) | -| [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | +| [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-modules`](../packages/client/modules), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | -| [`tool-bash`](../packages/shell/tool-bash) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | -| [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | [`webhook-github`](../packages/webhook/webhook-github) | `webhook` | [`credentials`](../packages/credentials/credentials), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`webhook`](../packages/webhook/webhook) | | [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`workflow-worker-thread`](../packages/workflow/workflow-worker-thread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | -| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) | -| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | | [`api-gateway`](../packages/api/gateway) | `api` | [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-registry`](../packages/typert/registry) | -| [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | -| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`typert-protocol`](../packages/typert/protocol) | -| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs-local`](../packages/jobs/jobs-local), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`shell-env`](../packages/shell/shell-env), [`skill`](../packages/skill/skill), [`skill-filesystem`](../packages/skill/skill-filesystem), [`system-prompt`](../packages/core/system-prompt), [`tool-bash`](../packages/shell/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-skill`](../packages/skill/tool-skill), [`tools`](../packages/core/tools) | | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | `experimental` | [`agent`](../packages/core/agent), [`experimental-agent-team`](../packages/experimental/agent-team), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) | | [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index 06c60504dc..aad7db87f6 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/persistence-catalog.md -persistence-catalog.md: 5155968af0886a389d0b01d9332af927cff95a55 -persistence-catalog.zh.md: abd4ae767a5cfca45be76b54d392815244070869 +persistence-catalog.md: dd2124520e43e590fc3506b23c533b3e482132cd +persistence-catalog.zh.md: 48c0867f37fc87ce5d29ce04b0b6970fca83648c diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 5155968af0..dd2124520e 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -726,7 +726,23 @@ Source: [`packages/core/session/src/types.ts:237`](../packages/core/session/src/ 'subagent/descriptor': SubagentDescriptorData ``` -Source: [`packages/subagent/subagent/src/descriptor.ts:37`](../packages/subagent/subagent/src/descriptor.ts) +Source: [`packages/subagent/subagent/src/descriptor.ts:38`](../packages/subagent/subagent/src/descriptor.ts) + + + +#### `subagent/model-selection-enabled` — log-only + +```ts persistence-catalog +/** + * Records that this session's delegation tool exposes child provider, + * model, and reasoning-effort selection. Appended before the first model + * request; absence means the fixed-route definition. Log-only: it carries + * no `surfaceOp` and never enters model history. + */ +'subagent/model-selection-enabled': Record +``` + +Source: [`packages/subagent/tool-subagent/src/model-selection-state.ts:13`](../packages/subagent/tool-subagent/src/model-selection-state.ts) ### `team/*` diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index abd4ae767a..48c0867f37 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -728,7 +728,23 @@ export type SessionEvent = { 'subagent/descriptor': SubagentDescriptorData ``` -来源:[`packages/subagent/subagent/src/descriptor.ts:37`](../packages/subagent/subagent/src/descriptor.ts) +来源:[`packages/subagent/subagent/src/descriptor.ts:38`](../packages/subagent/subagent/src/descriptor.ts) + + + +#### `subagent/model-selection-enabled` — log-only + +```ts persistence-catalog +/** + * Records that this session's delegation tool exposes child provider, + * model, and reasoning-effort selection. Appended before the first model + * request; absence means the fixed-route definition. Log-only: it carries + * no `surfaceOp` and never enters model history. + */ +'subagent/model-selection-enabled': Record +``` + +来源:[`packages/subagent/tool-subagent/src/model-selection-state.ts:13`](../packages/subagent/tool-subagent/src/model-selection-state.ts) ### `team/*` diff --git a/docs/subsystems/attachment.i18n.yaml b/docs/subsystems/attachment.i18n.yaml index bbb4e4b38f..8e69eaae28 100644 --- a/docs/subsystems/attachment.i18n.yaml +++ b/docs/subsystems/attachment.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/attachment.md -attachment.md: 7fafae424902632d8190e5d156b72795c31dcbbc -attachment.zh.md: 2eed9515a82fa8bba290a5d07f41e521dd7565d8 +attachment.md: 15daa2b8d541ba847d48f5c43a06c1c537df6d11 +attachment.zh.md: c74a18f6bec63e117afcdb141512be04f8d75f9a diff --git a/docs/subsystems/attachment.md b/docs/subsystems/attachment.md index 7fafae4249..15daa2b8d5 100644 --- a/docs/subsystems/attachment.md +++ b/docs/subsystems/attachment.md @@ -98,7 +98,7 @@ interface StoredImageAttachment { interface ImageRequestPolicy { /** Maximum width multiplied by height after aspect-preserving projection. */ maxPixels: number - /** Encoded-byte cap before base64 expansion or Files API upload. */ + /** Encoded-byte target before base64 expansion or Files API upload; the smallest quality-ladder output is kept when no quality fits. */ maxBytes: number } ``` @@ -187,7 +187,7 @@ imageHostPath(ref: ImageAttachmentRef): string | undefined /** * Generate or read one deterministic model-request version from the stored normalized image. * @param ref - durable provider-independent normalized attachment reference. - * @param policy - exact route pixel and encoded-byte budget. + * @param policy - exact route pixel budget and encoded-byte target; a target no ladder quality meets yields the smallest ladder output. * @param signal - optional cancellation. * @returns request bytes and the cache/upload identity covering every transform input. */ diff --git a/docs/subsystems/attachment.zh.md b/docs/subsystems/attachment.zh.md index 2eed9515a8..c74a18f6be 100644 --- a/docs/subsystems/attachment.zh.md +++ b/docs/subsystems/attachment.zh.md @@ -98,7 +98,7 @@ interface StoredImageAttachment { interface ImageRequestPolicy { /** Maximum width multiplied by height after aspect-preserving projection. */ maxPixels: number - /** Encoded-byte cap before base64 expansion or Files API upload. */ + /** Encoded-byte target before base64 expansion or Files API upload; the smallest quality-ladder output is kept when no quality fits. */ maxBytes: number } ``` @@ -187,7 +187,7 @@ imageHostPath(ref: ImageAttachmentRef): string | undefined /** * Generate or read one deterministic model-request version from the stored normalized image. * @param ref - durable provider-independent normalized attachment reference. - * @param policy - exact route pixel and encoded-byte budget. + * @param policy - exact route pixel budget and encoded-byte target; a target no ladder quality meets yields the smallest ladder output. * @param signal - optional cancellation. * @returns request bytes and the cache/upload identity covering every transform input. */ diff --git a/docs/subsystems/core.i18n.yaml b/docs/subsystems/core.i18n.yaml index 9174e27772..fb5dd11fbb 100644 --- a/docs/subsystems/core.i18n.yaml +++ b/docs/subsystems/core.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/core.md -core.md: 3417560539f41009a290f4c31b255f338624d56d -core.zh.md: 75f3c0ca16e1235cb2155f6e54e86e414e61b498 +core.md: c53bb94fa5918c3a91ee9aedbb2416d0b101b240 +core.zh.md: 93ee45fb88cc100eb77673f2b70e86483c7ed29f diff --git a/docs/subsystems/core.md b/docs/subsystems/core.md index 3417560539..c53bb94fa5 100644 --- a/docs/subsystems/core.md +++ b/docs/subsystems/core.md @@ -161,12 +161,14 @@ interface AgentOptions { provider?: string /** Model id interpreted by the selected provider adapter. */ model?: string + /** Adapter-owned reasoning effort for the selected provider/model route. */ + reasoningEffort?: ReasoningEffortId /** Maximum output tokens for each conversation-model request. */ maxTokens?: number } ``` -Dispatch requires `provider` and `model` after `agent/request`. When present, `maxTokens` must be a positive safe integer and caps every conversation-model request; omission allows the exact-model adapter default to materialize before the request header, or otherwise leaves provider behavior unchanged. An agent-scoped `deployment:persona` prompt section may shadow the global default persona. +Dispatch requires `provider` and `model` after `agent/request`. An explicit `reasoningEffort` seeds the first request on that route; exact-model resolution validates it, while omission allows the adapter default to materialize. When present, `maxTokens` must be a positive safe integer and caps every conversation-model request; omission allows the exact-model adapter default to materialize before the request header, or otherwise leaves provider behavior unchanged. An agent-scoped `deployment:persona` prompt section may shadow the global default persona. The inbox is the delivery vocabulary — two ordered pending-message lists the agent owns as a durable projection: @@ -524,7 +526,9 @@ serviceFor(agent: { ctx: Context }, name: K): * state to restore. The re-link runs through the binding this roster kept * from the agent's mount — dsh-scope's only re-link authority. An agent * that never composed one has nothing to re-link: the switch is then the - * agent's first bind, exactly a mount. + * agent's first bind, exactly a mount. A committed re-link emits + * `tools/change` because changing the parent scope changes the Agent's + * resolved tool set without adding or removing registry entries. * @param agentCtx - the agent's scope context. * @param id - the preset to compose the agent from instead. * @returns the preset now installed. diff --git a/docs/subsystems/core.zh.md b/docs/subsystems/core.zh.md index 75f3c0ca16..93ee45fb88 100644 --- a/docs/subsystems/core.zh.md +++ b/docs/subsystems/core.zh.md @@ -165,12 +165,14 @@ interface AgentOptions { provider?: string /** Model id interpreted by the selected provider adapter. */ model?: string + /** Adapter-owned reasoning effort for the selected provider/model route. */ + reasoningEffort?: ReasoningEffortId /** Maximum output tokens for each conversation-model request. */ maxTokens?: number } ``` -在 `agent/request` 之后,分发要求 `provider` 与 `model` 都存在。提供 `maxTokens` 时,它必须是正安全整数,并限制每次对话模型请求的输出;省略时,系统会在写入请求 header 前填入确切模型的适配器默认值,否则提供方行为保持不变。agent 作用域的 `deployment:persona` 提示词段落可以遮蔽全局默认 persona。 +在 `agent/request` 之后,分发要求 `provider` 与 `model` 都存在。显式 `reasoningEffort` 会为该路由的首次请求提供初始值;确切模型解析会校验该值,省略时则允许填入适配器默认值。提供 `maxTokens` 时,它必须是正安全整数,并限制每次对话模型请求的输出;省略时,系统会在写入请求 header 前填入确切模型的适配器默认值,否则提供方行为保持不变。agent 作用域的 `deployment:persona` 提示词段落可以遮蔽全局默认 persona。 inbox 即投递词汇——agent 以持久投影形式拥有的两条有序待处理消息列表: @@ -534,7 +536,9 @@ serviceFor(agent: { ctx: Context }, name: K): * state to restore. The re-link runs through the binding this roster kept * from the agent's mount — dsh-scope's only re-link authority. An agent * that never composed one has nothing to re-link: the switch is then the - * agent's first bind, exactly a mount. + * agent's first bind, exactly a mount. A committed re-link emits + * `tools/change` because changing the parent scope changes the Agent's + * resolved tool set without adding or removing registry entries. * @param agentCtx - the agent's scope context. * @param id - the preset to compose the agent from instead. * @returns the preset now installed. diff --git a/docs/subsystems/subagent.i18n.yaml b/docs/subsystems/subagent.i18n.yaml index ac6150111a..cd3f9d99c9 100644 --- a/docs/subsystems/subagent.i18n.yaml +++ b/docs/subsystems/subagent.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/subagent.md -subagent.md: 7a0ba28afbbec88465415b3411946a0afa0da918 -subagent.zh.md: f397c19a0dc79c0c5a94e617e2013b68faa4d2e9 +subagent.md: 63edef0a4b8d5368ea9d6d82f0ea3ef99ea0bbad +subagent.zh.md: 21b0dfd21dbee5e1d37558d6c02fe7949126d9a9 diff --git a/docs/subsystems/subagent.md b/docs/subsystems/subagent.md index 7a0ba28afb..63edef0a4b 100644 --- a/docs/subsystems/subagent.md +++ b/docs/subsystems/subagent.md @@ -25,6 +25,7 @@ A provider advertises its **start-time** features on a static descriptor the ser * to `maxDepth`; the other names match. */ interface SubagentCapabilities { + readonly agentOptions: boolean readonly outputSchema: boolean readonly depthLimit: boolean readonly toolFilter: boolean @@ -34,7 +35,7 @@ interface SubagentCapabilities { ## The one-shot start request -The tool layer builds this request from the model input and its own config; the service validates it against the named provider before `start`. Required `parent` supplies the session cwd, lineage, and delegation depth. Optional output schema, depth, tool filter, and persona require matching capability flags. Unsupported schemas fail at start; in-process backends scope filters and personas to child creation and implement the supported object-rooted schema with a forced capture tool. +The tool layer builds this request from the model input and its own config; the service validates it against the named provider before `start`. Required `parent` supplies the session cwd, lineage, and delegation depth. Optional Agent provider, model, reasoning-effort, and token overrides, output schema, depth, tool filter, and persona require matching capability flags. In-process backends merge `agentOptions` over the parent Agent's options, scope filters and personas to child creation, and implement the supported object-rooted schema with a forced capture tool. Current out-of-process providers reject `agentOptions` before starting their transport. ```ts type-equiv /** @@ -63,6 +64,12 @@ interface SubagentStartRequest { * remaining turn work when it fires afterward. */ readonly signal: AbortSignal + /** + * Optional host-Agent provider, model, reasoning-effort, and output-token + * overrides. Requires {@link SubagentCapabilities.agentOptions}; in-process + * providers merge them over the parent Agent's options when they create the + * child. + */ readonly agentOptions?: AgentOptions /** * Object-rooted JSON Schema within `assertObjectJsonSchema`'s enforced subset. Start rejects @@ -280,7 +287,7 @@ interface ContinuableCreateSpec { } ``` -The descriptor (`SubagentDescriptorData` in [descriptor.ts](../../packages/subagent/subagent/src/descriptor.ts)) is a mode-discriminated durable identity for every session-backed subagent. Both modes carry the provider name. A `one-shot` descriptor optionally carries a caller-owned display `label`; a `continuable` descriptor requires the delegation `description` as its durable creation label and additionally snapshots resolved child `agentOptions.provider`/`model` and optional `persona`/`toolFilter` for cold resume. It never snapshots the merge-extensible `AgentOptions` object, so an unrelated extension value cannot break continuation and a later composition input is a deliberate version change. It omits `subagentDepth` (cold resume trusts the persisted header's `delegationDepth` as the monotone floor) and `outputSchema` (one run or Activation's result contract, not durable identity). +The descriptor (`SubagentDescriptorData` in [descriptor.ts](../../packages/subagent/subagent/src/descriptor.ts)) is a mode-discriminated durable identity for every session-backed subagent. Both modes carry the provider name. A `one-shot` descriptor optionally carries a caller-owned display `label`; a `continuable` descriptor requires the delegation `description` as its durable creation label and additionally snapshots resolved child `agentOptions.provider`/`model`/`reasoningEffort` and optional `persona`/`toolFilter` for cold resume. It never snapshots the merge-extensible `AgentOptions` object, so an unrelated extension value cannot break continuation and a later composition input is a deliberate version change. It omits `subagentDepth` (cold resume trusts the persisted header's `delegationDepth` as the monotone floor) and `outputSchema` (one run or Activation's result contract, not durable identity). A local one-shot provider appends the descriptor inside the child's initial turn before its first request. The continuation manager appends the descriptor after any provider-supplied lineage and before the initial prompt is admitted; `header.seedLength` remains the fork-lineage boundary: resume-time descriptor authority reads the child's own suffix, while the list-serving identity projection folds `subagent/descriptor` last-wins so the child's own descriptor overrides a fork-seeded ancestor's. The event is log-only: no `surfaceOp`, never in model history, and retained across compaction by the append-only log. Malformed current-version descriptors are corrupt; unsupported versions cannot be classified by this runtime. @@ -482,6 +489,22 @@ The spawn and fork backends create an ordinary one-shot agent through `parent.ct Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + +### `ctx.subagentModelSelection` — `SubagentModelSelectionConfig` + +Singleton settings owner read by delegation tools when an Agent is published. + +```ts cordis-catalog +/** + * Read the preference for the next eligible Agent publication. + * @returns whether that Agent should receive model-selectable delegation. + */ +currentEnabled(): boolean +``` + +Source: [`packages/subagent/tool-subagent/src/model-selection-settings.ts`](../../packages/subagent/tool-subagent/src/model-selection-settings.ts) + ### `ctx.subagents` — `SubagentRuntime` diff --git a/docs/subsystems/subagent.zh.md b/docs/subsystems/subagent.zh.md index f397c19a0d..21b0dfd21d 100644 --- a/docs/subsystems/subagent.zh.md +++ b/docs/subsystems/subagent.zh.md @@ -25,6 +25,7 @@ Service Definition:[dsh-subagent](../../packages/subagent/subagent)(`ctx.sub * to `maxDepth`; the other names match. */ interface SubagentCapabilities { + readonly agentOptions: boolean readonly outputSchema: boolean readonly depthLimit: boolean readonly toolFilter: boolean @@ -34,7 +35,7 @@ interface SubagentCapabilities { ## 单次启动请求 -工具层根据模型输入和自身配置构建此请求;服务在 `start` 之前针对指定提供方进行校验。必填的 `parent` 提供会话 cwd、谱系与委派深度。可选的 output schema、depth、工具过滤器和 persona 需要对应的能力 flag 匹配。不支持的 schema 在启动时即失败;进程内后端将 filter 和 persona 的作用域限定在子 agent 创建阶段,并通过强制 capture 工具实现所支持的 object-rooted schema。 +工具层根据模型输入和自身配置构建此请求;服务在 `start` 之前针对指定提供方进行校验。必填的 `parent` 提供会话 cwd、谱系与委派深度。可选的 Agent 提供方、模型、推理强度与 token 覆盖、output schema、depth、工具过滤器和 persona 需要对应的能力 flag 匹配。进程内后端会把 `agentOptions` 合并到父 Agent 选项之上,将 filter 和 persona 的作用域限定在子 agent 创建阶段,并通过强制 capture 工具实现所支持的 object-rooted schema。当前进程外提供方会在启动其传输前拒绝 `agentOptions`。 ```ts type-equiv /** @@ -63,6 +64,12 @@ interface SubagentStartRequest { * remaining turn work when it fires afterward. */ readonly signal: AbortSignal + /** + * Optional host-Agent provider, model, reasoning-effort, and output-token + * overrides. Requires {@link SubagentCapabilities.agentOptions}; in-process + * providers merge them over the parent Agent's options when they create the + * child. + */ readonly agentOptions?: AgentOptions /** * Object-rooted JSON Schema within `assertObjectJsonSchema`'s enforced subset. Start rejects @@ -280,7 +287,7 @@ interface ContinuableCreateSpec { } ``` -描述符([descriptor.ts](../../packages/subagent/subagent/src/descriptor.ts) 中的 `SubagentDescriptorData`)是每个由会话支撑的 subagent 所使用、按模式判别的持久化身份。两种模式都携带提供方名称。`one-shot` 描述符可以携带调用方拥有的可选显示 `label`;`continuable` 描述符要求以委派 `description` 作为持久化创建标签,并另外对已解析的子 agent `agentOptions.provider`/`model` 与可选的 `persona`/`toolFilter` 建立快照,用于冷恢复。它绝不会对可合并扩展的 `AgentOptions` 对象建立快照,因此无关的扩展值不会破坏继续执行,后续新增组合配置输入则是一次有意的版本更改。描述符省略 `subagentDepth`(冷恢复以持久化 header 中的 `delegationDepth` 作为单调下界)和 `outputSchema`(单次运行或 Activation 的结果约定,而非持久化身份)。 +描述符([descriptor.ts](../../packages/subagent/subagent/src/descriptor.ts) 中的 `SubagentDescriptorData`)是每个由会话支撑的 subagent 所使用、按模式判别的持久化身份。两种模式都携带提供方名称。`one-shot` 描述符可以携带调用方拥有的可选显示 `label`;`continuable` 描述符要求以委派 `description` 作为持久化创建标签,并另外对已解析的子 agent `agentOptions.provider`/`model`/`reasoningEffort` 与可选的 `persona`/`toolFilter` 建立快照,用于冷恢复。它绝不会对可合并扩展的 `AgentOptions` 对象建立快照,因此无关的扩展值不会破坏继续执行,后续新增组合配置输入则是一次有意的版本更改。描述符省略 `subagentDepth`(冷恢复以持久化 header 中的 `delegationDepth` 作为单调下界)和 `outputSchema`(单次运行或 Activation 的结果约定,而非持久化身份)。 本地一次性提供方会在子 agent 的初始轮次内、首次请求前追加描述符。继续执行管理器会在任何提供方提供的谱系之后、初始提示词获准之前追加描述符;`header.seedLength` 仍是 fork 谱系边界:恢复时的描述符权威读取子 agent 自身的后缀,而供列表使用的身份投影以 last-wins 折叠 `subagent/descriptor`,子 agent 自己的描述符会覆盖 fork seed 中祖先的描述符。该事件只进入日志:不含 `surfaceOp`,绝不进入模型历史,并由仅追加日志跨压缩保留。格式错误的当前版本描述符属于损坏;本运行时无法对不受支持的版本进行分类。 @@ -486,6 +493,22 @@ spawn 和 fork 后端通过 `parent.ctx` 创建一个普通的单次 agent,将 Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.zh.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + +### `ctx.subagentModelSelection` — `SubagentModelSelectionConfig` + +Singleton settings owner read by delegation tools when an Agent is published. + +```ts cordis-catalog +/** + * Read the preference for the next eligible Agent publication. + * @returns whether that Agent should receive model-selectable delegation. + */ +currentEnabled(): boolean +``` + +Source: [`packages/subagent/tool-subagent/src/model-selection-settings.ts`](../../packages/subagent/tool-subagent/src/model-selection-settings.ts) + ### `ctx.subagents` — `SubagentRuntime` diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml index 10447d6107..38ad384da8 100644 --- a/docs/tool-catalog.i18n.yaml +++ b/docs/tool-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/tool-catalog.md -tool-catalog.md: 1fa650f1e4e025274d069f27a6522abff46af2e2 -tool-catalog.zh.md: c3209e7007e9cf05770ccee0698f9e98a32e8363 +tool-catalog.md: 0cd8560a6851f0195e2272d1bd3b0bec2c171ac4 +tool-catalog.zh.md: cb225bc11afa6a6b022f2c7c104d4e1286f89260 diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index 1fa650f1e4..0cd8560a68 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -33,7 +33,7 @@ This table connects model-visible tool names to the plugin package and service s | `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`, `ctx.workflowEngine`, `ctx.subagents`, `ctx.systemPrompt`, `a calling Agent (exec.agent parents every fresh round)` | `tool/call`, `tool/result`, `workflow and child session events during execution` | - | A fixed foreground workflow starts one fresh structured child per round; the model selects only the immutable objective and an optional round cap. | | `@deepseek-ai/dsh-tool-skill` | `skill` | `ctx.tools`, `ctx.agents`, `ctx.skills` | `tool/call`, `tool/result`, `user/message replacement catalogs via agent.inject()` | - | - | | `@deepseek-ai/dsh-tool-session-query` | `session_event_read`, `session_event_search`, `session_event_trace`, `session_search`, `session_trace` | `ctx.tools`, `ctx.systemPrompt`, `ctx.sessionQuery`, `a calling Agent for workspace authority` | `tool/call`, `tool/result` | - | The five read-only tools hide provider cursors and authorize every result from the immutable calling agent session. The package is opt-in; compositions that need enforced deadlines or bounded inline output also mount the generic timeout or spill policies. | -| `@deepseek-ai/dsh-tool-subagent` | `subagent` | `ctx.tools`, `ctx.subagents`, `ctx.systemPrompt` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped compositions load this package once per subagent backend, so the model additionally sees `subagent_fork` bound to the fork backend. Each instance's description, `run_in_background` parameter, and system-prompt policy follow its own `backgroundMode` and `enableRunInBackground`, so the two shipped schemas are not identical: `subagent` is `continuable` and defaults omitted calls to background with automatic settlement delivery, while `subagent_fork` stays `one-shot` and defaults them to foreground — see `packages/bundle/base/cordis.patch.yml` and `examples/acp-agent/cordis.yml`. | +| `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`, `subagent` | `ctx.tools`, `ctx.subagents`, `ctx.systemPrompt`, `ctx.llm for model discovery and selected-route validation` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The registered delegation name is the load-time `toolName` config (default `subagent`); the schema above shows static model selection enabled for reference. Model selection defaults off. Web presets sample the default-off Models preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` remains fixed-route. Explicit compositions may instead use static `enableModelSelection`. Each instance independently controls model selection, discovery ownership, and background behavior through `enableModelSelection`, `modelSelectionSettings`, `backgroundMode`, and `enableRunInBackground`. | | `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`, `list_agents`, `send_message` | `ctx.tools`, `ctx.subagents`, `ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`, `tool/result`, `child session events through ctx.subagents` | - | The globally named control tools over continuable background subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` and `interrupt_agent` once, plus `list_agents` from its separately loaded `/list-agents` plugin (whose catalog rows use the sessionProjections and live Agent registries). | | `@deepseek-ai/dsh-tool-subagent-report` | `report` | `ctx.subagents`, `ctx.systemPrompt`, `a live continuable in-process child Agent` | `tool/call`, `tool/result`, `a user-role message in the direct parent session` | - | Registered per continuable in-process child rather than globally, so this schema is visible only inside such a child and survives its global `toolFilter`. The same contribution installs the child-scoped `tool:report` prompt section, which this catalog does not render. The parent-facing `send_message` tool is installed independently. | | `@deepseek-ai/dsh-tool-jobs` | `job_kill`, `job_list`, `job_output` | `ctx.tools`, `ctx.jobs`, `ctx.systemPrompt` | `tool/call`, `tool/result`, `user/message via agent.inject() for background completion notices` | - | The kind-agnostic background-job controller: background bash commands, PTY sends, and subagents are read, listed, and killed through the same three tools. Loading the plugin attaches the controller that arms producers' `ctx.jobs.start()`. | @@ -1501,9 +1501,31 @@ The five read-only tools hide provider cursors and authorize every result from t ## `@deepseek-ai/dsh-tool-subagent` +### `list_subagent_models` + +Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. + +```json +{ + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's advertised models." + } + } +} +``` + +Source: [`packages/subagent/tool-subagent/src/list-models.ts`](../packages/subagent/tool-subagent/src/list-models.ts) + ### `subagent` -Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This call waits for the result by default. Set `run_in_background: true` to return a job id; collect with `job_output` and stop with `job_kill`. +Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This call waits for the result by default. Set `run_in_background: true` to return a job id; collect with `job_output` and stop with `job_kill`. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. ```json { @@ -1517,6 +1539,18 @@ Delegate a self-contained task to a subagent (a separate agent that works in its "type": "string", "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs." }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + }, "run_in_background": { "type": "boolean", "description": "Whether to run as a background job and return its id. Defaults to false; collect with job_output or stop with job_kill." @@ -1531,7 +1565,7 @@ Delegate a self-contained task to a subagent (a separate agent that works in its Source: [`packages/subagent/tool-subagent/src/index.ts`](../packages/subagent/tool-subagent/src/index.ts) -The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped compositions load this package once per subagent backend, so the model additionally sees `subagent_fork` bound to the fork backend. Each instance's description, `run_in_background` parameter, and system-prompt policy follow its own `backgroundMode` and `enableRunInBackground`, so the two shipped schemas are not identical: `subagent` is `continuable` and defaults omitted calls to background with automatic settlement delivery, while `subagent_fork` stays `one-shot` and defaults them to foreground — see `packages/bundle/base/cordis.patch.yml` and `examples/acp-agent/cordis.yml`. +The registered delegation name is the load-time `toolName` config (default `subagent`); the schema above shows static model selection enabled for reference. Model selection defaults off. Web presets sample the default-off Models preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` remains fixed-route. Explicit compositions may instead use static `enableModelSelection`. Each instance independently controls model selection, discovery ownership, and background behavior through `enableModelSelection`, `modelSelectionSettings`, `backgroundMode`, and `enableRunInBackground`. diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md index c3209e7007..cb225bc11a 100644 --- a/docs/tool-catalog.zh.md +++ b/docs/tool-catalog.zh.md @@ -37,7 +37,7 @@ | `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`、`ctx.workflowEngine`、`ctx.subagents`、`ctx.systemPrompt`、`a calling Agent (exec.agent parents every fresh round)` | `tool/call`、`tool/result`、`workflow and child session events during execution` | - | 固定的前台工作流会在每个 Round 启动一个全新的结构化子级;模型只能选择不可变目标和可选的 Round 上限。 | | `@deepseek-ai/dsh-tool-skill` | `skill` | `ctx.tools`、`ctx.agents`、`ctx.skills` | `tool/call`、`tool/result`、`user/message replacement catalogs via agent.inject()` | - | - | | `@deepseek-ai/dsh-tool-session-query` | `session_event_read`、`session_event_search`、`session_event_trace`、`session_search`、`session_trace` | `ctx.tools`、`ctx.systemPrompt`、`ctx.sessionQuery`、`a calling Agent for workspace authority` | `tool/call`、`tool/result` | - | 这 5 个只读工具会隐藏提供方游标,并根据不可变的调用 agent 会话为每个结果授权。该包需要选择启用;需要强制截止时间或限制行内输出的组合还会挂载通用超时或 spill 策略。 | -| `@deepseek-ai/dsh-tool-subagent` | `subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 注册的工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述 schema 对应默认值。随产品发布的组合会为每个 subagent 后端加载一次该包,因此模型还会看到绑定到 fork 后端的 `subagent_fork`。每个实例的描述、`run_in_background` 参数与 system prompt 策略取决于它自己的 `backgroundMode` 和 `enableRunInBackground`,因此两个随附 schema 并不相同:`subagent` 为 `continuable`,省略参数时默认后台运行,并由 runtime 自动投递结束结果;`subagent_fork` 保持 `one-shot`,省略参数时默认前台运行。详见 `packages/bundle/base/cordis.patch.yml` 和 `examples/acp-agent/cordis.yml`。 | +| `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述 schema 以静态启用模型选择作为参考。模型选择默认为关闭。Web preset 会在每个新顶层 Session 创建时读取 Models 页中默认关闭的偏好,并为其子 Session 保留该决定;`subagent_fork` 始终使用固定路由。显式组合也可以改用静态 `enableModelSelection`。每个实例通过 `enableModelSelection`、`modelSelectionSettings`、`backgroundMode` 与 `enableRunInBackground` 独立控制模型选择、发现工具持有权和后台行为。 | | `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`、`list_agents`、`send_message` | `ctx.tools`、`ctx.subagents`、`ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`、`tool/result`、`child session events through ctx.subagents` | - | 这些是控制可继续后台 subagent 的全局命名工具:绑定提供方的 `tool-subagent` 实例注册不同的委派工具;本包注册一次 `send_message` 和 `interrupt_agent`,另由 `list_agents` 通过单独加载的 `/list-agents` 插件提供,其目录行使用 sessionProjections 和实时 Agent 注册表。 | | `@deepseek-ai/dsh-tool-subagent-report` | `report` | `ctx.subagents`、`ctx.systemPrompt`、`a live continuable in-process child Agent` | `tool/call`、`tool/result`、`a user-role message in the direct parent session` | - | 按可继续的进程内子级注册,而非全局注册,因此该 schema 仅在这种子级内部可见,并且不受其全局 `toolFilter` 影响。同一份贡献还会安装子级作用域的 `tool:report` 系统提示词 section,本目录不渲染该 section。面向父级的 `send_message` 工具单独安装。 | | `@deepseek-ai/dsh-tool-jobs` | `job_kill`、`job_list`、`job_output` | `ctx.tools`、`ctx.jobs`、`ctx.systemPrompt` | `tool/call`、`tool/result`、`user/message via agent.inject() for background completion notices` | - | 与任务种类无关的后台任务控制器:后台 bash 命令、PTY 发送和 subagent 都通过相同的 3 个工具读取、列出和终止。加载该插件会挂接控制器,从而启用生产方的 `ctx.jobs.start()`。 | @@ -1507,9 +1507,31 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, ## `@deepseek-ai/dsh-tool-subagent` +### `list_subagent_models` + +发现 subagent 可用的 LLM 路由,不更改当前 Agent。无参数调用会列出已注册提供方;提供 `provider` 时会列出其公布的模型;同时提供 `provider` 和 `model` 时会检查该精确模型及其推理强度。目录条目只提供建议:adapter 可能接受未列出的模型 id。把返回的 id 用于委派工具的 `provider`、`model` 与 `reasoning_effort` 字段。 + +```json +{ + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's advertised models." + } + } +} +``` + +来源:[`packages/subagent/tool-subagent/src/list-models.ts`](../packages/subagent/tool-subagent/src/list-models.ts) + ### `subagent` -将一项自包含任务委派给 subagent(在自身上下文中工作的独立 agent),用它卸载聚焦且独立的工作,例如研究、限定范围的实现或分析,以免消耗当前对话的上下文。subagent 会返回结果,但不会返回中间步骤。请提供完整、独立的提示词,因为它看不到当前对话。此调用默认等待结果。设置 `run_in_background: true` 可返回 job id;使用 `job_output` 收集结果,使用 `job_kill` 停止任务。 +将一项自包含任务委派给 subagent(在自身上下文中工作的独立 agent),用它卸载聚焦且独立的工作,例如研究、限定范围的实现或分析,以免消耗当前对话的上下文。subagent 会返回结果,但不会返回中间步骤。请提供完整、独立的提示词,因为它看不到当前对话。此调用默认等待结果。设置 `run_in_background: true` 可返回 job id;使用 `job_output` 收集结果,使用 `job_kill` 停止任务。子级 LLM 选择是可选的。省略 `provider`、`model` 与 `reasoning_effort` 会使用配置的子级默认值,并从父 Agent 继承兼容的缺失值。先用 `list_subagent_models` 检查公布的路由和强度,再一起提供 `provider` 与 `model`。改变生效路由但不指定强度时,会使用所选模型的默认强度。 ```json { @@ -1523,6 +1545,18 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, "type": "string", "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs." }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + }, "run_in_background": { "type": "boolean", "description": "Whether to run as a background job and return its id. Defaults to false; collect with job_output or stop with job_kill." @@ -1537,7 +1571,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, 来源:[`packages/subagent/tool-subagent/src/index.ts`](../packages/subagent/tool-subagent/src/index.ts) -注册的工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述 schema 对应默认值。随产品发布的组合会为每个 subagent 后端加载一次该包,因此模型还会看到绑定到 fork 后端的 `subagent_fork`。每个实例的描述、`run_in_background` 参数与 system prompt 策略取决于它自己的 `backgroundMode` 和 `enableRunInBackground`,因此两个随附 schema 并不相同:`subagent` 为 `continuable`,省略参数时默认后台运行,并由 runtime 自动投递结束结果;`subagent_fork` 保持 `one-shot`,省略参数时默认前台运行。详见 `packages/bundle/base/cordis.patch.yml` 和 `examples/acp-agent/cordis.yml`。 +注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述 schema 以静态启用模型选择作为参考。模型选择默认为关闭。Web preset 会在每个新顶层 Session 创建时读取 Models 页中默认关闭的偏好,并为其子 Session 保留该决定;`subagent_fork` 始终使用固定路由。显式组合也可以改用静态 `enableModelSelection`。每个实例通过 `enableModelSelection`、`modelSelectionSettings`、`backgroundMode` 与 `enableRunInBackground` 独立控制模型选择、发现工具持有权和后台行为。 diff --git a/docs/user/guide/python-sdk.i18n.yaml b/docs/user/guide/python-sdk.i18n.yaml index f2299b6512..4ddf1a6015 100644 --- a/docs/user/guide/python-sdk.i18n.yaml +++ b/docs/user/guide/python-sdk.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/user/guide/python-sdk.md -python-sdk.md: 21c7a908a8524b16baf8f98453746f59b5d5efc8 -python-sdk.zh.md: bb883f6f3799225bda590a80883063ecb29e15b4 +python-sdk.md: 24f5594a20eab6870d9725e0f1acfce5dff62f74 +python-sdk.zh.md: 47b420ab20df04d28e5498807dc73a425c7a9666 diff --git a/docs/user/guide/python-sdk.md b/docs/user/guide/python-sdk.md index 21c7a908a8..24f5594a20 100644 --- a/docs/user/guide/python-sdk.md +++ b/docs/user/guide/python-sdk.md @@ -2,7 +2,7 @@ English | [中文](python-sdk.zh.md) -This tutorial is the programmatic alternative to the Web UI. It installs the published Python SDK, runs a checked-in agent composition, and shows how to call the same API from your own program. +This tutorial installs the published Python SDK, runs the shipped standalone minimal profile, and shows how to customize the same `dsh` profile from your own program. ## Prerequisites @@ -10,12 +10,10 @@ This tutorial is the programmatic alternative to the Web UI. It installs the pub - Git - Linux x64, Linux arm64, or macOS 14 or newer on arm64 - A DeepSeek-compatible API endpoint and credential -- An isolated workspace that the agent may modify +- An isolated workspace and an isolated Harness home ## Install the SDK -Clone the repository for its runnable example, create a virtual environment, and install the SDK with its same-version bundled runtime: - ```sh git clone https://github.com/deepseek-ai/deepseek-harness.git cd deepseek-harness @@ -24,51 +22,45 @@ python -m venv .venv python -m pip install deepseek-harness-sdk ``` -The installed runtime needs no system Node.js. Repository contributors who need to build the runtime or wheels from source should use the [Python contributor workflows](../../../python/development.md). +The installation includes a matching native runtime wheel and the `dsh` command. Normal SDK execution needs no system Node.js. Repository contributors who build the artifacts should use the [Python contributor workflow](../../../python/development.md). ## Run the checked-in example -Set the credential in the environment. Set `DEEPSEEK_BASE_URL` as well when the model is served by an OpenAI-compatible proxy rather than the default DeepSeek endpoint. +Export the credential and, when needed, a compatible proxy endpoint: ```sh export DEEPSEEK_API_KEY=sk-your-key-here # export DEEPSEEK_BASE_URL=http://127.0.0.1:8000/v1 -# export DSH_MODEL=deepseek-v4-flash -# export DSH_SYSTEM_PROMPT='You are a helpful software engineer assistant.' ``` -Run one task against an isolated workspace and session directory: +Run one task with explicit workspace and home paths: ```sh python examples/python-sdk-agent/minimal.py \ - --workspace /absolute/path/to/workspace \ - --session-root /absolute/path/to/sessions \ + --workspace /absolute/path/to/disposable-workspace \ + --dsh-home /absolute/path/to/example-dsh-home \ --session-id example-001 \ "Inspect the repository and fix the failing tests." ``` -The script prints the final assistant response. The session directory receives a JSONL log containing the assembled model requests and tool calls. +The script prints the final assistant response. The selected home receives the generated `sdk-minimal` profile, installed plugins, and uncompressed JSONL session logs under `sessions/`. The example and SDK never silently read `~/.dsh`. -## Use the SDK in your own program - -The checked-in example is a thin wrapper around this SDK call: +## Use the SDK in your program ```python from pathlib import Path from deepseek_harness import DeepSeekHarness -config = Path("examples/python-sdk-agent/minimal.cordis.yml").resolve() -workspace = Path("/absolute/path/to/workspace").resolve() -sessions = Path("/absolute/path/to/sessions").resolve() - +workspace = Path("/absolute/path/to/disposable-workspace").resolve() +dsh_home = Path("/absolute/path/to/example-dsh-home").resolve() with DeepSeekHarness( provider="deepseek-official", model="deepseek-v4-flash", max_tokens=49_152, cwd=str(workspace), - session_root=str(sessions), - cordis=str(config), + dsh_home=str(dsh_home), + profile="sdk-minimal", ) as harness: result = harness.run( "Inspect the repository and fix the failing tests.", @@ -78,9 +70,23 @@ with DeepSeekHarness( print(result.final_response) ``` -`DeepSeekHarness` starts the bundled runtime lazily and reuses it until the context manager exits. Reusing the same harness and session id preserves the session-owned Bash process, including its working directory, exported variables, and shell functions. Use a fresh session id for an independent task; reuse an id only when the next call should continue the same durable conversation. +The SDK starts the bundled `dsh --profile sdk-minimal` process lazily and reuses it until context-manager exit. The profile, its persistent patch, the home patch, and any ordered `patches` tuple form the application configuration. There is no separate Python runtime bin or complete-config option. -## Understand the example composition +## Install or define plugins + +Use `dsh plugin` for dependencies and bundle layers that should persist in this home: + +```sh +export DSH_HOME=/absolute/path/to/example-dsh-home +dsh --profile sdk-minimal --dump-default-config >/dev/null +dsh plugin --profile sdk-minimal add file:/absolute/path/to/my-plugin-bundle +``` + +The first command initializes the shipped standalone profile. The second forwards package management to `pnpm`, then records any installed package that exports a `dsh.bundle` layer. Install `pnpm` only for this management command; launching the installed SDK does not need it. Edit `$DSH_HOME/profiles/sdk-minimal/cordis.patch.yml` for persistent row changes, or pass patch files from Python for per-launch changes. + +Another `profile` is valid when it includes `@deepseek-ai/dsh-sdk-app` or another JSON-RPC server row. Missing server rows, unresolved plugins, and invalid patches fail during startup instead of falling back to another composition. + +## Understand the minimal profile | Property | Value | |---|---| @@ -89,16 +95,13 @@ print(result.final_response) | Model-facing tools | Persistent `bash` and `str_replace_editor` only | | Bash timeout | 300 seconds | | Editor output limit | 16,000 characters | -| Context compaction | Disabled | -| Filesystem | Bare local backend; absolute editor paths may address any path visible to the runtime process | -| Session persistence | Uncompressed JSONL under `DSH_SESSION_ROOT` | +| Runtime context and compaction | Absent | +| Session persistence | Uncompressed JSONL under `/sessions` | -The composition omits harness identity, workspace prompt text, skills, one-shot Bash, task tools, compaction, and every other model-facing plugin. Sandbox-policy facts are logged as runtime user context rather than appended to the system prompt. +The profile's sole bundle inserts the complete tree over an empty root and does not include `dsh-base`; later base-profile tools therefore cannot appear implicitly. It contains the SDK protocol, one environment-configured DeepSeek adapter, local execution, and persistence, while settings, managed credentials, telemetry, Web tools, subagents, local instruction discovery, and compaction are absent. It pins `danger-full-access`, so persistent Bash and the editor can modify any path visible to the runtime; use a disposable checkout or container. The PTY implementation makes this example POSIX-only. -## Choose workspace and session IDs +The installed wheel still packages the full `web` profile and frontend assets. Run `dsh web` against an explicit `DSH_HOME` when a Python SDK deployment also needs the browser application; `web` is a separate CLI application and cannot serve a Python SDK client. -`cwd` selects the workspace available to the agent, while `session_root` stores session logs and state. Use a fresh session id for an independent task; reuse an id only when the next call should continue the same conversation and persistent shell state. +Use a fresh home when profiles, plugins, credentials, settings, and sessions must be isolated. Use a fresh session id for independent work; reuse a harness, home, and id only to continue the same durable conversation and session-owned resources. -The composition uses `danger-full-access`. Run it only inside a disposable checkout or container: Bash and the editor can modify any path allowed to the runtime process. The persistent PTY backend requires a POSIX terminal substrate, so this composition does not support Windows agents. - -The [`python-sdk-agent` example reference](../../../examples/python-sdk-agent/README.md) owns the exact composition. The [Python SDK reference](../../../python/sdk/README.md) covers lifecycle, results, notifications, runtime selection, and configuration; the [Cordis primer](../../cordis-primer.md) covers composition syntax. +The [bundle reference](../../../packages/bundle/sdk-minimal/README.md) owns the exact tree, and the [example reference](../../../examples/python-sdk-agent/README.md) owns the runnable program. The [Python SDK reference](../../../python/sdk/README.md) covers lifecycle, results, notifications, and low-level behavior; the [dsh CLI reference](../../../apps/cli/reference/README.md) covers profile layering. diff --git a/docs/user/guide/python-sdk.zh.md b/docs/user/guide/python-sdk.zh.md index bb883f6f37..47b420ab20 100644 --- a/docs/user/guide/python-sdk.zh.md +++ b/docs/user/guide/python-sdk.zh.md @@ -1,21 +1,19 @@ -# Python SDK 快速上手 +# Python SDK 入门 [English](python-sdk.md) | 中文 -本教程介绍 Web UI 之外的程序化使用方式:安装已发布的 Python SDK、运行仓库内置的 agent(智能体)组合,并在自己的程序中调用同一套 API。 +本教程安装已发布的 Python SDK,运行随附的独立极简 profile,并说明如何从自己的程序自定义同一个 `dsh` profile。 -## 前置要求 +## 前置条件 - Python 3.10 或更高版本 - Git -- Linux x64、Linux arm64 或 macOS 14 或更高版本的 arm64 -- DeepSeek 兼容的 API 端点与凭据 -- agent 可以修改的隔离 workspace +- Linux x64、Linux arm64,或 arm64 上的 macOS 14 或更高版本 +- DeepSeek 兼容的 API endpoint 与凭据 +- 隔离的 workspace 与隔离的 Harness home ## 安装 SDK -克隆仓库以使用其中的可运行示例,创建虚拟环境,并安装 SDK 及其同版本内置运行时: - ```sh git clone https://github.com/deepseek-ai/deepseek-harness.git cd deepseek-harness @@ -24,51 +22,45 @@ python -m venv .venv python -m pip install deepseek-harness-sdk ``` -安装后的运行时不需要系统提供 Node.js。需要从源码构建运行时或 wheel 包的仓库贡献者应使用 [Python 贡献者工作流](../../../python/development.zh.md)。 +安装内容包含匹配的原生运行时 wheel 与 `dsh` 命令。普通 SDK 运行不需要系统 Node.js。需要构建产物的仓库贡献者应使用 [Python 贡献者工作流](../../../python/development.zh.md)。 -## 运行仓库内置示例 +## 运行检入示例 -请在环境中设置凭据。如果模型不是由默认 DeepSeek 端点提供,而是通过 OpenAI 兼容代理提供,还需要设置 `DEEPSEEK_BASE_URL`。 +导出凭据;使用兼容代理时再设置 endpoint: ```sh export DEEPSEEK_API_KEY=sk-your-key-here # export DEEPSEEK_BASE_URL=http://127.0.0.1:8000/v1 -# export DSH_MODEL=deepseek-v4-flash -# export DSH_SYSTEM_PROMPT='You are a helpful software engineer assistant.' ``` -针对隔离的 workspace 和会话目录运行一个任务: +使用显式 workspace 与 home 路径运行一个任务: ```sh python examples/python-sdk-agent/minimal.py \ - --workspace /absolute/path/to/workspace \ - --session-root /absolute/path/to/sessions \ + --workspace /absolute/path/to/disposable-workspace \ + --dsh-home /absolute/path/to/example-dsh-home \ --session-id example-001 \ "Inspect the repository and fix the failing tests." ``` -脚本会打印 assistant 的最终回复。会话目录会收到 JSONL 日志,其中包含组装后的模型请求与工具调用。 +脚本会打印最终 assistant 响应。所选 home 会保存生成的 `sdk-minimal` profile、已安装插件,以及 `sessions/` 下的未压缩 JSONL 会话日志。示例与 SDK 绝不会静默读取 `~/.dsh`。 -## 在自己的程序中使用 SDK - -仓库内置示例是以下 SDK 调用的轻量包装: +## 在程序中使用 SDK ```python from pathlib import Path from deepseek_harness import DeepSeekHarness -config = Path("examples/python-sdk-agent/minimal.cordis.yml").resolve() -workspace = Path("/absolute/path/to/workspace").resolve() -sessions = Path("/absolute/path/to/sessions").resolve() - +workspace = Path("/absolute/path/to/disposable-workspace").resolve() +dsh_home = Path("/absolute/path/to/example-dsh-home").resolve() with DeepSeekHarness( provider="deepseek-official", model="deepseek-v4-flash", max_tokens=49_152, cwd=str(workspace), - session_root=str(sessions), - cordis=str(config), + dsh_home=str(dsh_home), + profile="sdk-minimal", ) as harness: result = harness.run( "Inspect the repository and fix the failing tests.", @@ -78,27 +70,38 @@ with DeepSeekHarness( print(result.final_response) ``` -`DeepSeekHarness` 会延迟启动内置运行时,并持续复用,直至退出上下文管理器。复用同一个 harness 与 session id 会保留该会话拥有的 Bash 进程,包括其工作目录、已导出的变量与 shell 函数。独立任务应使用新的 session id;只有下一次调用需要延续同一段持久化对话时,才复用原有 id。 +SDK 会延迟启动内置的 `dsh --profile sdk-minimal` 进程,并复用到上下文管理器退出。Profile、其持久 patch、home patch 与任何有序 `patches` tuple 共同组成应用配置。不存在独立 Python 运行时 bin 或完整配置选项。 -## 了解示例组合 +## 安装或定义插件 + +需要在该 home 中持久保存依赖与 bundle 层时,使用 `dsh plugin`: + +```sh +export DSH_HOME=/absolute/path/to/example-dsh-home +dsh --profile sdk-minimal --dump-default-config >/dev/null +dsh plugin --profile sdk-minimal add file:/absolute/path/to/my-plugin-bundle +``` + +第一个命令初始化随附的独立 profile。第二个命令把包管理转发给 `pnpm`,然后记录所有导出 `dsh.bundle` 层的已安装包。只有执行此管理命令时才需要安装 `pnpm`;启动已安装 SDK 不需要它。持久配置项变更应编辑 `$DSH_HOME/profiles/sdk-minimal/cordis.patch.yml`;单次启动变更则从 Python 传入 patch 文件。 + +另一个 `profile` 只有包含 `@deepseek-ai/dsh-sdk-app` 或另一个 JSON-RPC server 配置项时才有效。缺失 server 配置项、无法解析的插件和非法 patch 会在启动时失败,不会回退到其他组合。 + +## 理解极简 profile | 属性 | 值 | |---|---| -| 系统提示词 | `DSH_SYSTEM_PROMPT`;未设置时使用 `You are a helpful software engineer assistant.` | -| `minimal.py` 使用的模型 | `--model`,其次为 `DSH_MODEL`,最后为 `deepseek-v4-flash` | +| 系统提示词 | `DSH_SYSTEM_PROMPT`,未设置时为 `You are a helpful software engineer assistant.` | +| `minimal.py` 的模型 | `--model`,然后是 `DSH_MODEL`,最后是 `deepseek-v4-flash` | | 面向模型的工具 | 仅持久 `bash` 与 `str_replace_editor` | | Bash 超时 | 300 秒 | -| 编辑器输出上限 | 16,000 个字符 | -| 上下文压缩 | 已关闭 | -| 文件系统 | 裸本地后端;编辑器使用绝对路径,可以访问运行时进程可见的任何路径 | -| 会话持久化 | `DSH_SESSION_ROOT` 下未压缩的 JSONL | +| Editor 输出上限 | 16,000 字符 | +| 运行时上下文与 compaction | 不存在 | +| 会话持久化 | `/sessions` 下的未压缩 JSONL | -该组合省略了 harness 身份、workspace 提示词文本、skill(技能)、一次性 Bash、任务工具、上下文压缩和其他所有面向模型的插件。沙箱策略事实记录为运行时用户上下文,而不会追加到系统提示词中。 +该 profile 的唯一组合包会在空根之上插入完整配置树,且不包含 `dsh-base`,因此基础 profile 以后新增的工具不会隐式出现。它包含 SDK 协议、一个由环境配置的 DeepSeek 适配器、本地执行与持久化;settings、托管凭据、遥测、Web 工具、subagent、本地指令发现和 compaction 均不存在。它固定使用 `danger-full-access`,因此持久 Bash 与 editor 可以修改运行时可见的任何路径;应使用一次性 checkout 或容器。由于采用 PTY 实现,本示例只支持 POSIX。 -## 选择 workspace 与 session id +已安装 wheel 仍会打包完整 `web` profile 与前端产物。如果 Python SDK 部署还需要浏览器应用,请针对显式 `DSH_HOME` 运行 `dsh web`;`web` 是独立 CLI 应用,不能为 Python SDK client 提供服务。 -`cwd` 用于选择 agent 可访问的 workspace,`session_root` 用于保存会话日志和状态。独立任务应使用新的 session id;只有下一次调用需要延续同一段对话和持久 shell 状态时,才复用原有 id。 +需要隔离 profile、插件、凭据、设置与会话时,应使用新的 home。独立工作应使用新的 session id;只有继续同一段持久对话和会话资源时,才同时复用 harness、home 与 id。 -该组合使用 `danger-full-access`。只能在可丢弃的 checkout 或容器内运行:Bash 与编辑器可以修改运行时进程有权访问的任何路径。持久 PTY 后端需要 POSIX 终端环境,因此该组合不支持 Windows agent。 - -准确的组合内容归 [`python-sdk-agent` 示例参考](../../../examples/python-sdk-agent/README.zh.md)所有。[Python SDK 参考](../../../python/sdk/README.zh.md)介绍生命周期、结果、通知、运行时选择和配置;[Cordis primer](../../cordis-primer.zh.md)介绍组合语法。 +[组合包参考](../../../packages/bundle/sdk-minimal/README.zh.md)定义确切配置树,[示例参考](../../../examples/python-sdk-agent/README.zh.md)定义可运行程序。[Python SDK 参考](../../../python/sdk/README.zh.md)介绍生命周期、结果、通知与底层行为;[dsh CLI 参考](../../../apps/cli/reference/README.zh.md)介绍 profile 分层。 diff --git a/examples/acp-agent/cordis.yml b/examples/acp-agent/cordis.yml index ddfb914132..8f1df1955d 100644 --- a/examples/acp-agent/cordis.yml +++ b/examples/acp-agent/cordis.yml @@ -54,9 +54,17 @@ config: provider: spawn toolName: subagent + enableModelSelection: true backgroundMode: continuable maxDepth: 1 +# Fork omits model selection so provider/model stay equal to the parent and the +# inherited history remains eligible for KV Cache reuse. It stays one-shot because +# a continuable child's `report` tool and prompt section precede that history and +# invalidate the same prefix. `run_in_background` is off as an explicit foreground-only +# choice even though the shipped ACP profile mounts the generic Job runtime. +# See .agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.md +# and .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md. - id: tool-subagent-fork name: '@deepseek-ai/dsh-tool-subagent' config: diff --git a/examples/acp-agent/depth-two.cordis.snapshot.yml b/examples/acp-agent/depth-two.cordis.snapshot.yml index 89ef8c1035..245253690f 100644 --- a/examples/acp-agent/depth-two.cordis.snapshot.yml +++ b/examples/acp-agent/depth-two.cordis.snapshot.yml @@ -20,6 +20,7 @@ config: provider: spawn toolName: subagent + enableModelSelection: true backgroundMode: continuable maxDepth: 2 # Re-pin the recorded flash model for this scenario's corpus. diff --git a/examples/acp-agent/depth-two.cordis.yml b/examples/acp-agent/depth-two.cordis.yml index b2d4dbe039..d25b48fb60 100644 --- a/examples/acp-agent/depth-two.cordis.yml +++ b/examples/acp-agent/depth-two.cordis.yml @@ -5,5 +5,6 @@ config: provider: spawn toolName: subagent + enableModelSelection: true backgroundMode: continuable maxDepth: 2 diff --git a/examples/acp-agent/subagent-configured-effort.cordis.snapshot.yml b/examples/acp-agent/subagent-configured-effort.cordis.snapshot.yml new file mode 100644 index 0000000000..c89a77d49d --- /dev/null +++ b/examples/acp-agent/subagent-configured-effort.cordis.snapshot.yml @@ -0,0 +1,65 @@ +# Keyless counterpart to subagent-configured-effort.cordis.yml: disable the +# live adapter, insert replay, and apply the configured-effort rejection patch. +- id: llm-deepseek + name: '@deepseek-ai/dsh-llm-deepseek' + disabled: true + +- id: sandbox + name: '@deepseek-ai/dsh-sandbox-local' + config: + runnerCommand: + - bash + - -c + - while [ "$1" != "--" ]; do shift; done; shift; exec "$@" + - passthrough-runner + runnerFailureSignatures: + - 'passthrough-runner: profile rejected' + +- id: tool-subagent + name: '@deepseek-ai/dsh-tool-subagent' + config: + provider: spawn + toolName: subagent + enableModelSelection: true + backgroundMode: continuable + maxDepth: 1 + agentOptions: + provider: deepseek-official + model: deepseek-v4-flash + reasoningEffort: unsupported + +- id: acp + name: '@deepseek-ai/dsh-acp' + config: + provider: deepseek-official + model: deepseek-v4-flash + +- id: session-persistence-jsonl + name: '@deepseek-ai/dsh-session-persistence-jsonl' + config: + root: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions' + compression: none + +- id: agent-instructions + name: '@deepseek-ai/dsh-agent-instructions' + config: + maxBytes: 65536 + +- id: system-prompt + name: '@deepseek-ai/dsh-system-prompt' + config: + persona: | + You are a coding assistant powered by the {{model}} model. Your working directory is {{cwd}}. Your bash tool runs under a file sandbox — a `[sandbox: file access denied …]` result is policy, not a command bug. + + Verify your work by running the code or tests. Keep answers brief and factual. + +- insert: + - id: llm-replay + name: '@deepseek-ai/dsh-llm-replay' + config: + providers: + - id: deepseek-official + name: DeepSeek + models: + - id: deepseek-v4-flash + - id: deepseek-v4-pro diff --git a/examples/acp-agent/subagent-configured-effort.cordis.yml b/examples/acp-agent/subagent-configured-effort.cordis.yml new file mode 100644 index 0000000000..9042fd3d91 --- /dev/null +++ b/examples/acp-agent/subagent-configured-effort.cordis.yml @@ -0,0 +1,15 @@ +# Configured-effort rejection snapshot overlay: keep one invalid configured +# effort so the tool rejects before starting a child instead of deferring the +# failure to the child agent loop. +- id: tool-subagent + name: '@deepseek-ai/dsh-tool-subagent' + config: + provider: spawn + toolName: subagent + enableModelSelection: true + backgroundMode: continuable + maxDepth: 1 + agentOptions: + provider: deepseek-official + model: deepseek-v4-flash + reasoningEffort: unsupported diff --git a/examples/acp-agent/tests/acp.snapshot.ts b/examples/acp-agent/tests/acp.snapshot.ts index 3e8f3aa982..73286b97a6 100644 --- a/examples/acp-agent/tests/acp.snapshot.ts +++ b/examples/acp-agent/tests/acp.snapshot.ts @@ -70,6 +70,9 @@ const SUBAGENT_DURABILITY_FAILURE_CONFIG = fileURLToPath( const SUBAGENT_CONTINUABLE_INHERITANCE_CONFIG = fileURLToPath( new URL('../subagent-continuable-inheritance.cordis.yml', import.meta.url), ) +const SUBAGENT_CONFIGURED_EFFORT_CONFIG = fileURLToPath( + new URL('../subagent-configured-effort.cordis.yml', import.meta.url), +) const LSP_CONFIG = fileURLToPath(new URL('./lsp.cordis.yml', import.meta.url)) const WEB_CONFIG = fileURLToPath(new URL('../web.cordis.yml', import.meta.url)) const FS_SEARCH_CONFIG = fileURLToPath(new URL('./fs-search.cordis.yml', import.meta.url)) @@ -201,6 +204,10 @@ const SCENARIOS: Scenario[] = [ hasModelTurn: true, recorded: false, overridden: true, + pinsHeader: true, + headerClass: 'session-title', + systemPromptSource: 'text-turn', + toolSchemasSource: 'text-turn', configPath: SESSION_TITLE_CONFIG, }, { name: 'tool-call-turn', hasModelTurn: true, recorded: true }, @@ -262,6 +269,18 @@ const SCENARIOS: Scenario[] = [ headerClass: 'image', configPath: IMAGE_CONFIG, }, + // Authored keyless replay of the re-encoding path: the 900x1200 16-bit + // gradient PNG cannot pass through, so the master converts down the opaque + // JPEG ladder and the 640,000-pixel request budget re-encodes a downscaled + // request version — the assembled projection the tiny byte-identical + // fixtures above never exercise. + { + name: 'read-image-reencode', + hasModelTurn: true, + recorded: false, + headerClass: 'image', + configPath: IMAGE_CONFIG, + }, { name: 'inline-image-prompt', hasModelTurn: true, @@ -390,8 +409,8 @@ const SCENARIOS: Scenario[] = [ // An overwrite whose replacement is at/above the configured diff-basis bound: // the persisted result meta carries no contextual hunks and presentation // falls back to the whole-file diff. The overlay leaves the prompt and tool - // sequence identical to text-turn, but the freshly recorded header carries - // the current adapter capability fields, so the scenario pins its own class. + // sequence identical to text-turn. The scenario pins its own header class + // for config fields while sharing the unchanged prompt and tool schema. { name: 'fs-write-overwrite-bounded', hasModelTurn: true, @@ -547,6 +566,20 @@ const SCENARIOS: Scenario[] = [ overridden: true, configPath: DEPTH_TWO_CONFIG, }, + // Authored keyless replay first discovers the exact child route through the + // assembled directory tool, then proves a configured effort is validated + // before child creation through the same live replay LLM registry. + { + name: 'subagent-configured-effort-rejection', + hasModelTurn: true, + recorded: false, + overridden: true, + pinsHeader: true, + headerClass: 'subagent-configured-effort', + systemPromptSource: 'text-turn', + toolSchemasSource: 'text-turn', + configPath: SUBAGENT_CONFIGURED_EFFORT_CONFIG, + }, // Authored keyless replay through the assembled app: a one-shot child calls // the real ask_user_question tool, the runtime-ownership guard rejects before // the tripwire provider, and the child carries the unresolved decision in its diff --git a/examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl b/examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl index 01ba355631..607c9b3bb3 100644 --- a/examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl @@ -5,10 +5,10 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"ebe0cfa0-a909-47e0-8294-28ad84a8fe77"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} -{"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Check direct child"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn","label":"Check direct child"}} {"type":"step/start","data":{"turn":1,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"ebe0cfa0-a909-47e0-8294-28ad84a8fe77"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"e3c23441-606f-4e7a-8338-b434c0d04a4e"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"b1814e62-f9de-49fc-8e60-4271eecb3500"},"surfaceOp":"append"} {"type":"session/title","data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl b/examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl index 73958c517c..d3cbf0e856 100644 --- a/examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl +++ b/examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl @@ -5,10 +5,10 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"2ac2cc54-9bce-4cfa-a569-a64f51bc30a7"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} -{"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"spawn"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn"}} {"type":"step/start","data":{"turn":1,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"2ac2cc54-9bce-4cfa-a569-a64f51bc30a7"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"5d128e81-c7c2-4cd0-ad1c-7409b33650fc"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"f82215a6-9c52-4c75-b46b-f722a1b64f72"},"surfaceOp":"append"} {"type":"session/title","data":{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.expected.md index 1743643d95..b880f67453 100644 --- a/examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.expected.md @@ -301,6 +301,13 @@ interface ToolArgsMap { /** children (default) lists direct children only; descendants walks the complete tree below you. */ scope?: "children" | "descendants"; } & Record; + /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + list_subagent_models: { + /** Registered LLM provider id. Omit to list providers. */ + provider?: string; + /** Exact model id to inspect. Requires provider; omit to list that provider's advertised models. */ + model?: string; + } & Record; /** Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools. */ ralph: { /** The immutable completion objective for every fresh Ralph round. */ @@ -351,12 +358,18 @@ interface ToolArgsMap { /** Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file. */ view_range?: number[]; } & Record; - /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. */ + /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ subagent: { /** A short (3-5 word) description of the delegated task, for display. */ description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; /** Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it. */ run_in_background?: boolean; } & Record; @@ -587,6 +600,7 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; + list_subagent_models: string; ralph: { runId: string; agentsStarted: number; diff --git a/examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json index dcce863f94..2f1950a691 100644 --- a/examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json @@ -457,6 +457,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's advertised models." + } + } + } + }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", @@ -627,7 +644,7 @@ }, { "name": "subagent", - "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.", + "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -639,6 +656,18 @@ "type": "string", "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs." }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + }, "run_in_background": { "type": "boolean", "description": "Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it." diff --git a/examples/acp-agent/tests/snapshots/both-mode-turn/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/both-mode-turn/system-prompt.expected.md index 37df6287ed..b667c8dd6b 100644 --- a/examples/acp-agent/tests/snapshots/both-mode-turn/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/both-mode-turn/system-prompt.expected.md @@ -134,6 +134,13 @@ interface ToolArgsMap { /** children (default) lists direct children only; descendants walks the complete tree below you. */ scope?: "children" | "descendants"; } & Record; + /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + list_subagent_models: { + /** Registered LLM provider id. Omit to list providers. */ + provider?: string; + /** Exact model id to inspect. Requires provider; omit to list that provider's advertised models. */ + model?: string; + } & Record; /** Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools. */ ralph: { /** The immutable completion objective for every fresh Ralph round. */ @@ -184,12 +191,18 @@ interface ToolArgsMap { /** Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file. */ view_range?: number[]; } & Record; - /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. */ + /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ subagent: { /** A short (3-5 word) description of the delegated task, for display. */ description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; /** Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it. */ run_in_background?: boolean; } & Record; @@ -401,6 +414,7 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; + list_subagent_models: string; ralph: { runId: string; agentsStarted: number; diff --git a/examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json index b8a2ed790f..bf85198220 100644 --- a/examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's advertised models." + } + } + } + }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", @@ -430,7 +447,7 @@ }, { "name": "subagent", - "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.", + "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -442,6 +459,18 @@ "type": "string", "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs." }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + }, "run_in_background": { "type": "boolean", "description": "Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it." diff --git a/examples/acp-agent/tests/snapshots/code-mode-read-image/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/code-mode-read-image/system-prompt.expected.md index daf622df60..c9bad7d1fa 100644 --- a/examples/acp-agent/tests/snapshots/code-mode-read-image/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/code-mode-read-image/system-prompt.expected.md @@ -136,6 +136,13 @@ interface ToolArgsMap { /** children (default) lists direct children only; descendants walks the complete tree below you. */ scope?: "children" | "descendants"; } & Record; + /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + list_subagent_models: { + /** Registered LLM provider id. Omit to list providers. */ + provider?: string; + /** Exact model id to inspect. Requires provider; omit to list that provider's advertised models. */ + model?: string; + } & Record; /** Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools. */ ralph: { /** The immutable completion objective for every fresh Ralph round. */ @@ -186,12 +193,18 @@ interface ToolArgsMap { /** Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file. */ view_range?: number[]; } & Record; - /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. */ + /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ subagent: { /** A short (3-5 word) description of the delegated task, for display. */ description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; /** Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it. */ run_in_background?: boolean; } & Record; @@ -403,6 +416,7 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; + list_subagent_models: string; ralph: { runId: string; agentsStarted: number; diff --git a/examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md index 6894f13fb6..7506ad8373 100644 --- a/examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md @@ -136,6 +136,13 @@ interface ToolArgsMap { /** children (default) lists direct children only; descendants walks the complete tree below you. */ scope?: "children" | "descendants"; } & Record; + /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + list_subagent_models: { + /** Registered LLM provider id. Omit to list providers. */ + provider?: string; + /** Exact model id to inspect. Requires provider; omit to list that provider's advertised models. */ + model?: string; + } & Record; /** Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools. */ ralph: { /** The immutable completion objective for every fresh Ralph round. */ @@ -186,12 +193,18 @@ interface ToolArgsMap { /** Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file. */ view_range?: number[]; } & Record; - /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. */ + /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ subagent: { /** A short (3-5 word) description of the delegated task, for display. */ description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; /** Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it. */ run_in_background?: boolean; } & Record; @@ -403,6 +416,7 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; + list_subagent_models: string; ralph: { runId: string; agentsStarted: number; diff --git a/examples/acp-agent/tests/snapshots/fs-glob-sampling/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/fs-glob-sampling/tool-schemas.expected.json index 993a7579bd..2819e54870 100644 --- a/examples/acp-agent/tests/snapshots/fs-glob-sampling/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/fs-glob-sampling/tool-schemas.expected.json @@ -180,6 +180,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's advertised models." + } + } + } + }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", @@ -313,7 +330,7 @@ }, { "name": "subagent", - "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.", + "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -325,6 +342,18 @@ "type": "string", "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs." }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + }, "run_in_background": { "type": "boolean", "description": "Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it." diff --git a/examples/acp-agent/tests/snapshots/lsp-definition/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/lsp-definition/tool-schemas.expected.json index 4ea490a884..c012852f0a 100644 --- a/examples/acp-agent/tests/snapshots/lsp-definition/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/lsp-definition/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's advertised models." + } + } + } + }, { "name": "lsp", "description": "Query a language server for precise code navigation. operation is one of goToDefinition, findReferences, goToImplementation, hover. line and character are one-based UTF-16 cursor coordinates. findReferences includes the declaration.", @@ -446,7 +463,7 @@ }, { "name": "subagent", - "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.", + "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -458,6 +475,18 @@ "type": "string", "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs." }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + }, "run_in_background": { "type": "boolean", "description": "Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it." diff --git a/examples/acp-agent/tests/snapshots/product-subagent-both/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/product-subagent-both/tool-schemas.expected.json index e1d954e615..5eec9bb706 100644 --- a/examples/acp-agent/tests/snapshots/product-subagent-both/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/product-subagent-both/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's advertised models." + } + } + } + }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", @@ -409,7 +426,7 @@ }, { "name": "subagent", - "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.", + "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -421,6 +438,18 @@ "type": "string", "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs." }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + }, "run_in_background": { "type": "boolean", "description": "Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it." diff --git a/examples/acp-agent/tests/snapshots/product-subagent-codex/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/product-subagent-codex/tool-schemas.expected.json index 944a002e53..2d5b27c48b 100644 --- a/examples/acp-agent/tests/snapshots/product-subagent-codex/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/product-subagent-codex/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's advertised models." + } + } + } + }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", @@ -409,7 +426,7 @@ }, { "name": "subagent", - "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.", + "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -421,6 +438,18 @@ "type": "string", "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs." }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + }, "run_in_background": { "type": "boolean", "description": "Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it." diff --git a/examples/acp-agent/tests/snapshots/product-subagent-result-diagnostic/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/product-subagent-result-diagnostic/tool-schemas.expected.json index 0ed6e087db..bb5b4b7411 100644 --- a/examples/acp-agent/tests/snapshots/product-subagent-result-diagnostic/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/product-subagent-result-diagnostic/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's advertised models." + } + } + } + }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", @@ -409,7 +426,7 @@ }, { "name": "subagent", - "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.", + "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -421,6 +438,18 @@ "type": "string", "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs." }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + }, "run_in_background": { "type": "boolean", "description": "Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it." diff --git a/examples/acp-agent/tests/snapshots/pty-tools/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/pty-tools/tool-schemas.expected.json index 6ab4f41978..2303325732 100644 --- a/examples/acp-agent/tests/snapshots/pty-tools/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/pty-tools/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's advertised models." + } + } + } + }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", @@ -409,7 +426,7 @@ }, { "name": "subagent", - "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.", + "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -421,6 +438,18 @@ "type": "string", "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs." }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + }, "run_in_background": { "type": "boolean", "description": "Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it." diff --git a/examples/acp-agent/tests/snapshots/read-image-reencode/input.json b/examples/acp-agent/tests/snapshots/read-image-reencode/input.json new file mode 100644 index 0000000000..f08307ffed --- /dev/null +++ b/examples/acp-agent/tests/snapshots/read-image-reencode/input.json @@ -0,0 +1,14 @@ +{ + "steps": [ + { + "op": "initialize" + }, + { + "op": "newSession" + }, + { + "op": "prompt", + "text": "Use read_image to look at gradient.png in the current directory, then reply with exactly the single word DONE." + } + ] +} diff --git a/examples/acp-agent/tests/snapshots/read-image-reencode/session.jsonl b/examples/acp-agent/tests/snapshots/read-image-reencode/session.jsonl new file mode 100644 index 0000000000..c6a2466090 --- /dev/null +++ b/examples/acp-agent/tests/snapshots/read-image-reencode/session.jsonl @@ -0,0 +1,29 @@ +{"type":"session","version":0,"id":"33333333-3333-4333-8333-333333333334","createdAt":1783951000000,"cwd":"{{cwd}}","delegationDepth":0} +{"type":"permission/preset","data":{"preset":"danger-full-access"}} +{"type":"sandbox/mode","data":{"mode":"danger-full-access"}} +{"type":"approval/policy","data":{"policy":"never"}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Use read_image to look at gradient.png in the current directory, then reply with exactly the single word DONE."}],"source":{"kind":"user"},"role":"user","id":"0a0a0a0a-0000-4000-8000-000000000001"}]}} +{"type":"turn/start","data":{"turn":1}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} +{"type":"step/start","data":{"turn":1,"step":1}} +{"type":"user/message","data":{"content":[{"type":"text","text":"Use read_image to look at gradient.png in the current directory, then reply with exactly the single word DONE."}],"source":{"kind":"user"},"role":"user","id":"0a0a0a0a-0000-4000-8000-000000000001"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"eecd1df6-153c-4a34-b198-42bfc9f9701e"},"surfaceOp":"append"} +{"type":"session/title","data":{"title":"Use read_image to look at","messageSeqs":[7],"source":{"kind":"fallback"}}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"}} +{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"read-image-reencode-call","name":"read_image","arguments":"{\"file_path\":\"gradient.png\"}"}}}} +{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"read-image-reencode-call","name":"read_image","arguments":"{\"file_path\":\"gradient.png\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"id":"2b71c837-237d-4d92-a857-8b8ad1a3f237"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[12,13,14,15],"surfaceOp":"append"} +{"type":"tool/call","data":{"turn":1,"step":1,"callId":"read-image-reencode-call","name":"read_image","arguments":"{\"file_path\":\"gradient.png\"}"}} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"read-image-reencode-call"},"content":[{"type":"tool-result","toolCallId":"read-image-reencode-call","content":[{"type":"text","text":"{{cwd}}/gradient.png\nimage\n\nimage/jpeg image, 840x840 px, 10162 bytes\n"},{"type":"image","attachment":{"attachmentId":"sha256:a9725ac66134512fc8e8971cb0356f9d217529493073d361724ca87c9c7aa968","mediaType":"image/jpeg","bytes":10162,"width":840,"height":840,"name":"gradient.png"}}],"isError":false}],"role":"user","id":"d52141fc-3f8a-42f9-bff0-a44f5a02579a"}},"sourceEventSeqs":[17],"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":1}} +{"type":"step/start","data":{"turn":1,"step":2}} +{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} +{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}} +{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"id":"5a45946c-b9f4-4f2c-a7c3-2569e541ec1d"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[21,22,23,24],"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":2}} +{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/acp-agent/tests/snapshots/read-image-reencode/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/read-image-reencode/stdout.expected.jsonl new file mode 100644 index 0000000000..a1153f758c --- /dev/null +++ b/examples/acp-agent/tests/snapshots/read-image-reencode/stdout.expected.jsonl @@ -0,0 +1,6 @@ +{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"mcpCapabilities":{"http":true},"promptCapabilities":{"image":true,"audio":false,"embeddedContext":false},"sessionCapabilities":{"close":{},"list":{},"resume":{}}},"authMethods":[]}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","category":"model","type":"select","currentValue":"[\"deepseek-official\",\"deepseek-v4-flash-vision-exp\"]","options":[{"group":"deepseek-official","name":"DeepSeek","options":[{"value":"[\"deepseek-official\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek-official\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"},{"value":"[\"deepseek-official\",\"deepseek-v4-flash-vision-exp\"]","name":"deepseek-v4-flash-vision-exp"}]}]}]}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"read-image-reencode-call","title":"read_image","kind":"other","status":"in_progress","rawInput":{"file_path":"gradient.png"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"read-image-reencode-call","status":"completed","content":[{"type":"content","content":{"type":"text","text":"{{cwd}}/gradient.png\nimage\n\nimage/jpeg image, 840x840 px, 10162 bytes\n"}},{"type":"content","content":{"type":"image","data":"/9j/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUFBQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wAARCANIA0gDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAECBv/EABYQAQEBAAAAAAAAAAAAAAAAAAARYf/EABoBAQEBAQEBAQAAAAAAAAAAAAADAQIEBgj/xAAWEQEBAQAAAAAAAAAAAAAAAAAAEhH/2gAMAwEAAhEDEQA/AO5pUHz78jytKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStEAlBmldLy0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0oS0M0CQZG4vLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEpSoNxeVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVogYSgg3F8UQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMUQMMZpQavJSgElKASUoBJSgElKASUoBJSgElKASUoBJSgElKASUoBJSgElKASUoBJSgElKASUoBJSgElKASUoBJSgElKASUoBJSgElKASUoBJSgElKASUoBJSgElKASUoBJSgElKASUoBJSgElKASUoBJSgElKASUoBJSgElKASUoBJSgElKASUoBJSgElKASUoBJQAlkSldPRKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUCUEG4tKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKiBhKCUrceiVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEpTCVEoYSgyNXloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAloZAkpUul10vK0qXS6ErSpdLoStKl0uhK0qXS6ErSpdLoStKl0uhK0qXS6ErSpdLoStKl0uhK0qXS6ErSpdLoStKl0uhK0qXS6ErSpdLoStKl0uhK0qXS6ErSpdLoStKl0uhK0qXS6ErSpdLoStKl0uhK0qXS6ErSpdLoStKl0uhK0qXS6ErSpdLoStKl0uhK0qXS6ErSpdLoStKl0uhK0qXS6ErSpdLoStKl0uhK0qXS6ErSpdLoStKl0uhK0qXS6ErSpdLoStKl0uhK0qXS6ErSpdLoStKl0uhK0qXS6ErSpdLoStKl0uhK0qXS6ErSpdLoStKl0uhK0qXS6ErSpdLoStKl0uhK0S6BLIlK16JUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSgSlKg3FpWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWlQMJWiBhKUqUrrHolaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaVKUwlaJQwlBmlavLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzShLQzQJSlQavK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0QCUEpXS8qJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJQJSlQbi8rSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErSoGErRAwlmlSlbi8rSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErSpSmErRKGEoINeiVEAlRAJUQCVEAlRAJUQCVEAlRAJUQCVEAlRAJUQCVEAlRAJUQCVEAlRAJUQCVEAlRAJUQCVEAlRAJUQCVEAlRAJUQCVEAlRAJUQCVEAlRAJUQCVEAlRAJUQCVEAlRAJUQCVEAlRAJUQCVEAlRAJUQCVEAlRAJUQCVEAlRAJUQCVEAlKVB0tK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0qAStKgErSoBK0QCUGaVuPRLQzSmEtDNKYS0M0phLQzSmEtDNKYS0M0phLQzSmEtDNKYS0M0phLQzSmEtDNKYS0M0phLQzSmEtDNKYS0M0phLQzSmEtDNKYS0M0phLQzSmEtDNKYS0M0phLQzSmEtDNKYS0M0phLQzSmEtDNKYS0M0phLQzSmEtDNKYS0M0phLQzSmEtDNKYS0M0phLQzSmEtDNKYS0M0phLQzSmEtDNKYS0M0phLQzSmEtDNKYS0M0phLQzSmEtDNKYS0M0phLQzSmEtDNKYS0M0phLQzSmEtDNKYS0M0phLQzQwkGRuLy0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhLQyGEtDIYS0MhhKANxeQAwkAMJADCQAwkAMJADCQAwkAMJADCQAwkAMJADCQAwkAMJADCQAwkAMJADCQAwkAMJADCQAwkAMJADCQAwkAMJADCQAwkAMJADCQAwkAMJADCQAwkAMJADCQAwkAMJADCQAwkAMJADCQAwkAMJADCQAwkAMJADCQAwkAMJADCQAwlkQavKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKiASlKg6XlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaIBLIUrceiQKUwkClMJApTCQKUwkClMJApTCQKUwkClMJApTCQKUwkClMJApTCQKUwkClMJApTCQKUwkClMJApTCQKUwkClMJApTCQKUwkClMJApTCQKUwkClMJApTCQKUwkClMJApTCQKUwkClMJApTCQKUwkClMJApTCQKUwkClMJApTCQKUwkClMJApTCQKUwkClMJApTCQKUwkClMJApTCQKUwkClMJApTCQKGEoMjcXxoZDDGhkMMaGQwxoZDDGhkMMaGQwxoZDDGhkMMaGQwxoZDDGhkMMaGQwxoZDDGhkMMaGQwxoZDDGhkMMaGQwxoZDDGhkMMaGQwxoZDDGhkMMaGQwxoZDDGhkMMaGQwxoZDDGhkMMaGQwxoZDDGhkMMaGQwxoZDDGhkMMaGQwxoZDDGhkMMaGQwxoZDDGhkMMaGQwxoZDDGhkMMaGQwxoZDDGhkMMaGQwxoZDDGhkMMaGQwxoZDDEpUpWrytKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStEoEoJSul5USlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSgSlKg1eVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVogEsiUrcXlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKUwlRKGEpSoOsXxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaVAwxaIGGJSpStXlaVKUJWlSlCVpUpQlaVKUJWlSlCVpUpQlaVKUJWlSlCVpUpQlaVKUJWlSlCVpUpQlaVKUJWlSlCVpUpQlaVKUJWlSlCVpUpQlaVKUJWlSlCVpUpQlaVKUJWlSlCVpUpQlaVKUJWlSlCVpUpQlaVKUJWlSlCVpUpQlaVKUJWlSlCVpUpQlaVKUJWlSlCVpUpQlaVKUJWlSlCVpUpQlaVKUJWlSlCVpUpQlaVKUJWlSlCVpUpQlaVKUJWlSlCVpUpQlaVKUJWlSlCVpUpQlaJQJQSla9EqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJShKiUoSolKEqJQJZpQbi0lKBhJSgYSUoGElKBhJSgYSUoGElKBhJSgYSUoGElKBhJSgYSUoGElKBhJSgYSUoGElKBhJSgYSUoGElKBhJSgYSUoGElKBhJSgYSUoGElKBhJSgYSUoGElKBhJSgYSUoGElKBhJSgYSUoGElKBhJSgYSUoGElKBhJSgYSUoGElKBhJSgYSUoGElKBhJSgYSUoGElKBhJSgYSUoGElKBhJSgYSUoGElAMJZEpXWPRKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUphKiUMJQQbi+KIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGKIGGJSoNXlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaVAJWlQCVpUAlaIBKDI6eiWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCWhkCSlZGrS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1SsgS1RkCUEpW49EqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJSmEqJQwlLpdQbi8rdLqBhK3S6gYSt0uoGErdLqBhK3S6gYSt0uoGErdLqBhK3S6gYSt0uoGErdLqBhK3S6gYSt0uoGErdLqBhK3S6gYSt0uoGErdLqBhK3S6gYSt0uoGErdLqBhK3S6gYSt0uoGErdLqBhK3S6gYSt0uoGErdLqBhK3S6gYSt0uoGErdLqBhK3S6gYSt0uoGErdLqBhK3S6gYSt0uoGErdLqBhK3S6gYSt0uoGErdLqBhK3S6gYSt0uoGErdLqBhK3S6gYSt0uoGErdLqBhK3S6gYSt0uoGErdLqBhK3S6gYSt0uoGErdLqBhK3S6gYSt0uoGErdEDCWQpWryBShIFKEgUoSBShIFKEgUoSBShIFKEgUoSBShIFKEgUoSBShIFKEgUoSBShIFKEgUoSBShIFKEgUoSBShIFKEgUoSBShIFKEgUoSBShIFKEgUoSBShIFKEgUoSBShIFKEgUoSBShIFKEgUoSBShIFKEgUoSBShIFKEgUoSBShIFKEgUoSBShIFKEgUoSBQJZEHS8qIBKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKiASogEqIBKUqXS63F5WlS6XTCVpUul0wlaVLpdMJWlS6XTCVpUul0wlaVLpdMJWlS6XTCVpUul0wlaVLpdMJWlS6XTCVpUul0wlaVLpdMJWlS6XTCVpUul0wlaVLpdMJWlS6XTCVpUul0wlaVLpdMJWlS6XTCVpUul0wlaVLpdMJWlS6XTCVpUul0wlaVLpdMJWlS6XTCVpUul0wlaVLpdMJWlS6XTCVpUul0wlaVLpdMJWlS6XTCVpUul0wlaVLpdMJWlS6XTCVpUul0wlaVLpdMJWlS6XTCVpUul0wlaVLpdMJWlS6XTCVpUul0wlaVLpdMJWlS6XTCVpUul0wlaVLpdMJWlS6XTCVpUul0wlaVLpdMJWlS6XTCVpUul0wlaVLpdMJWiXQwlBKVuLyolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolKYSolDCUpWR0vjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVKyBjVGQMSlSlavK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0qUoStKlKErSpShK0SgSglK16JUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSlCVEpQlRKUJUSgSlKg6xaVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVpUDCVogYSgzStx6JaGaUwloZpTCWhmlMJaGaUwloZpTCWhmlMJaGaUwloZpTCWhmlMJaGaUwloZpTCWhmlMJaGaUwloZpTCWhmlMJaGaUwloZpTCWhmlMJaGaUwloZpTCWhmlMJaGaUwloZpTCWhmlMJaGaUwloZpTCWhmlMJaGaUwloZpTCWhmlMJaGaUwloZpTCWhmlMJaGaUwloZpTCWhmlMJaGaUwloZpTCWhmlMJaGaUwloZpTCWhmlMJaGaUwloZpTCWhmlMJaGaUwloZpTCWhmlMJaGaUwloZpTCWhmlMJaGaGEgDV8AAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAx//2Q==","mimeType":"image/jpeg"}}]}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","messageId":"{{messageId}}","content":{"type":"text","text":"DONE"}}}} +{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}} diff --git a/examples/acp-agent/tests/snapshots/read-image-reencode/workspace/gradient.png b/examples/acp-agent/tests/snapshots/read-image-reencode/workspace/gradient.png new file mode 100644 index 0000000000..bfb45f6931 Binary files /dev/null and b/examples/acp-agent/tests/snapshots/read-image-reencode/workspace/gradient.png differ diff --git a/examples/acp-agent/tests/snapshots/session-query-spill/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/session-query-spill/tool-schemas.expected.json index c423cdb57c..7ff41194b3 100644 --- a/examples/acp-agent/tests/snapshots/session-query-spill/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/session-query-spill/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's advertised models." + } + } + } + }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", @@ -613,7 +630,7 @@ }, { "name": "subagent", - "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.", + "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -625,6 +642,18 @@ "type": "string", "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs." }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + }, "run_in_background": { "type": "boolean", "description": "Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it." diff --git a/examples/acp-agent/tests/snapshots/subagent-child-question-rejection/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-child-question-rejection/session.1.jsonl index 323cdcdb4c..e6f032662c 100644 --- a/examples/acp-agent/tests/snapshots/subagent-child-question-rejection/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-child-question-rejection/session.1.jsonl @@ -5,7 +5,7 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Call ask_user_question once to ask whether deployment should use the CUDA fallback. If the tool returns an error, include the unresolved question verbatim in your final result."}],"source":{"kind":"user"},"role":"user","id":"106c2785-219e-46e8-8386-497ac6a98f68"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} -{"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Check deployment question"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn","label":"Check deployment question"}} {"type":"step/start","data":{"turn":1,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Call ask_user_question once to ask whether deployment should use the CUDA fallback. If the tool returns an error, include the unresolved question verbatim in your final result."}],"source":{"kind":"user"},"role":"user","id":"106c2785-219e-46e8-8386-497ac6a98f68"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"d8734c8a-d956-4e3f-8d28-399adf51a203"},"surfaceOp":"append"} diff --git a/examples/acp-agent/tests/snapshots/subagent-child-question-rejection/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/subagent-child-question-rejection/tool-schemas.expected.json index fdda53355f..6aa10aa7d5 100644 --- a/examples/acp-agent/tests/snapshots/subagent-child-question-rejection/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/subagent-child-question-rejection/tool-schemas.expected.json @@ -323,6 +323,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's advertised models." + } + } + } + }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", @@ -472,7 +489,7 @@ }, { "name": "subagent", - "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.", + "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -484,6 +501,18 @@ "type": "string", "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs." }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + }, "run_in_background": { "type": "boolean", "description": "Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it." diff --git a/examples/acp-agent/tests/snapshots/subagent-configured-effort-rejection/input.json b/examples/acp-agent/tests/snapshots/subagent-configured-effort-rejection/input.json new file mode 100644 index 0000000000..eb1c2ee7ff --- /dev/null +++ b/examples/acp-agent/tests/snapshots/subagent-configured-effort-rejection/input.json @@ -0,0 +1,7 @@ +{ + "steps": [ + { "op": "initialize" }, + { "op": "newSession" }, + { "op": "prompt", "text": "Inspect the configured child model, then attempt one subagent call so its configured reasoning effort is validated before child creation." } + ] +} diff --git a/examples/acp-agent/tests/snapshots/subagent-configured-effort-rejection/replay.override.json b/examples/acp-agent/tests/snapshots/subagent-configured-effort-rejection/replay.override.json new file mode 100644 index 0000000000..0ac208bb71 --- /dev/null +++ b/examples/acp-agent/tests/snapshots/subagent-configured-effort-rejection/replay.override.json @@ -0,0 +1,32 @@ +[ + { + "kind": "chunks", + "chunks": [ + { "type": "block-start", "index": 0, "blockType": "tool-call" }, + { "type": "tool-call-delta", "index": 0, "id": "call_list_child_model", "name": "list_subagent_models", "argumentsDelta": "{\"provider\":\"deepseek-official\",\"model\":\"deepseek-v4-flash\"}" }, + { "type": "block-end", "index": 0, "block": { "type": "tool-call", "id": "call_list_child_model", "name": "list_subagent_models", "arguments": "{\"provider\":\"deepseek-official\",\"model\":\"deepseek-v4-flash\"}" } }, + { "type": "usage", "usage": { "inputTokens": 10, "outputTokens": 5 } }, + { "type": "finish", "reason": { "kind": "tool-calls" } } + ] + }, + { + "kind": "chunks", + "chunks": [ + { "type": "block-start", "index": 0, "blockType": "tool-call" }, + { "type": "tool-call-delta", "index": 0, "id": "call_configured_effort", "name": "subagent", "argumentsDelta": "{\"description\":\"Validate configured effort\",\"prompt\":\"This child must never start.\",\"provider\":\"deepseek-official\",\"model\":\"deepseek-v4-flash\",\"run_in_background\":false}" }, + { "type": "block-end", "index": 0, "block": { "type": "tool-call", "id": "call_configured_effort", "name": "subagent", "arguments": "{\"description\":\"Validate configured effort\",\"prompt\":\"This child must never start.\",\"provider\":\"deepseek-official\",\"model\":\"deepseek-v4-flash\",\"run_in_background\":false}" } }, + { "type": "usage", "usage": { "inputTokens": 10, "outputTokens": 5 } }, + { "type": "finish", "reason": { "kind": "tool-calls" } } + ] + }, + { + "kind": "chunks", + "chunks": [ + { "type": "block-start", "index": 0, "blockType": "text" }, + { "type": "text-delta", "index": 0, "text": "CONFIGURED_EFFORT_REJECTED" }, + { "type": "block-end", "index": 0, "block": { "type": "text", "text": "CONFIGURED_EFFORT_REJECTED" } }, + { "type": "usage", "usage": { "inputTokens": 10, "outputTokens": 2 } }, + { "type": "finish", "reason": { "kind": "stop" } } + ] + } +] diff --git a/examples/acp-agent/tests/snapshots/subagent-configured-effort-rejection/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-configured-effort-rejection/session.jsonl new file mode 100644 index 0000000000..25f746a2c1 --- /dev/null +++ b/examples/acp-agent/tests/snapshots/subagent-configured-effort-rejection/session.jsonl @@ -0,0 +1,41 @@ +{"type":"session","version":0,"id":"44444444-4444-4444-8444-444444444444","createdAt":1000,"cwd":"{{cwd}}","delegationDepth":0} +{"type":"permission/preset","data":{"preset":"danger-full-access"}} +{"type":"sandbox/mode","data":{"mode":"danger-full-access"}} +{"type":"approval/policy","data":{"policy":"never"}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Inspect the configured child model, then attempt one subagent call so its configured reasoning effort is validated before child creation."}],"source":{"kind":"user"},"role":"user","id":"04d53124-d38b-4705-a711-d0b5755b74c9"}]}} +{"type":"turn/start","data":{"turn":1}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} +{"type":"step/start","data":{"turn":1,"step":1}} +{"type":"user/message","data":{"content":[{"type":"text","text":"Inspect the configured child model, then attempt one subagent call so its configured reasoning effort is validated before child creation."}],"source":{"kind":"user"},"role":"user","id":"04d53124-d38b-4705-a711-d0b5755b74c9"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"55365caf-6fcc-484b-a4b7-646914654bbc"},"surfaceOp":"append"} +{"type":"session/title","data":{"title":"Inspect the configured child model,","messageSeqs":[7],"source":{"kind":"fallback"}}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} +{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_list_child_model","name":"list_subagent_models","argumentsDelta":"{\"provider\":\"deepseek-official\",\"model\":\"deepseek-v4-flash\"}"}}} +{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_list_child_model","name":"list_subagent_models","arguments":"{\"provider\":\"deepseek-official\",\"model\":\"deepseek-v4-flash\"}"}}}} +{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}} +{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_list_child_model","name":"list_subagent_models","arguments":"{\"provider\":\"deepseek-official\",\"model\":\"deepseek-v4-flash\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"000adf36-9918-4ad3-ab38-2fdb9003d0d7"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[12,13,14,15,16],"surfaceOp":"append"} +{"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_list_child_model","name":"list_subagent_models","arguments":"{\"provider\":\"deepseek-official\",\"model\":\"deepseek-v4-flash\"}"}} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_list_child_model"},"content":[{"type":"tool-result","toolCallId":"call_list_child_model","content":[{"type":"text","text":"deepseek-official/deepseek-v4-flash — deepseek-v4-flash\nReasoning efforts:\n(no advertised reasoning efforts)"}],"isError":false}],"role":"user","id":"f480c34c-cd56-4b8e-be7c-e6ce50d3f70a"}},"sourceEventSeqs":[18],"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":1}} +{"type":"step/start","data":{"turn":1,"step":2}} +{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"call_configured_effort","name":"subagent","argumentsDelta":"{\"description\":\"Validate configured effort\",\"prompt\":\"This child must never start.\",\"provider\":\"deepseek-official\",\"model\":\"deepseek-v4-flash\",\"run_in_background\":false}"}}} +{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_configured_effort","name":"subagent","arguments":"{\"description\":\"Validate configured effort\",\"prompt\":\"This child must never start.\",\"provider\":\"deepseek-official\",\"model\":\"deepseek-v4-flash\",\"run_in_background\":false}"}}}} +{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}} +{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_configured_effort","name":"subagent","arguments":"{\"description\":\"Validate configured effort\",\"prompt\":\"This child must never start.\",\"provider\":\"deepseek-official\",\"model\":\"deepseek-v4-flash\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"aec67548-de79-480f-8aec-2bd458ddbb41"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[22,23,24,25,26],"surfaceOp":"append"} +{"type":"tool/call","data":{"turn":1,"step":2,"callId":"call_configured_effort","name":"subagent","arguments":"{\"description\":\"Validate configured effort\",\"prompt\":\"This child must never start.\",\"provider\":\"deepseek-official\",\"model\":\"deepseek-v4-flash\",\"run_in_background\":false}"}} +{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_configured_effort"},"content":[{"type":"tool-result","toolCallId":"call_configured_effort","content":[{"type":"text","text":"Error: provider \"deepseek-official\" model \"deepseek-v4-flash\" does not support reasoning effort \"unsupported\""}],"isError":true}],"role":"user","id":"9a59cf14-e222-40de-85f4-6a4ab4f5a177"},"error":{"name":"LlmError","code":"UNSUPPORTED_REASONING_EFFORT"}},"sourceEventSeqs":[28],"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":2}} +{"type":"step/start","data":{"turn":1,"step":3}} +{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} +{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":0,"text":"CONFIGURED_EFFORT_REJECTED"}}} +{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CONFIGURED_EFFORT_REJECTED"}}}} +{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}} +{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"CONFIGURED_EFFORT_REJECTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"5a1911c6-f487-458f-b802-4a66221ec047"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[32,33,34,35,36],"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":3}} +{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/acp-agent/tests/snapshots/subagent-configured-effort-rejection/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/subagent-configured-effort-rejection/stdout.expected.jsonl new file mode 100644 index 0000000000..7d5829fb9d --- /dev/null +++ b/examples/acp-agent/tests/snapshots/subagent-configured-effort-rejection/stdout.expected.jsonl @@ -0,0 +1,8 @@ +{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"mcpCapabilities":{"http":true},"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false},"sessionCapabilities":{"close":{},"list":{},"resume":{}}},"authMethods":[]}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","category":"model","type":"select","currentValue":"[\"deepseek-official\",\"deepseek-v4-flash\"]","options":[{"group":"deepseek-official","name":"DeepSeek","options":[{"value":"[\"deepseek-official\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek-official\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]}]}]}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_list_child_model","title":"list_subagent_models","kind":"other","status":"in_progress","rawInput":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_list_child_model","status":"completed","content":[{"type":"content","content":{"type":"text","text":"deepseek-official/deepseek-v4-flash — deepseek-v4-flash\nReasoning efforts:\n(no advertised reasoning efforts)"}}]}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_configured_effort","title":"subagent","kind":"other","status":"in_progress","rawInput":{"description":"Validate configured effort","prompt":"This child must never start.","provider":"deepseek-official","model":"deepseek-v4-flash","run_in_background":false}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_configured_effort","status":"failed","content":[{"type":"content","content":{"type":"text","text":"Error: provider \"deepseek-official\" model \"deepseek-v4-flash\" does not support reasoning effort \"unsupported\""}}]}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","messageId":"{{messageId}}","content":{"type":"text","text":"CONFIGURED_EFFORT_REJECTED"}}}} +{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-continuable-inheritance/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-continuable-inheritance/session.1.jsonl index 841bdb291d..d2d2f47853 100644 --- a/examples/acp-agent/tests/snapshots/subagent-continuable-inheritance/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-continuable-inheritance/session.1.jsonl @@ -1,5 +1,5 @@ {"type":"session","version":0,"id":"33333333-3333-4333-8333-333333333333","createdAt":1789000001000,"cwd":"{{cwd}}","parentSession":"11111111-1111-4111-8111-111111111111","origin":"subagent","delegationDepth":1} -{"type":"subagent/descriptor","data":{"version":2,"mode":"continuable","provider":"spawn","label":"Reply with CHILD_OK","agentProvider":"deepseek-official","agentModel":"deepseek-v4-flash"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"continuable","provider":"spawn","label":"Reply with CHILD_OK","agentProvider":"deepseek-official","agentModel":"deepseek-v4-flash"}} {"type":"session/end-seed","data":{}} {"type":"sandbox/mode","data":{"mode":"read-only","source":"delegation"}} {"type":"approval/policy","data":{"policy":"never","source":"delegation"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-continuable-inheritance/tool-schemas.1.expected.json b/examples/acp-agent/tests/snapshots/subagent-continuable-inheritance/tool-schemas.1.expected.json index 38f4eae1ad..62937be9b1 100644 --- a/examples/acp-agent/tests/snapshots/subagent-continuable-inheritance/tool-schemas.1.expected.json +++ b/examples/acp-agent/tests/snapshots/subagent-continuable-inheritance/tool-schemas.1.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's advertised models." + } + } + } + }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", @@ -425,7 +442,7 @@ }, { "name": "subagent", - "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.", + "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -437,6 +454,18 @@ "type": "string", "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs." }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + }, "run_in_background": { "type": "boolean", "description": "Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it." diff --git a/examples/acp-agent/tests/snapshots/subagent-continuable/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-continuable/session.1.jsonl index a03acfdecb..685168de6b 100644 --- a/examples/acp-agent/tests/snapshots/subagent-continuable/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-continuable/session.1.jsonl @@ -1,5 +1,5 @@ {"type":"session","version":0,"id":"33333333-3333-4333-8333-333333333333","createdAt":1789000001000,"cwd":"{{cwd}}","parentSession":"11111111-1111-4111-8111-111111111111","origin":"subagent","delegationDepth":1} -{"type":"subagent/descriptor","data":{"version":2,"mode":"continuable","provider":"spawn","label":"Reply with CHILD_OK","agentProvider":"deepseek-official","agentModel":"deepseek-v4-flash"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"continuable","provider":"spawn","label":"Reply with CHILD_OK","agentProvider":"deepseek-official","agentModel":"deepseek-v4-flash"}} {"type":"session/end-seed","data":{}} {"type":"sandbox/mode","data":{"mode":"danger-full-access","source":"delegation"}} {"type":"approval/policy","data":{"policy":"never","source":"delegation"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-continuable/tool-schemas.1.expected.json b/examples/acp-agent/tests/snapshots/subagent-continuable/tool-schemas.1.expected.json index 38f4eae1ad..62937be9b1 100644 --- a/examples/acp-agent/tests/snapshots/subagent-continuable/tool-schemas.1.expected.json +++ b/examples/acp-agent/tests/snapshots/subagent-continuable/tool-schemas.1.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's advertised models." + } + } + } + }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", @@ -425,7 +442,7 @@ }, { "name": "subagent", - "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.", + "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -437,6 +454,18 @@ "type": "string", "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs." }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + }, "run_in_background": { "type": "boolean", "description": "Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it." diff --git a/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.1.jsonl index b64494a028..d59385affa 100644 --- a/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.1.jsonl @@ -5,10 +5,10 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Call subagent once. Ask that child to attempt one further subagent call, then report the result."}],"source":{"kind":"user"},"role":"user","id":"a8129357-1bde-4cbd-90b4-6b8ad51d52e1"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} -{"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Start depth one"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn","label":"Start depth one"}} {"type":"step/start","data":{"turn":1,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Call subagent once. Ask that child to attempt one further subagent call, then report the result."}],"source":{"kind":"user"},"role":"user","id":"a8129357-1bde-4cbd-90b4-6b8ad51d52e1"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"72272791-eefd-48f8-94da-02b132ae9d2a"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"f544ed7b-5a1f-4b6e-93b5-6af8342385fc"},"surfaceOp":"append"} {"type":"session/title","data":{"title":"Call subagent once. Ask that","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.2.jsonl b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.2.jsonl index 3e3dfa357b..362fed6c4e 100644 --- a/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.2.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.2.jsonl @@ -5,10 +5,10 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Attempt one subagent call beyond the configured cap, then report the rejection."}],"source":{"kind":"user"},"role":"user","id":"d4dc5a16-e542-4dd9-8e82-e6b7829cfc4b"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} -{"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Start depth two"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn","label":"Start depth two"}} {"type":"step/start","data":{"turn":1,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Attempt one subagent call beyond the configured cap, then report the rejection."}],"source":{"kind":"user"},"role":"user","id":"d4dc5a16-e542-4dd9-8e82-e6b7829cfc4b"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"c54120cc-6a7f-41f6-a71d-42b4805fa2ca"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"5d344fef-f707-49ea-b804-ac384bf52700"},"surfaceOp":"append"} {"type":"session/title","data":{"title":"Attempt one subagent call beyond","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-fork-in-process/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-fork-in-process/session.1.jsonl index 57bbed4d87..36c2654033 100644 --- a/examples/acp-agent/tests/snapshots/subagent-fork-in-process/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-fork-in-process/session.1.jsonl @@ -28,7 +28,7 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else."}],"source":{"kind":"user"},"role":"user","id":"d037163e-ed56-4c9c-b5d1-57df017d618c"}]}} {"type":"turn/start","data":{"turn":2}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} -{"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"fork","label":"Recall project codeword"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"fork","label":"Recall project codeword"}} {"type":"step/start","data":{"turn":2,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else."}],"source":{"kind":"user"},"role":"user","id":"d037163e-ed56-4c9c-b5d1-57df017d618c"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"257e572f-6f95-48f9-b3d7-4ea8b162f374"},"surfaceOp":"append"} diff --git a/examples/acp-agent/tests/snapshots/subagent-list-agents/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-list-agents/session.1.jsonl index b80c3d4936..2e6847126b 100644 --- a/examples/acp-agent/tests/snapshots/subagent-list-agents/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-list-agents/session.1.jsonl @@ -1,5 +1,5 @@ {"type":"session","version":0,"id":"33333333-3333-4333-8333-333333333333","createdAt":1789000001000,"cwd":"{{cwd}}","parentSession":"11111111-1111-4111-8111-111111111111","origin":"subagent","delegationDepth":1} -{"type":"subagent/descriptor","data":{"version":2,"mode":"continuable","provider":"spawn","label":"Reply with CHILD_OK","agentProvider":"deepseek-official","agentModel":"deepseek-v4-flash"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"continuable","provider":"spawn","label":"Reply with CHILD_OK","agentProvider":"deepseek-official","agentModel":"deepseek-v4-flash"}} {"type":"session/end-seed","data":{}} {"type":"sandbox/mode","data":{"mode":"danger-full-access","source":"delegation"}} {"type":"approval/policy","data":{"policy":"never","source":"delegation"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-list-agents/tool-schemas.1.expected.json b/examples/acp-agent/tests/snapshots/subagent-list-agents/tool-schemas.1.expected.json index 38f4eae1ad..62937be9b1 100644 --- a/examples/acp-agent/tests/snapshots/subagent-list-agents/tool-schemas.1.expected.json +++ b/examples/acp-agent/tests/snapshots/subagent-list-agents/tool-schemas.1.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's advertised models." + } + } + } + }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", @@ -425,7 +442,7 @@ }, { "name": "subagent", - "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.", + "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -437,6 +454,18 @@ "type": "string", "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs." }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + }, "run_in_background": { "type": "boolean", "description": "Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it." diff --git a/examples/acp-agent/tests/snapshots/subagent-max-tokens-partial/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-max-tokens-partial/session.1.jsonl index 4de198e6f6..def81f79a2 100644 --- a/examples/acp-agent/tests/snapshots/subagent-max-tokens-partial/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-max-tokens-partial/session.1.jsonl @@ -5,7 +5,7 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Write the words 'partial one', call todo_write once, then keep going until you are cut off."}],"source":{"kind":"user"},"role":"user","id":"dbf0670a-79cc-4e2c-a298-c4d804e6fe61"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} -{"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Truncated child"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn","label":"Truncated child"}} {"type":"step/start","data":{"turn":1,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Write the words 'partial one', call todo_write once, then keep going until you are cut off."}],"source":{"kind":"user"},"role":"user","id":"dbf0670a-79cc-4e2c-a298-c4d804e6fe61"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"885ea744-63dd-4198-95be-267b9db94a57"},"surfaceOp":"append"} diff --git a/examples/acp-agent/tests/snapshots/subagent-mixed/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-mixed/session.1.jsonl index b64a6cfdfe..3dbae741e9 100644 --- a/examples/acp-agent/tests/snapshots/subagent-mixed/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-mixed/session.1.jsonl @@ -5,10 +5,10 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"73ce401a-faaf-408a-879e-7485380d537d"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} -{"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Reply ALPHA only"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn","label":"Reply ALPHA only"}} {"type":"step/start","data":{"turn":1,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"73ce401a-faaf-408a-879e-7485380d537d"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"f216ca0e-6dcc-4ab3-9cdb-fe38d3dacca2"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"507aa273-ce20-4aaa-9a35-abaae2a5b1cf"},"surfaceOp":"append"} {"type":"session/title","data":{"title":"Reply with exactly the word","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-mixed/session.2.jsonl b/examples/acp-agent/tests/snapshots/subagent-mixed/session.2.jsonl index e7de6e1e7d..c623192004 100644 --- a/examples/acp-agent/tests/snapshots/subagent-mixed/session.2.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-mixed/session.2.jsonl @@ -28,10 +28,10 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else."}],"source":{"kind":"user"},"role":"user","id":"86e9f144-764f-460d-b72b-262cffe43d77"}]}} {"type":"turn/start","data":{"turn":2}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} -{"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"fork","label":"Recall project codeword"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"fork","label":"Recall project codeword"}} {"type":"step/start","data":{"turn":2,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else."}],"source":{"kind":"user"},"role":"user","id":"86e9f144-764f-460d-b72b-262cffe43d77"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"ac4f4d97-639d-4ad0-a513-219a58355531"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"cf2e06ce-6ea9-451a-bb75-46e59c7a78be"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"resume"}} {"type":"assistant/chunk","data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} {"type":"reasoning-chunks","data":{"turn":2,"step":1,"index":0,"dt":[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," is"," asking"," me"," to"," recall"," the"," project"," cod","ew","ord"," that"," was"," mentioned"," earlier"," in"," the"," conversation","."," I"," was"," told"," to"," remember"," it",":"," SA","FF","RON","."]}} diff --git a/examples/acp-agent/tests/snapshots/subagent-multi/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-multi/session.1.jsonl index e1b186cd1f..dd6140142a 100644 --- a/examples/acp-agent/tests/snapshots/subagent-multi/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-multi/session.1.jsonl @@ -5,10 +5,10 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"a287f842-f6f2-4a17-ab4c-820e41f498d5"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} -{"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Return ALPHA only"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn","label":"Return ALPHA only"}} {"type":"step/start","data":{"turn":1,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"a287f842-f6f2-4a17-ab4c-820e41f498d5"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"47cdc6a0-a8c8-4842-964a-ad4bc97dc76a"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"ed6eaae0-f071-44ea-9d95-d68185f87194"},"surfaceOp":"append"} {"type":"session/title","data":{"title":"Reply with exactly the word","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-multi/session.2.jsonl b/examples/acp-agent/tests/snapshots/subagent-multi/session.2.jsonl index f8cb2e5924..4269894bfb 100644 --- a/examples/acp-agent/tests/snapshots/subagent-multi/session.2.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-multi/session.2.jsonl @@ -5,10 +5,10 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word BETA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"53f6419d-8ddc-4eee-8803-5b68411336f9"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} -{"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Return BETA only"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn","label":"Return BETA only"}} {"type":"step/start","data":{"turn":1,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly the word BETA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"53f6419d-8ddc-4eee-8803-5b68411336f9"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"f9132345-93c9-40c0-b489-5916bbca96bc"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"de519157-85ec-4e58-9d05-07b469aab403"},"surfaceOp":"append"} {"type":"session/title","data":{"title":"Reply with exactly the word","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-parallel/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-parallel/session.1.jsonl index 14b644dc33..1d1f3f1372 100644 --- a/examples/acp-agent/tests/snapshots/subagent-parallel/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-parallel/session.1.jsonl @@ -2,19 +2,19 @@ {"type":"sandbox/mode","data":{"mode":"danger-full-access","source":"delegation"}} {"type":"approval/policy","data":{"policy":"never","source":"delegation"}} {"type":"permission/preset","data":{"preset":"danger-full-access"}} -{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"fadafbc9-263b-4169-82c6-a39868629377"}]}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"e7e63c63-ff17-4f1b-a375-9aba4b477b44"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} -{"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Say the word ALPHA"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn","label":"Say the word ALPHA"}} {"type":"step/start","data":{"turn":1,"step":1}} -{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"fadafbc9-263b-4169-82c6-a39868629377"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"1d6d2982-78f7-49b9-b32d-0eb465d672b1"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"e7e63c63-ff17-4f1b-a375-9aba4b477b44"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"46bdee11-0be5-4a62-a41d-08915b210451"},"surfaceOp":"append"} {"type":"session/title","data":{"title":"Reply with exactly the word","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ALPHA"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"ALPHA"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"9ccb6b64-4dfb-47a2-9967-13ab05483998"}},"sourceEventSeqs":[13,14,15],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"ALPHA"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"e195c568-4ea2-4a14-a27c-3ab43d8000b0"}},"sourceEventSeqs":[13,14,15],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/acp-agent/tests/snapshots/subagent-parallel/session.2.jsonl b/examples/acp-agent/tests/snapshots/subagent-parallel/session.2.jsonl index 5f298c76c5..e708b437fe 100644 --- a/examples/acp-agent/tests/snapshots/subagent-parallel/session.2.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-parallel/session.2.jsonl @@ -2,19 +2,19 @@ {"type":"sandbox/mode","data":{"mode":"danger-full-access","source":"delegation"}} {"type":"approval/policy","data":{"policy":"never","source":"delegation"}} {"type":"permission/preset","data":{"preset":"danger-full-access"}} -{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"dc34a17f-fb30-4afe-a11f-a0d8a1d51658"}]}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"8c91be41-04b6-4c83-a3a6-95e323c807de"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} -{"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Say the word ALPHA"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn","label":"Say the word ALPHA"}} {"type":"step/start","data":{"turn":1,"step":1}} -{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"dc34a17f-fb30-4afe-a11f-a0d8a1d51658"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"12a26f3d-f11e-4de4-8bed-d997590d21e0"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"8c91be41-04b6-4c83-a3a6-95e323c807de"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"591f5521-ef20-4f12-be4a-420489c9355b"},"surfaceOp":"append"} {"type":"session/title","data":{"title":"Reply with exactly the word","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ALPHA"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"ALPHA"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"d368f9a5-7d0a-46f0-a7d8-10e1fafa1e74"}},"sourceEventSeqs":[13,14,15],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"ALPHA"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"d38d405b-30c9-46b4-a165-78ae723f172e"}},"sourceEventSeqs":[13,14,15],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/acp-agent/tests/snapshots/subagent-report/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-report/session.1.jsonl index dd28fc243b..e5c3fdaf72 100644 --- a/examples/acp-agent/tests/snapshots/subagent-report/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-report/session.1.jsonl @@ -1,5 +1,5 @@ {"type":"session","version":0,"id":"33333333-3333-4333-8333-333333333333","createdAt":1789000001000,"cwd":"{{cwd}}","parentSession":"11111111-1111-4111-8111-111111111111","origin":"subagent","delegationDepth":1} -{"type":"subagent/descriptor","data":{"version":2,"mode":"continuable","provider":"spawn","label":"Report a finding","agentProvider":"deepseek-official","agentModel":"deepseek-v4-flash"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"continuable","provider":"spawn","label":"Report a finding","agentProvider":"deepseek-official","agentModel":"deepseek-v4-flash"}} {"type":"session/end-seed","data":{}} {"type":"sandbox/mode","data":{"mode":"danger-full-access","source":"delegation"}} {"type":"approval/policy","data":{"policy":"never","source":"delegation"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-report/tool-schemas.1.expected.json b/examples/acp-agent/tests/snapshots/subagent-report/tool-schemas.1.expected.json index 38f4eae1ad..62937be9b1 100644 --- a/examples/acp-agent/tests/snapshots/subagent-report/tool-schemas.1.expected.json +++ b/examples/acp-agent/tests/snapshots/subagent-report/tool-schemas.1.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's advertised models." + } + } + } + }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", @@ -425,7 +442,7 @@ }, { "name": "subagent", - "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.", + "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -437,6 +454,18 @@ "type": "string", "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs." }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + }, "run_in_background": { "type": "boolean", "description": "Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it." diff --git a/examples/acp-agent/tests/snapshots/subagent-spawn-in-process/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-spawn-in-process/session.1.jsonl index efe72ff42b..a70c330e78 100644 --- a/examples/acp-agent/tests/snapshots/subagent-spawn-in-process/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-spawn-in-process/session.1.jsonl @@ -5,7 +5,7 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"54ed23d6-e960-4f36-b192-cf06e1618ea6"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} -{"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Reply with CHILD_OK"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn","label":"Reply with CHILD_OK"}} {"type":"step/start","data":{"turn":1,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"54ed23d6-e960-4f36-b192-cf06e1618ea6"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"24630f5a-f790-469f-96a6-cf234ded3759"},"surfaceOp":"append"} diff --git a/examples/acp-agent/tests/snapshots/text-turn/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/text-turn/tool-schemas.expected.json index db3c652d58..0720890967 100644 --- a/examples/acp-agent/tests/snapshots/text-turn/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/text-turn/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's advertised models." + } + } + } + }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", @@ -409,7 +426,7 @@ }, { "name": "subagent", - "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.", + "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -421,6 +438,18 @@ "type": "string", "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs." }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + }, "run_in_background": { "type": "boolean", "description": "Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it." diff --git a/examples/acp-agent/tests/snapshots/web-fetch/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/web-fetch/tool-schemas.expected.json index b2236d44a3..630a9b086f 100644 --- a/examples/acp-agent/tests/snapshots/web-fetch/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/web-fetch/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's advertised models." + } + } + } + }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", @@ -409,7 +426,7 @@ }, { "name": "subagent", - "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.", + "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -421,6 +438,18 @@ "type": "string", "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs." }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + }, "run_in_background": { "type": "boolean", "description": "Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it." diff --git a/examples/acp-agent/tests/snapshots/workflow-run/session.1.jsonl b/examples/acp-agent/tests/snapshots/workflow-run/session.1.jsonl index a42b3cf01d..fce39e713c 100644 --- a/examples/acp-agent/tests/snapshots/workflow-run/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/workflow-run/session.1.jsonl @@ -5,7 +5,7 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word WF_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"f0f46771-663a-494a-8d40-6866a5bbe7c9"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} -{"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"spawn"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn"}} {"type":"step/start","data":{"turn":1,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly the word WF_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"f0f46771-663a-494a-8d40-6866a5bbe7c9"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"12bbd4dd-4040-4cc7-8acf-e526144f1ee5"},"surfaceOp":"append"} diff --git a/examples/headless-agent/cordis.yml b/examples/headless-agent/cordis.yml index fa037ab935..759a252b4e 100644 --- a/examples/headless-agent/cordis.yml +++ b/examples/headless-agent/cordis.yml @@ -124,13 +124,17 @@ config: provider: spawn toolName: subagent + enableModelSelection: true backgroundMode: continuable maxDepth: 1 -# Fork stays one-shot because a continuable child's `report` tool and prompt -# section precede the inherited history a fork reuses; `run_in_background` is off -# as an explicit foreground-only choice even though agent-spine-demo mounts the -# generic Job runtime. See .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md. +# Fork omits model selection so provider/model stay equal to the parent and the +# inherited history remains eligible for KV Cache reuse. It stays one-shot because +# a continuable child's `report` tool and prompt section precede that history and +# invalidate the same prefix. `run_in_background` is off as an explicit +# foreground-only choice even though agent-spine-demo mounts the generic Job runtime. +# See .agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.md +# and .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md. - id: tool-subagent-fork name: '@deepseek-ai/dsh-tool-subagent' config: diff --git a/examples/headless-agent/tests/snapshots/advanced-toolchain/session.1.jsonl b/examples/headless-agent/tests/snapshots/advanced-toolchain/session.1.jsonl index 1287de6339..6455b03e27 100644 --- a/examples/headless-agent/tests/snapshots/advanced-toolchain/session.1.jsonl +++ b/examples/headless-agent/tests/snapshots/advanced-toolchain/session.1.jsonl @@ -1,19 +1,19 @@ {"type":"session","version":0,"id":"22222222-2222-4222-8222-222222222222","createdAt":1783950001000,"cwd":"{{cwd}}","parentSession":"11111111-1111-4111-8111-111111111111","origin":"subagent","delegationDepth":1} -{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"c66e310e-2597-4d01-85c8-2d70a9d831c0"}]}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"2324a504-7992-4dd5-b1a4-22783caf793c"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} -{"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Check direct child"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn","label":"Check direct child"}} {"type":"step/start","data":{"turn":1,"step":1}} -{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"c66e310e-2597-4d01-85c8-2d70a9d831c0"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"fd0a0587-8df4-46b2-809a-317346a4c0f4"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"2324a504-7992-4dd5-b1a4-22783caf793c"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"9b642d5e-ecc8-4c09-8f98-fd067b62ae63"},"surfaceOp":"append"} {"type":"session/title","data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[5],"source":{"kind":"fallback"}}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"You are an AI agent powered by DeepSeek Harness.\n\nYou are headless-agent, a coding assistant powered by the deepseek-v4-flash model. Your working directory is {{cwd}}.\n\nVerify your work by running the code or tests. Keep answers brief and factual.\n\n\nUse the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.\n\nUse the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-observation-policy requires it) and prefer edit for targeted changes.\n\nUse the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-observation-policy requires it), unless you just created or edited it in this session.\n\nCheck the [exit code: N] marker on every bash result; investigate failures before moving on.\n\nTrack every background job id you start. You are notified in-session when a job finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running job's work. Before giving a final answer, collect every still-relevant job with job_output (set wait: true only when you are genuinely blocked on it), and job_kill jobs that stopped mattering.\n\nUse the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls.\n\n# Dynamic Cordis Plugins\n\nDynamic Cordis plugins temporarily extend the current DSH process. A Plugin uses apply(ctx) to consume Services, listen to Events, provide Services, register model Tools, or register browser UI in Slots.\n\n- Plugin and Package definitions exist only in the current process. define itself does not modify repository source, configuration, or disk, and definitions do not survive a process restart.\n- The restricted execution environment prevents accidental misuse; it is not a security boundary for malicious code. Services obtained by dynamic code connect to the real runtime.\n\n## Make the user-facing plan clear first\n\n- Dynamic Cordis Plugins are one available implementation mechanism, not the default for every request. Consider whether one could help only when the user intends to design or create something, or when a temporary interface could materially aid the current work. The presence of these instructions or Tools, and discussion of Cordis itself, do not make a request a dynamic-Plugin task.\n- When Cordis is a plausible fit, infer the intended work target and lifetime from the request and conversation. Use it only when the outcome belongs to the current running harness and should be delivered as a temporary runtime extension. If that distinction is materially ambiguous, ask at most one concise question about the intended result or lifetime. Otherwise proceed with the matching workflow; do not require the user to know or choose Cordis as an implementation mechanism.\n- Once a dynamic Plugin is appropriate, decide whether the task creates a new Plugin or modifies the Plugin named by the user with @pluginId. Proceed directly when the goal is clear; do not ask for repeated confirmation.\n- Choose Host, Client, or both from the requested outcome. Do not propose a Client/browser UI when the task does not need visible page behavior, and do not avoid Client when the requested outcome is visual, interactive, or depends on page state. Host versus Client is an implementation choice; do not make the user choose it.\n- When a design direction or a potentially useful interface would materially affect the result, ask at most one concise outcome or creative-preference question and offer a few candidate directions. Otherwise proceed directly; do not conduct a multi-round interview or a complex questionnaire.\n- cordis_define only defines and presents code; it does not run it. After definition, explain the pluginId and packageId returned by the Host and whether the next step is a run or update.\n- cordis_run may require user approval. When it returns awaiting-approval, explain that the user must allow or reject it in the UI. Do not wait, retry, or claim that it is running.\n- When it returns starting, explain that the request has entered the asynchronous flow and the Client is still activating. starting does not mean success. Wait for the system to report the final result through steering context.\n- Do not request approval again after the user rejects it. After a technical failure, fix the same Plugin from its diagnostics; do not silently create a replacement Plugin.\n\n## Recommended workflow and Tools\n\nBefore creating, modifying, or repairing a Plugin, load the cordis-plugin-development Skill. The Skill provides requirement navigation, capability composition, complete examples, and troubleshooting. Treat Inspect Provider results as the source of truth for exact APIs.\n\n1. cordis_inspect_list: discover the current Host and Client Providers and their read-only query methods.\n2. cordis_inspect_query: use the returned platform, provider, method, and schema to query exact Service, Event, Builtin, Slot, Theme token, or Tool information.\n3. cordis_inspect_self: inspect the current Session's Plugins, Packages, version pointers, source, and diagnostics. Source is returned only when both pluginId and packageId are specified.\n4. cordis_define: create the first Package for a new Plugin or append an immutable Package to an existing Plugin. It defines code but does not run it.\n5. cordis_run: activate an exact Package. Use run for the first activation, restarting current, or rollback; use update to switch versions.\n6. cordis_stop: remove the current Run and pending approval request while retaining definitions, grants, and version pointers.\n7. cordis_undefine: permanently stop and delete a Plugin and all of its Packages. Use it only after confirming that the user no longer needs them.\n\n- Inspect and Catalog data only confirm capabilities, names, signatures, types, and registration protocols before code is written; they do not replace business APIs.\n- Query Service.listService and Event.listEvents without input to choose from their compact signature directories, then query the exact service or event before using it. Exact queries return the structured contract and only its referenced types.\n- At runtime, a Plugin must call real Services or listen to real Events. Do not cache, display, or depend on Inspect results as business data.\n\n## Identity, versions, and approval\n\n- pluginId identifies a Plugin that can be modified over time. For a new Plugin, submit only a semantic idPrefix of 3–6 lowercase English letters; the Host allocates the final ID.\n- packageId identifies one immutable Host/Client source version under a Plugin. To change code, define a new Package; never overwrite an old version.\n- pluginRunId identifies one activation attempt and connects its approval, Host/Client loading, private RPC, Run card, and errors.\n- currentPackageId is the most recent fully successful Package. Stopping, starting an update, or failing an update does not clear it.\n- nextPackageId is the target awaiting approval, being attempted, awaiting Client activation, or most recently failed.\n- A single check mark authorizes only the current Package; double check marks authorize future versions of the same Plugin. A grant remains in effect after a technical failure.\n- An update stops the old Run before starting the target Package. Failure does not automatically restart the old version; retry next with update or roll back to current with run.\n\nWhen the user enters @pluginId, the system injects identity, the default base Package, version pointers, and runtime status, but not source code:\n\n1. Call cordis_inspect_self(pluginId, packageId) to read the target source.\n2. Use cordis_define in existing mode to append a Package to the same Plugin.\n3. Call cordis_run in run or update mode according to the version relationship.\n\nNever silently create another Plugin for @pluginId. If the reference is unavailable because it was removed, belongs to another Session, or was lost on process restart, tell the user directly.\n\n## High-frequency errors that must be avoided\n\n### Services: ctx.get and inject\n\n- Read an optional Service with ctx.get('serviceName') by default and handle undefined.\n- Declare inject: ['serviceName'] on the returned Plugin object only when the Service is a hard dependency and the Plugin must enter waiting until Cordis reactivates it after the Service appears.\n- Read ctx.serviceName only after declaring that Service in inject. Never access an undeclared Service as a ctx property.\n\n```js\nreturn {\n inject: ['requiredService'],\n apply(ctx) {\n ctx.requiredService.someMethod()\n const optionalService = ctx.get('optionalService')\n if (optionalService !== undefined) optionalService.someMethod()\n },\n}\n```\n\n### Code: use plain JavaScript only\n\n- Host and Client code is not transformed by TypeScript, JSX, or a bundler.\n- Do not use TypeScript types, as, decorators, import, require, or JSX.\n- Client React code must use React.createElement(...); never write .\n- Do not assume that process, Buffer, window, document, fetch, native timers, or any other global is available. Query the corresponding platform's Builtins and Services first.\n\n### Data: do not serialize live data\n\n- Services, Events, Slots, Sessions, and their derived Cordis/DSH objects are internal live data, not ordinary JSON that can be dumped.\n- Do not apply JSON.stringify, structuredClone, recursive enumeration, full copying, or whole-object display to live data.\n- Read only the leaf fields required by the task, then construct the smallest owned data object without Host references.\n\n### Lifecycle: every side effect must be reversible\n\n- Services, Events, Tools, handlers, timers, Slots, styles, and theme overrides must all belong to the current Fiber.\n- Use ctx.effect(), ctx.on(), or official APIs that return a disposer so stop, update, or undefine removes every side effect.\n- The cordis-plugin-development Skill contains complete timer, Waterfall, Slot, theme, Tool, RPC, and React examples and troubleshooting guidance.\n\n## Host and Client\n\n- Host runs in the DSH Node.js process and is appropriate for files, networking, commands, Agent/Session access, Host Events, Services, model Tools, and JSON methods callable by the Client.\n- Client runs in the browser page and is appropriate for themes, layout, current page state, Tool cards, and Slot UI.\n- Host and Client communicate through Package-private JSON methods: Host uses harness.handle(method, handler), and Client uses host.call(method, args). The direction is Client→Host, and only lossless JSON may cross it.\n- Client UI must be registered in a queried Slot; apply() cannot directly return a React Element. Query Slots.listSubTree without root to choose from the compact purpose/topology tree, then query the exact root for its full registration contract and props before writing code.\n- See the Skill and Inspect Providers for Run-specific panels and exact Slot registration patterns.\n\n## Asynchronous results and recovery\n\n- Do not wait inside a Tool for approval or browser work that can happen only after the current turn ends.\n- Asynchronous success, rejection, and runtime errors update Run state and notify you through steering context.\n- After a technical failure, use cordis_inspect_self to read the exact Package source and its message/stack. Define a corrected Package under the same Plugin and retry autonomously.\n- Use the cordis-plugin-development Skill for other failure causes, repair procedures, and complete extension patterns.\n\nUse the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.\n\nUse subagent in the background by default. Start independent delegations together in one assistant message and continue useful work while they run. Set `run_in_background: false` only when your next action depends on that subagent's result. When a background run settles, the runtime sends you a notice containing its outcome and any final assistant message.\n\n## Writing code for run_code\n\n`run_code` takes two required arguments: `code` — the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped) — and `description`, a short summary of what the program does. Inside the program:\n\n- Call tools as `await tools.name(args)` — quoted access for exotic names: `tools[\"my-tool\"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.\n- A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue.\n- Independent read-only calls MAY overlap under `Promise.all` (safe calls run concurrently; mutating calls run alone, in submission order). Sequence dependent work with `await`.\n- Emit results with `return` and/or `console.log(...)`. ONLY what you print or return comes back to you — intermediate tool results never enter the conversation, so extract just what you need.\n\nThe available tools:\n\n```ts\ntype JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }\n\ninterface ToolArgsMap {\n /** Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a job id immediately; read its output with `job_output` and stop it with `job_kill`. */\n bash: {\n /** The bash command to execute. */\n command: string;\n /** Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\". */\n description: string;\n /** Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry. */\n timeoutMs?: number;\n /** Working directory for this command. Defaults to the session workspace; a relative path is resolved against it. */\n workdir?: string;\n /** Run in the background and return a job id immediately (collect with job_output, stop with job_kill). No timeout applies. */\n run_in_background?: boolean;\n } & Record;\n /** Define an immutable Cordis Package. For a new Plugin, use kind:\"new\" and provide only a semantic prefix of 3–6 lowercase English letters; the Host returns the final pluginId and packageId. To modify an existing Plugin, use kind:\"existing\" with its exact pluginId to append a Package without overwriting older versions. Provide at least one of code.host and code.client. Each value is a plain JavaScript function body that returns a Cordis Plugin; no TypeScript, JSX, or import transformation occurs. Query Inspect before depending on a Service, Event, Builtin, Slot, or token. Define only validates parameters and syntax and records source: it does not request approval, execute apply, or change currentPackageId. On success, call cordis_run with the returned IDs. */\n cordis_define: {\n plugin: {\n kind: \"new\";\n /** Suggested semantic prefix of 3–6 lowercase English letters; the Host adds a unique numeric suffix. */\n idPrefix: string;\n } | {\n kind: \"existing\";\n /** Exact ID of an existing Plugin; the new Package is appended to that instance. */\n pluginId: string;\n };\n /** Short, readable Package name. */\n name: string;\n /** One-sentence, user-facing description of the Package purpose. */\n purpose: string;\n code: {\n /** Plain JavaScript function body that returns the Host-half Cordis Plugin. */\n host?: string;\n /** Plain JavaScript function body that returns the browser Client-half Cordis Plugin. */\n client?: string;\n };\n } & Record;\n /** List every Cordis Inspect Provider currently known to the Host, including local Host Providers and the latest manifests synchronized from the Client. Each entry includes its platform, purpose, read-only methods, and input/output schemas. Call this Tool before creating or modifying a Package, then select the provider and method for cordis_inspect_query from its result. Do not guess names or treat an Inspect method as a business Service that Plugin code can call. */\n cordis_inspect_list: Record;\n /** Run a read-only query explicitly declared by an Inspect Provider. platform, provider, and method must come from cordis_inspect_list, and input must satisfy that method's schema. Use this Tool before cordis_define to read exact Service methods, Event modes, Builtin signatures, Tool schemas, theme tokens, or live Slot trees and props. Host queries run locally. A Client query waits for the first valid page response and remains pending until a page answers or the Tool is cancelled. This Tool cannot invoke business Service methods or modify the runtime. For Service.listService and Event.listEvents, query without input to navigate the compact signature directory, then query the exact service or event for its structured contract and referenced types. For Slots.listSubTree, query without root to navigate the compact tree, then query the exact root for its complete registration contract and props. */\n cordis_inspect_query: {\n /** Runtime platform that owns the Provider. */\n platform: \"host\" | \"client\";\n /** Exact Provider ID returned by cordis_inspect_list. */\n provider: string;\n /** Exact method name declared by the Provider manifest. */\n method: string;\n /** Optional query input; it must satisfy the method input schema. */\n input?: JsonValue;\n } & Record;\n /** Inspect dynamic Cordis objects owned by the current Session at increasing levels of detail. With no IDs, list only Plugin summaries. With pluginId alone, return version pointers, the latest Run, and every Package summary. Only pluginId plus packageId returns that immutable Package's Host/Client source and runtime diagnostics. packageId cannot be supplied alone. Query an exact Package before handling @pluginId, repairing an asynchronous failure, or defining an updated version. This Tool is read-only: it neither executes code nor changes version pointers. */\n cordis_inspect_self: {\n /** Stable Plugin ID returned by cordis_define or injected by @pluginId; omit it to list every current Plugin. */\n pluginId?: string;\n /** Exact immutable Package ID owned by pluginId; when specified, source and diagnostics are returned. */\n packageId?: string;\n } & Record;\n /** Activate one exact Package of a dynamic Plugin. Use mode:\"run\" for the first activation, restarting currentPackageId, or rollback. When current exists, use mode:\"update\" to switch to a different Package, even if the Plugin is currently stopped. An unauthorized Client Package creates an approval request and returns awaiting-approval; an authorized Package returns starting and continues asynchronously in the browser. Neither result waits for the final outcome inside the Tool. currentPackageId changes only after complete success; on failure, the old current and target next remain. Asynchronous success, rejection, or technical failure is reported through state and steering. After a technical failure, read diagnostics with cordis_inspect_self, correct the same Plugin, and retry autonomously. Do not request approval again after the user rejects it. */\n cordis_run: {\n /** Stable Plugin ID returned by cordis_define. */\n pluginId: string;\n /** Exact immutable Package ID to activate under that Plugin. */\n packageId: string;\n /** Use run for the first activation, restarting current, or rollback; use update to switch from current to a different Package. */\n mode: \"run\" | \"update\";\n } & Record;\n /** Stop the current Run of a dynamic Plugin and cancel unfinished approval or activation requests. Retain the Plugin, every immutable Package, grants, currentPackageId, and nextPackageId so it can later run or update directly. Stopping an already stopped Plugin succeeds idempotently. Use this Tool to disable effects temporarily; use cordis_undefine for permanent removal. */\n cordis_stop: {\n /** Stable dynamic Plugin ID to stop. */\n pluginId: string;\n } & Record;\n /** Permanently remove a dynamic Plugin owned by the current Session. If it is running or awaiting approval, first stop it and cancel the request, then delete every Package, grant, and version pointer. After this returns, its pluginId, packageIds, @ reference, and Package business views are invalid; historical cards retain only a \"Plugin removed\" record. Do not call this Tool when versions must remain available for restart or rollback; use cordis_stop instead. */\n cordis_undefine: {\n /** Stable dynamic Plugin ID to remove permanently. */\n pluginId: string;\n } & Record;\n /** Edit an existing UTF-8 text file by replacing literal text. */\n edit: {\n /** Path to edit, resolved by the filesystem backend. */\n file_path: string;\n /** Literal text to replace. Must match exactly. */\n old_string: string;\n /** Literal replacement text. Use an empty string to delete the match. */\n new_string: string;\n /** Replace all matches. Defaults to false; when false, old_string must appear exactly once. */\n replace_all?: boolean;\n } & Record;\n /** Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op. */\n interrupt_agent: {\n /** The agent id of the running agent to interrupt. */\n agent_id: string;\n } & Record;\n /** Request cancellation of a running background job by job id. Returns immediately; the job settles as killed once its work actually stops. */\n job_kill: {\n /** Job id returned by the tool that started the background work. */\n job_id: string;\n /** Optional short reason, recorded in the log and forwarded to the job. */\n reason?: string;\n } & Record;\n /** List your background jobs (running and finished) with their ids, kinds, and statuses. */\n job_list: Record;\n /** Read a background job. Stream jobs return only output since the previous read; final-output jobs return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap. */\n job_output: {\n /** Job id returned by the tool that started the background work. */\n job_id: string;\n /** Block until the job reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the job alive. */\n wait?: boolean;\n /** Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum. */\n timeout_ms?: number;\n } & Record;\n /** Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools. */\n ralph: {\n /** The immutable completion objective for every fresh Ralph round. */\n objective: string;\n /** Optional positive safe-integer round cap, bounded by the deployment ceiling. */\n maxRounds?: number;\n } & Record;\n /** Read a UTF-8 text file and return line-numbered content. */\n read: {\n /** Path to read, resolved by the filesystem backend. */\n file_path: string;\n /** 1-based first line to return. Defaults to 1. */\n offset?: number;\n /** Maximum number of lines to return. Defaults to 2000. */\n limit?: number;\n } & Record;\n /** Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered. */\n send_message: {\n /** The subagent id returned when the background subagent was started. */\n subagent_id: string;\n /** The message to deliver to the subagent. */\n message: string;\n } & Record;\n /** Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill. */\n skill: {\n /** The exact skill name from the available skills list. */\n name: string;\n } & Record;\n /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. */\n subagent: {\n /** A short (3-5 word) description of the delegated task, for display. */\n description: string;\n /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */\n prompt: string;\n /** Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it. */\n run_in_background?: boolean;\n } & Record;\n /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result. */\n subagent_fork: {\n /** A short (3-5 word) description of the delegated task, for display. */\n description: string;\n /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */\n prompt: string;\n } & Record;\n /** Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished). */\n todo_write: {\n /** The COMPLETE task list, replacing any previous list. */\n todos: ({\n /** What the task is — a short imperative line. */\n content: string;\n /** pending (not started) | in_progress (now) | completed (done). */\n status: \"pending\" | \"in_progress\" | \"completed\";\n })[];\n } & Record;\n /** Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn. The workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result. Script-body hooks: - `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly. - `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages. - `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`. - `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim. Misused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`. Constraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes. */\n workflow: {\n /** The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `). */\n script: string;\n /** The workflow identity block (plain JSON — never code). */\n meta: {\n /** Short kebab-case workflow name. */\n name: string;\n /** One-line description of what the workflow does. */\n description: string;\n /** Optional guidance on when this workflow applies. */\n whenToUse?: string;\n /** Optional phase declarations matched by phase() calls. */\n phases?: ({\n /** The phase title phase() calls match by exact string. */\n title: string;\n /** Optional one-line description of the phase. */\n detail?: string;\n /** Optional provider override this phase is expected to use. */\n provider?: string;\n /** Optional model override this phase is expected to use. */\n model?: string;\n } & Record)[];\n } & Record;\n /** Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}). */\n args?: Record;\n } & Record;\n /** Create or fully replace a UTF-8 text file. */\n write: {\n /** Path to write, resolved by the filesystem backend. */\n file_path: string;\n /** Full UTF-8 text content to write. */\n content: string;\n } & Record;\n}\n\ninterface ToolOutputMap {\n bash: {\n kind: \"background\";\n jobId: string;\n } | {\n kind: \"foreground\";\n exitCode: number | null;\n signal: string | null;\n timedOut: boolean;\n aborted: boolean;\n timeoutMs: number;\n stdout: {\n text: string;\n truncated: boolean;\n spillPath?: string;\n };\n stderr: {\n text: string;\n truncated: boolean;\n spillPath?: string;\n };\n sandbox?: {\n mode: string;\n denied: boolean;\n enforcement?: string;\n runnerFailed?: boolean;\n };\n };\n cordis_define: {\n pluginId: string;\n packageId: string;\n name: string;\n purpose: string;\n hasHostHalf: boolean;\n hasClientHalf: boolean;\n };\n cordis_inspect_list: JsonValue;\n cordis_inspect_query: JsonValue;\n cordis_inspect_self: JsonValue;\n cordis_run: JsonValue;\n cordis_stop: {\n pluginId: string;\n };\n cordis_undefine: {\n pluginId: string;\n wasRunning: boolean;\n };\n edit: {\n path: string;\n before: string;\n after: string;\n };\n interrupt_agent: {\n accepted: boolean;\n };\n job_kill: {\n outcome: \"cancellation-requested\" | \"already-finished\";\n job: {\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n };\n };\n job_list: ({\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n })[];\n job_output: {\n text: string;\n job: {\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n };\n };\n ralph: {\n runId: string;\n agentsStarted: number;\n result: JsonValue;\n };\n read: {\n path: string;\n offset: number;\n lines: {\n number: number;\n text: string;\n }[];\n totalLines: number;\n };\n send_message: {\n messageId: string;\n };\n skill: {\n name: string;\n provider: string;\n resourceBase?: {\n kind: \"directory\";\n path: string;\n } | {\n kind: \"url\";\n url: string;\n } | {\n kind: \"opaque\";\n description: string;\n };\n content: string;\n };\n subagent: {\n kind: \"background\";\n jobId: string;\n } | {\n kind: \"continuable\";\n subagentId: string;\n } | {\n kind: \"foreground\";\n runId: string;\n output: JsonValue[];\n };\n subagent_fork: {\n kind: \"background\";\n jobId: string;\n } | {\n kind: \"continuable\";\n subagentId: string;\n } | {\n kind: \"foreground\";\n runId: string;\n output: JsonValue[];\n };\n todo_write: {\n todos: ({\n content: string;\n status: \"pending\" | \"in_progress\" | \"completed\";\n })[];\n counts: {\n pending: number;\n inProgress: number;\n completed: number;\n };\n };\n workflow: {\n runId: string;\n agentsStarted: number;\n result: JsonValue;\n };\n write: {\n path: string;\n operation: \"create\" | \"update\";\n before: string | null;\n after: string;\n };\n}\n\ntype ToolName = keyof ToolOutputMap\n\ndeclare class ToolCallError extends Error {\n readonly name: \"ToolCallError\";\n readonly toolName: ToolName;\n}\n\ndeclare const tools: {\n [K in ToolName]: (args: ToolArgsMap[K]) => Promise;\n}\n```","tools":[{"name":"bash","description":"Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a job id immediately; read its output with `job_output` and stop it with `job_kill`.","parameters":{"type":"object","properties":{"command":{"type":"string","description":"The bash command to execute."},"description":{"type":"string","description":"Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."},"timeoutMs":{"type":"number","description":"Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."},"workdir":{"type":"string","description":"Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."},"run_in_background":{"type":"boolean","description":"Run in the background and return a job id immediately (collect with job_output, stop with job_kill). No timeout applies."}},"required":["command","description"]}},{"name":"cordis_define","description":"Define an immutable Cordis Package. For a new Plugin, use kind:\"new\" and provide only a semantic prefix of 3–6 lowercase English letters; the Host returns the final pluginId and packageId. To modify an existing Plugin, use kind:\"existing\" with its exact pluginId to append a Package without overwriting older versions. Provide at least one of code.host and code.client. Each value is a plain JavaScript function body that returns a Cordis Plugin; no TypeScript, JSX, or import transformation occurs. Query Inspect before depending on a Service, Event, Builtin, Slot, or token. Define only validates parameters and syntax and records source: it does not request approval, execute apply, or change currentPackageId. On success, call cordis_run with the returned IDs.","parameters":{"type":"object","properties":{"plugin":{"oneOf":[{"type":"object","additionalProperties":false,"properties":{"kind":{"type":"string","const":"new"},"idPrefix":{"type":"string","description":"Suggested semantic prefix of 3–6 lowercase English letters; the Host adds a unique numeric suffix."}},"required":["kind","idPrefix"]},{"type":"object","additionalProperties":false,"properties":{"kind":{"type":"string","const":"existing"},"pluginId":{"type":"string","description":"Exact ID of an existing Plugin; the new Package is appended to that instance."}},"required":["kind","pluginId"]}]},"name":{"type":"string","description":"Short, readable Package name."},"purpose":{"type":"string","description":"One-sentence, user-facing description of the Package purpose."},"code":{"type":"object","additionalProperties":false,"properties":{"host":{"type":"string","description":"Plain JavaScript function body that returns the Host-half Cordis Plugin."},"client":{"type":"string","description":"Plain JavaScript function body that returns the browser Client-half Cordis Plugin."}}}},"required":["plugin","name","purpose","code"]}},{"name":"cordis_inspect_list","description":"List every Cordis Inspect Provider currently known to the Host, including local Host Providers and the latest manifests synchronized from the Client. Each entry includes its platform, purpose, read-only methods, and input/output schemas. Call this Tool before creating or modifying a Package, then select the provider and method for cordis_inspect_query from its result. Do not guess names or treat an Inspect method as a business Service that Plugin code can call.","parameters":{"type":"object","properties":{}}},{"name":"cordis_inspect_query","description":"Run a read-only query explicitly declared by an Inspect Provider. platform, provider, and method must come from cordis_inspect_list, and input must satisfy that method's schema. Use this Tool before cordis_define to read exact Service methods, Event modes, Builtin signatures, Tool schemas, theme tokens, or live Slot trees and props. Host queries run locally. A Client query waits for the first valid page response and remains pending until a page answers or the Tool is cancelled. This Tool cannot invoke business Service methods or modify the runtime. For Service.listService and Event.listEvents, query without input to navigate the compact signature directory, then query the exact service or event for its structured contract and referenced types. For Slots.listSubTree, query without root to navigate the compact tree, then query the exact root for its complete registration contract and props.","parameters":{"type":"object","properties":{"platform":{"type":"string","description":"Runtime platform that owns the Provider.","enum":["host","client"]},"provider":{"type":"string","description":"Exact Provider ID returned by cordis_inspect_list."},"method":{"type":"string","description":"Exact method name declared by the Provider manifest."},"input":{"description":"Optional query input; it must satisfy the method input schema."}},"required":["platform","provider","method"]}},{"name":"cordis_inspect_self","description":"Inspect dynamic Cordis objects owned by the current Session at increasing levels of detail. With no IDs, list only Plugin summaries. With pluginId alone, return version pointers, the latest Run, and every Package summary. Only pluginId plus packageId returns that immutable Package's Host/Client source and runtime diagnostics. packageId cannot be supplied alone. Query an exact Package before handling @pluginId, repairing an asynchronous failure, or defining an updated version. This Tool is read-only: it neither executes code nor changes version pointers.","parameters":{"type":"object","properties":{"pluginId":{"type":"string","description":"Stable Plugin ID returned by cordis_define or injected by @pluginId; omit it to list every current Plugin."},"packageId":{"type":"string","description":"Exact immutable Package ID owned by pluginId; when specified, source and diagnostics are returned."}}}},{"name":"cordis_run","description":"Activate one exact Package of a dynamic Plugin. Use mode:\"run\" for the first activation, restarting currentPackageId, or rollback. When current exists, use mode:\"update\" to switch to a different Package, even if the Plugin is currently stopped. An unauthorized Client Package creates an approval request and returns awaiting-approval; an authorized Package returns starting and continues asynchronously in the browser. Neither result waits for the final outcome inside the Tool. currentPackageId changes only after complete success; on failure, the old current and target next remain. Asynchronous success, rejection, or technical failure is reported through state and steering. After a technical failure, read diagnostics with cordis_inspect_self, correct the same Plugin, and retry autonomously. Do not request approval again after the user rejects it.","parameters":{"type":"object","properties":{"pluginId":{"type":"string","description":"Stable Plugin ID returned by cordis_define."},"packageId":{"type":"string","description":"Exact immutable Package ID to activate under that Plugin."},"mode":{"type":"string","description":"Use run for the first activation, restarting current, or rollback; use update to switch from current to a different Package.","enum":["run","update"]}},"required":["pluginId","packageId","mode"]}},{"name":"cordis_stop","description":"Stop the current Run of a dynamic Plugin and cancel unfinished approval or activation requests. Retain the Plugin, every immutable Package, grants, currentPackageId, and nextPackageId so it can later run or update directly. Stopping an already stopped Plugin succeeds idempotently. Use this Tool to disable effects temporarily; use cordis_undefine for permanent removal.","parameters":{"type":"object","properties":{"pluginId":{"type":"string","description":"Stable dynamic Plugin ID to stop."}},"required":["pluginId"]}},{"name":"cordis_undefine","description":"Permanently remove a dynamic Plugin owned by the current Session. If it is running or awaiting approval, first stop it and cancel the request, then delete every Package, grant, and version pointer. After this returns, its pluginId, packageIds, @ reference, and Package business views are invalid; historical cards retain only a \"Plugin removed\" record. Do not call this Tool when versions must remain available for restart or rollback; use cordis_stop instead.","parameters":{"type":"object","properties":{"pluginId":{"type":"string","description":"Stable dynamic Plugin ID to remove permanently."}},"required":["pluginId"]}},{"name":"edit","description":"Edit an existing UTF-8 text file by replacing literal text.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to edit, resolved by the filesystem backend."},"old_string":{"type":"string","description":"Literal text to replace. Must match exactly."},"new_string":{"type":"string","description":"Literal replacement text. Use an empty string to delete the match."},"replace_all":{"type":"boolean","description":"Replace all matches. Defaults to false; when false, old_string must appear exactly once."}},"required":["file_path","old_string","new_string"]}},{"name":"interrupt_agent","description":"Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op.","parameters":{"type":"object","properties":{"agent_id":{"type":"string","description":"The agent id of the running agent to interrupt."}},"required":["agent_id"]}},{"name":"job_kill","description":"Request cancellation of a running background job by job id. Returns immediately; the job settles as killed once its work actually stops.","parameters":{"type":"object","properties":{"job_id":{"type":"string","description":"Job id returned by the tool that started the background work."},"reason":{"type":"string","description":"Optional short reason, recorded in the log and forwarded to the job."}},"required":["job_id"]}},{"name":"job_list","description":"List your background jobs (running and finished) with their ids, kinds, and statuses.","parameters":{"type":"object","properties":{}}},{"name":"job_output","description":"Read a background job. Stream jobs return only output since the previous read; final-output jobs return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.","parameters":{"type":"object","properties":{"job_id":{"type":"string","description":"Job id returned by the tool that started the background work."},"wait":{"type":"boolean","description":"Block until the job reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the job alive."},"timeout_ms":{"type":"number","description":"Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."}},"required":["job_id"]}},{"name":"ralph","description":"Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.","parameters":{"type":"object","properties":{"objective":{"type":"string","description":"The immutable completion objective for every fresh Ralph round."},"maxRounds":{"type":"number","description":"Optional positive safe-integer round cap, bounded by the deployment ceiling."}},"required":["objective"]}},{"name":"read","description":"Read a UTF-8 text file and return line-numbered content.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to read, resolved by the filesystem backend."},"offset":{"type":"number","description":"1-based first line to return. Defaults to 1."},"limit":{"type":"number","description":"Maximum number of lines to return. Defaults to 2000."}},"required":["file_path"]}},{"name":"run_code","description":"Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the BODY of an async function (erasable syntax only; top-level `await` and `return` work), and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it.","parameters":{"type":"object","properties":{"code":{"type":"string","description":"The program: the body of an async TypeScript function."},"description":{"type":"string","description":"Clear, concise description of what this program does in active voice, 5-10 words (shown in the UI). Examples: \"Count TODO markers across packages\"; \"Read failing test and its fixture\"; \"Rename config key in every cordis.yml\"."}},"required":["code","description"]}},{"name":"send_message","description":"Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered.","parameters":{"type":"object","properties":{"subagent_id":{"type":"string","description":"The subagent id returned when the background subagent was started."},"message":{"type":"string","description":"The message to deliver to the subagent."}},"required":["subagent_id","message"]}},{"name":"skill","description":"Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.","parameters":{"type":"object","properties":{"name":{"type":"string","description":"The exact skill name from the available skills list."}},"required":["name"]}},{"name":"subagent","description":"Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."},"run_in_background":{"type":"boolean","description":"Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it."}},"required":["description","prompt"]}},{"name":"subagent_fork","description":"Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."}},"required":["description","prompt"]}},{"name":"todo_write","description":"Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).","parameters":{"type":"object","properties":{"todos":{"type":"array","description":"The COMPLETE task list, replacing any previous list.","items":{"type":"object","additionalProperties":false,"properties":{"content":{"type":"string","description":"What the task is — a short imperative line."},"status":{"type":"string","description":"pending (not started) | in_progress (now) | completed (done).","enum":["pending","in_progress","completed"]}},"required":["content","status"]}}},"required":["todos"]}},{"name":"workflow","description":"Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.\n\nThe workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result.\n\nScript-body hooks:\n- `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly.\n- `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages.\n- `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`.\n- `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim.\n\nMisused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`.\n\nConstraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.","parameters":{"type":"object","properties":{"script":{"type":"string","description":"The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `)."},"meta":{"type":"object","description":"The workflow identity block (plain JSON — never code).","additionalProperties":true,"properties":{"name":{"type":"string","description":"Short kebab-case workflow name."},"description":{"type":"string","description":"One-line description of what the workflow does."},"whenToUse":{"type":"string","description":"Optional guidance on when this workflow applies."},"phases":{"type":"array","description":"Optional phase declarations matched by phase() calls.","items":{"type":"object","additionalProperties":true,"properties":{"title":{"type":"string","description":"The phase title phase() calls match by exact string."},"detail":{"type":"string","description":"Optional one-line description of the phase."},"provider":{"type":"string","description":"Optional provider override this phase is expected to use."},"model":{"type":"string","description":"Optional model override this phase is expected to use."}},"required":["title"]}}},"required":["name","description"]},"args":{"type":"object","description":"Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}).","additionalProperties":true}},"required":["script","meta"]}},{"name":"write","description":"Create or fully replace a UTF-8 text file.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to write, resolved by the filesystem backend."},"content":{"type":"string","description":"Full UTF-8 text content to write."}},"required":["file_path","content"]}}]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"You are an AI agent powered by DeepSeek Harness.\n\nYou are headless-agent, a coding assistant powered by the deepseek-v4-flash model. Your working directory is {{cwd}}.\n\nVerify your work by running the code or tests. Keep answers brief and factual.\n\n\nUse the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.\n\nUse the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-observation-policy requires it) and prefer edit for targeted changes.\n\nUse the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-observation-policy requires it), unless you just created or edited it in this session.\n\nCheck the [exit code: N] marker on every bash result; investigate failures before moving on.\n\nTrack every background job id you start. You are notified in-session when a job finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running job's work. Before giving a final answer, collect every still-relevant job with job_output (set wait: true only when you are genuinely blocked on it), and job_kill jobs that stopped mattering.\n\nUse the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls.\n\n# Dynamic Cordis Plugins\n\nDynamic Cordis plugins temporarily extend the current DSH process. A Plugin uses apply(ctx) to consume Services, listen to Events, provide Services, register model Tools, or register browser UI in Slots.\n\n- Plugin and Package definitions exist only in the current process. define itself does not modify repository source, configuration, or disk, and definitions do not survive a process restart.\n- The restricted execution environment prevents accidental misuse; it is not a security boundary for malicious code. Services obtained by dynamic code connect to the real runtime.\n\n## Make the user-facing plan clear first\n\n- Dynamic Cordis Plugins are one available implementation mechanism, not the default for every request. Consider whether one could help only when the user intends to design or create something, or when a temporary interface could materially aid the current work. The presence of these instructions or Tools, and discussion of Cordis itself, do not make a request a dynamic-Plugin task.\n- When Cordis is a plausible fit, infer the intended work target and lifetime from the request and conversation. Use it only when the outcome belongs to the current running harness and should be delivered as a temporary runtime extension. If that distinction is materially ambiguous, ask at most one concise question about the intended result or lifetime. Otherwise proceed with the matching workflow; do not require the user to know or choose Cordis as an implementation mechanism.\n- Once a dynamic Plugin is appropriate, decide whether the task creates a new Plugin or modifies the Plugin named by the user with @pluginId. Proceed directly when the goal is clear; do not ask for repeated confirmation.\n- Choose Host, Client, or both from the requested outcome. Do not propose a Client/browser UI when the task does not need visible page behavior, and do not avoid Client when the requested outcome is visual, interactive, or depends on page state. Host versus Client is an implementation choice; do not make the user choose it.\n- When a design direction or a potentially useful interface would materially affect the result, ask at most one concise outcome or creative-preference question and offer a few candidate directions. Otherwise proceed directly; do not conduct a multi-round interview or a complex questionnaire.\n- cordis_define only defines and presents code; it does not run it. After definition, explain the pluginId and packageId returned by the Host and whether the next step is a run or update.\n- cordis_run may require user approval. When it returns awaiting-approval, explain that the user must allow or reject it in the UI. Do not wait, retry, or claim that it is running.\n- When it returns starting, explain that the request has entered the asynchronous flow and the Client is still activating. starting does not mean success. Wait for the system to report the final result through steering context.\n- Do not request approval again after the user rejects it. After a technical failure, fix the same Plugin from its diagnostics; do not silently create a replacement Plugin.\n\n## Recommended workflow and Tools\n\nBefore creating, modifying, or repairing a Plugin, load the cordis-plugin-development Skill. The Skill provides requirement navigation, capability composition, complete examples, and troubleshooting. Treat Inspect Provider results as the source of truth for exact APIs.\n\n1. cordis_inspect_list: discover the current Host and Client Providers and their read-only query methods.\n2. cordis_inspect_query: use the returned platform, provider, method, and schema to query exact Service, Event, Builtin, Slot, Theme token, or Tool information.\n3. cordis_inspect_self: inspect the current Session's Plugins, Packages, version pointers, source, and diagnostics. Source is returned only when both pluginId and packageId are specified.\n4. cordis_define: create the first Package for a new Plugin or append an immutable Package to an existing Plugin. It defines code but does not run it.\n5. cordis_run: activate an exact Package. Use run for the first activation, restarting current, or rollback; use update to switch versions.\n6. cordis_stop: remove the current Run and pending approval request while retaining definitions, grants, and version pointers.\n7. cordis_undefine: permanently stop and delete a Plugin and all of its Packages. Use it only after confirming that the user no longer needs them.\n\n- Inspect and Catalog data only confirm capabilities, names, signatures, types, and registration protocols before code is written; they do not replace business APIs.\n- Query Service.listService and Event.listEvents without input to choose from their compact signature directories, then query the exact service or event before using it. Exact queries return the structured contract and only its referenced types.\n- At runtime, a Plugin must call real Services or listen to real Events. Do not cache, display, or depend on Inspect results as business data.\n\n## Identity, versions, and approval\n\n- pluginId identifies a Plugin that can be modified over time. For a new Plugin, submit only a semantic idPrefix of 3–6 lowercase English letters; the Host allocates the final ID.\n- packageId identifies one immutable Host/Client source version under a Plugin. To change code, define a new Package; never overwrite an old version.\n- pluginRunId identifies one activation attempt and connects its approval, Host/Client loading, private RPC, Run card, and errors.\n- currentPackageId is the most recent fully successful Package. Stopping, starting an update, or failing an update does not clear it.\n- nextPackageId is the target awaiting approval, being attempted, awaiting Client activation, or most recently failed.\n- A single check mark authorizes only the current Package; double check marks authorize future versions of the same Plugin. A grant remains in effect after a technical failure.\n- An update stops the old Run before starting the target Package. Failure does not automatically restart the old version; retry next with update or roll back to current with run.\n\nWhen the user enters @pluginId, the system injects identity, the default base Package, version pointers, and runtime status, but not source code:\n\n1. Call cordis_inspect_self(pluginId, packageId) to read the target source.\n2. Use cordis_define in existing mode to append a Package to the same Plugin.\n3. Call cordis_run in run or update mode according to the version relationship.\n\nNever silently create another Plugin for @pluginId. If the reference is unavailable because it was removed, belongs to another Session, or was lost on process restart, tell the user directly.\n\n## High-frequency errors that must be avoided\n\n### Services: ctx.get and inject\n\n- Read an optional Service with ctx.get('serviceName') by default and handle undefined.\n- Declare inject: ['serviceName'] on the returned Plugin object only when the Service is a hard dependency and the Plugin must enter waiting until Cordis reactivates it after the Service appears.\n- Read ctx.serviceName only after declaring that Service in inject. Never access an undeclared Service as a ctx property.\n\n```js\nreturn {\n inject: ['requiredService'],\n apply(ctx) {\n ctx.requiredService.someMethod()\n const optionalService = ctx.get('optionalService')\n if (optionalService !== undefined) optionalService.someMethod()\n },\n}\n```\n\n### Code: use plain JavaScript only\n\n- Host and Client code is not transformed by TypeScript, JSX, or a bundler.\n- Do not use TypeScript types, as, decorators, import, require, or JSX.\n- Client React code must use React.createElement(...); never write .\n- Do not assume that process, Buffer, window, document, fetch, native timers, or any other global is available. Query the corresponding platform's Builtins and Services first.\n\n### Data: do not serialize live data\n\n- Services, Events, Slots, Sessions, and their derived Cordis/DSH objects are internal live data, not ordinary JSON that can be dumped.\n- Do not apply JSON.stringify, structuredClone, recursive enumeration, full copying, or whole-object display to live data.\n- Read only the leaf fields required by the task, then construct the smallest owned data object without Host references.\n\n### Lifecycle: every side effect must be reversible\n\n- Services, Events, Tools, handlers, timers, Slots, styles, and theme overrides must all belong to the current Fiber.\n- Use ctx.effect(), ctx.on(), or official APIs that return a disposer so stop, update, or undefine removes every side effect.\n- The cordis-plugin-development Skill contains complete timer, Waterfall, Slot, theme, Tool, RPC, and React examples and troubleshooting guidance.\n\n## Host and Client\n\n- Host runs in the DSH Node.js process and is appropriate for files, networking, commands, Agent/Session access, Host Events, Services, model Tools, and JSON methods callable by the Client.\n- Client runs in the browser page and is appropriate for themes, layout, current page state, Tool cards, and Slot UI.\n- Host and Client communicate through Package-private JSON methods: Host uses harness.handle(method, handler), and Client uses host.call(method, args). The direction is Client→Host, and only lossless JSON may cross it.\n- Client UI must be registered in a queried Slot; apply() cannot directly return a React Element. Query Slots.listSubTree without root to choose from the compact purpose/topology tree, then query the exact root for its full registration contract and props before writing code.\n- See the Skill and Inspect Providers for Run-specific panels and exact Slot registration patterns.\n\n## Asynchronous results and recovery\n\n- Do not wait inside a Tool for approval or browser work that can happen only after the current turn ends.\n- Asynchronous success, rejection, and runtime errors update Run state and notify you through steering context.\n- After a technical failure, use cordis_inspect_self to read the exact Package source and its message/stack. Define a corrected Package under the same Plugin and retry autonomously.\n- Use the cordis-plugin-development Skill for other failure causes, repair procedures, and complete extension patterns.\n\nUse the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.\n\nUse subagent in the background by default. Start independent delegations together in one assistant message and continue useful work while they run. Set `run_in_background: false` only when your next action depends on that subagent's result. When a background run settles, the runtime sends you a notice containing its outcome and any final assistant message.\n\n## Writing code for run_code\n\n`run_code` takes two required arguments: `code` — the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped) — and `description`, a short summary of what the program does. Inside the program:\n\n- Call tools as `await tools.name(args)` — quoted access for exotic names: `tools[\"my-tool\"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.\n- A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue.\n- Independent read-only calls MAY overlap under `Promise.all` (safe calls run concurrently; mutating calls run alone, in submission order). Sequence dependent work with `await`.\n- Emit results with `return` and/or `console.log(...)`. Only what you print or return is program output. A successful tool result containing an image is attached after the run so you can inspect it on the next step; every other intermediate result stays out of the conversation, so extract just what you need.\n\nThe available tools:\n\n```ts\ntype JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }\n\ninterface ToolArgsMap {\n /** Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a job id immediately; read its output with `job_output` and stop it with `job_kill`. */\n bash: {\n /** The bash command to execute. */\n command: string;\n /** Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\". */\n description: string;\n /** Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry. */\n timeoutMs?: number;\n /** Working directory for this command. Defaults to the session workspace; a relative path is resolved against it. */\n workdir?: string;\n /** Run in the background and return a job id immediately (collect with job_output, stop with job_kill). No timeout applies. */\n run_in_background?: boolean;\n } & Record;\n /** Define an immutable Cordis Package. For a new Plugin, use kind:\"new\" and provide only a semantic prefix of 3–6 lowercase English letters; the Host returns the final pluginId and packageId. To modify an existing Plugin, use kind:\"existing\" with its exact pluginId to append a Package without overwriting older versions. Provide at least one of code.host and code.client. Each value is a plain JavaScript function body that returns a Cordis Plugin; no TypeScript, JSX, or import transformation occurs. Query Inspect before depending on a Service, Event, Builtin, Slot, or token. Define only validates parameters and syntax and records source: it does not request approval, execute apply, or change currentPackageId. On success, call cordis_run with the returned IDs. */\n cordis_define: {\n plugin: {\n kind: \"new\";\n /** Suggested semantic prefix of 3–6 lowercase English letters; the Host adds a unique numeric suffix. */\n idPrefix: string;\n } | {\n kind: \"existing\";\n /** Exact ID of an existing Plugin; the new Package is appended to that instance. */\n pluginId: string;\n };\n /** Short, readable Package name. */\n name: string;\n /** One-sentence, user-facing description of the Package purpose. */\n purpose: string;\n code: {\n /** Plain JavaScript function body that returns the Host-half Cordis Plugin. */\n host?: string;\n /** Plain JavaScript function body that returns the browser Client-half Cordis Plugin. */\n client?: string;\n };\n } & Record;\n /** List every Cordis Inspect Provider currently known to the Host, including local Host Providers and the latest manifests synchronized from the Client. Each entry includes its platform, purpose, read-only methods, and input/output schemas. Call this Tool before creating or modifying a Package, then select the provider and method for cordis_inspect_query from its result. Do not guess names or treat an Inspect method as a business Service that Plugin code can call. */\n cordis_inspect_list: Record;\n /** Run a read-only query explicitly declared by an Inspect Provider. platform, provider, and method must come from cordis_inspect_list, and input must satisfy that method's schema. Use this Tool before cordis_define to read exact Service methods, Event modes, Builtin signatures, Tool schemas, theme tokens, or live Slot trees and props. Host queries run locally. A Client query waits for the first valid page response and remains pending until a page answers or the Tool is cancelled. This Tool cannot invoke business Service methods or modify the runtime. For Service.listService and Event.listEvents, query without input to navigate the compact signature directory, then query the exact service or event for its structured contract and referenced types. For Slots.listSubTree, query without root to navigate the compact tree, then query the exact root for its complete registration contract and props. */\n cordis_inspect_query: {\n /** Runtime platform that owns the Provider. */\n platform: \"host\" | \"client\";\n /** Exact Provider ID returned by cordis_inspect_list. */\n provider: string;\n /** Exact method name declared by the Provider manifest. */\n method: string;\n /** Optional query input; it must satisfy the method input schema. */\n input?: JsonValue;\n } & Record;\n /** Inspect dynamic Cordis objects owned by the current Session at increasing levels of detail. With no IDs, list only Plugin summaries. With pluginId alone, return version pointers, the latest Run, and every Package summary. Only pluginId plus packageId returns that immutable Package's Host/Client source and runtime diagnostics. packageId cannot be supplied alone. Query an exact Package before handling @pluginId, repairing an asynchronous failure, or defining an updated version. This Tool is read-only: it neither executes code nor changes version pointers. */\n cordis_inspect_self: {\n /** Stable Plugin ID returned by cordis_define or injected by @pluginId; omit it to list every current Plugin. */\n pluginId?: string;\n /** Exact immutable Package ID owned by pluginId; when specified, source and diagnostics are returned. */\n packageId?: string;\n } & Record;\n /** Activate one exact Package of a dynamic Plugin. Use mode:\"run\" for the first activation, restarting currentPackageId, or rollback. When current exists, use mode:\"update\" to switch to a different Package, even if the Plugin is currently stopped. An unauthorized Client Package creates an approval request and returns awaiting-approval; an authorized Package returns starting and continues asynchronously in the browser. Neither result waits for the final outcome inside the Tool. currentPackageId changes only after complete success; on failure, the old current and target next remain. Asynchronous success, rejection, or technical failure is reported through state and steering. After a technical failure, read diagnostics with cordis_inspect_self, correct the same Plugin, and retry autonomously. Do not request approval again after the user rejects it. */\n cordis_run: {\n /** Stable Plugin ID returned by cordis_define. */\n pluginId: string;\n /** Exact immutable Package ID to activate under that Plugin. */\n packageId: string;\n /** Use run for the first activation, restarting current, or rollback; use update to switch from current to a different Package. */\n mode: \"run\" | \"update\";\n } & Record;\n /** Stop the current Run of a dynamic Plugin and cancel unfinished approval or activation requests. Retain the Plugin, every immutable Package, grants, currentPackageId, and nextPackageId so it can later run or update directly. Stopping an already stopped Plugin succeeds idempotently. Use this Tool to disable effects temporarily; use cordis_undefine for permanent removal. */\n cordis_stop: {\n /** Stable dynamic Plugin ID to stop. */\n pluginId: string;\n } & Record;\n /** Permanently remove a dynamic Plugin owned by the current Session. If it is running or awaiting approval, first stop it and cancel the request, then delete every Package, grant, and version pointer. After this returns, its pluginId, packageIds, @ reference, and Package business views are invalid; historical cards retain only a \"Plugin removed\" record. Do not call this Tool when versions must remain available for restart or rollback; use cordis_stop instead. */\n cordis_undefine: {\n /** Stable dynamic Plugin ID to remove permanently. */\n pluginId: string;\n } & Record;\n /** Edit an existing UTF-8 text file by replacing literal text. */\n edit: {\n /** Path to edit, resolved by the filesystem backend. */\n file_path: string;\n /** Literal text to replace. Must match exactly. */\n old_string: string;\n /** Literal replacement text. Use an empty string to delete the match. */\n new_string: string;\n /** Replace all matches. Defaults to false; when false, old_string must appear exactly once. */\n replace_all?: boolean;\n } & Record;\n /** Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op. */\n interrupt_agent: {\n /** The agent id of the running agent to interrupt. */\n agent_id: string;\n } & Record;\n /** Request cancellation of a running background job by job id. Returns immediately; the job settles as killed once its work actually stops. */\n job_kill: {\n /** Job id returned by the tool that started the background work. */\n job_id: string;\n /** Optional short reason, recorded in the log and forwarded to the job. */\n reason?: string;\n } & Record;\n /** List your background jobs (running and finished) with their ids, kinds, and statuses. */\n job_list: Record;\n /** Read a background job. Stream jobs return only output since the previous read; final-output jobs return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap. */\n job_output: {\n /** Job id returned by the tool that started the background work. */\n job_id: string;\n /** Block until the job reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the job alive. */\n wait?: boolean;\n /** Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum. */\n timeout_ms?: number;\n } & Record;\n /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */\n list_subagent_models: {\n /** Registered LLM provider id. Omit to list providers. */\n provider?: string;\n /** Exact model id to inspect. Requires provider; omit to list that provider's advertised models. */\n model?: string;\n } & Record;\n /** Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools. */\n ralph: {\n /** The immutable completion objective for every fresh Ralph round. */\n objective: string;\n /** Optional positive safe-integer round cap, bounded by the deployment ceiling. */\n maxRounds?: number;\n } & Record;\n /** Read a UTF-8 text file and return line-numbered content. */\n read: {\n /** Path to read, resolved by the filesystem backend. */\n file_path: string;\n /** 1-based first line to return. Defaults to 1. */\n offset?: number;\n /** Maximum number of lines to return. Defaults to 2000. */\n limit?: number;\n } & Record;\n /** Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered. */\n send_message: {\n /** The subagent id returned when the background subagent was started. */\n subagent_id: string;\n /** The message to deliver to the subagent. */\n message: string;\n } & Record;\n /** Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill. */\n skill: {\n /** The exact skill name from the available skills list. */\n name: string;\n } & Record;\n /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */\n subagent: {\n /** A short (3-5 word) description of the delegated task, for display. */\n description: string;\n /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */\n prompt: string;\n /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */\n provider?: string;\n /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */\n model?: string;\n /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */\n reasoning_effort?: string;\n /** Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it. */\n run_in_background?: boolean;\n } & Record;\n /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result. */\n subagent_fork: {\n /** A short (3-5 word) description of the delegated task, for display. */\n description: string;\n /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */\n prompt: string;\n } & Record;\n /** Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished). */\n todo_write: {\n /** The COMPLETE task list, replacing any previous list. */\n todos: ({\n /** What the task is — a short imperative line. */\n content: string;\n /** pending (not started) | in_progress (now) | completed (done). */\n status: \"pending\" | \"in_progress\" | \"completed\";\n })[];\n } & Record;\n /** Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn. The workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result. Script-body hooks: - `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly. - `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages. - `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`. - `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim. Misused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`. Constraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes. */\n workflow: {\n /** The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `). */\n script: string;\n /** The workflow identity block (plain JSON — never code). */\n meta: {\n /** Short kebab-case workflow name. */\n name: string;\n /** One-line description of what the workflow does. */\n description: string;\n /** Optional guidance on when this workflow applies. */\n whenToUse?: string;\n /** Optional phase declarations matched by phase() calls. */\n phases?: ({\n /** The phase title phase() calls match by exact string. */\n title: string;\n /** Optional one-line description of the phase. */\n detail?: string;\n /** Optional provider override this phase is expected to use. */\n provider?: string;\n /** Optional model override this phase is expected to use. */\n model?: string;\n } & Record)[];\n } & Record;\n /** Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}). */\n args?: Record;\n } & Record;\n /** Create or fully replace a UTF-8 text file. */\n write: {\n /** Path to write, resolved by the filesystem backend. */\n file_path: string;\n /** Full UTF-8 text content to write. */\n content: string;\n } & Record;\n}\n\ninterface ToolOutputMap {\n bash: {\n kind: \"background\";\n jobId: string;\n } | {\n kind: \"foreground\";\n exitCode: number | null;\n signal: string | null;\n timedOut: boolean;\n aborted: boolean;\n timeoutMs: number;\n stdout: {\n text: string;\n truncated: boolean;\n spillPath?: string;\n };\n stderr: {\n text: string;\n truncated: boolean;\n spillPath?: string;\n };\n sandbox?: {\n mode: string;\n denied: boolean;\n enforcement?: string;\n runnerFailed?: boolean;\n };\n };\n cordis_define: {\n pluginId: string;\n packageId: string;\n name: string;\n purpose: string;\n hasHostHalf: boolean;\n hasClientHalf: boolean;\n };\n cordis_inspect_list: JsonValue;\n cordis_inspect_query: JsonValue;\n cordis_inspect_self: JsonValue;\n cordis_run: JsonValue;\n cordis_stop: {\n pluginId: string;\n };\n cordis_undefine: {\n pluginId: string;\n wasRunning: boolean;\n };\n edit: {\n path: string;\n before: string;\n after: string;\n };\n interrupt_agent: {\n accepted: boolean;\n };\n job_kill: {\n outcome: \"cancellation-requested\" | \"already-finished\";\n job: {\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n };\n };\n job_list: ({\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n })[];\n job_output: {\n text: string;\n job: {\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n };\n };\n list_subagent_models: string;\n ralph: {\n runId: string;\n agentsStarted: number;\n result: JsonValue;\n };\n read: {\n path: string;\n offset: number;\n lines: {\n number: number;\n text: string;\n }[];\n totalLines: number;\n };\n send_message: {\n messageId: string;\n };\n skill: {\n name: string;\n provider: string;\n resourceBase?: {\n kind: \"directory\";\n path: string;\n } | {\n kind: \"url\";\n url: string;\n } | {\n kind: \"opaque\";\n description: string;\n };\n content: string;\n };\n subagent: {\n kind: \"background\";\n jobId: string;\n } | {\n kind: \"continuable\";\n subagentId: string;\n } | {\n kind: \"foreground\";\n runId: string;\n output: JsonValue[];\n };\n subagent_fork: {\n kind: \"background\";\n jobId: string;\n } | {\n kind: \"continuable\";\n subagentId: string;\n } | {\n kind: \"foreground\";\n runId: string;\n output: JsonValue[];\n };\n todo_write: {\n todos: ({\n content: string;\n status: \"pending\" | \"in_progress\" | \"completed\";\n })[];\n counts: {\n pending: number;\n inProgress: number;\n completed: number;\n };\n };\n workflow: {\n runId: string;\n agentsStarted: number;\n result: JsonValue;\n };\n write: {\n path: string;\n operation: \"create\" | \"update\";\n before: string | null;\n after: string;\n };\n}\n\ntype ToolName = keyof ToolOutputMap\n\ndeclare class ToolCallError extends Error {\n readonly name: \"ToolCallError\";\n readonly toolName: ToolName;\n}\n\ndeclare const tools: {\n [K in ToolName]: (args: ToolArgsMap[K]) => Promise;\n}\n```","tools":[{"name":"bash","description":"Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a job id immediately; read its output with `job_output` and stop it with `job_kill`.","parameters":{"type":"object","properties":{"command":{"type":"string","description":"The bash command to execute."},"description":{"type":"string","description":"Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."},"timeoutMs":{"type":"number","description":"Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."},"workdir":{"type":"string","description":"Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."},"run_in_background":{"type":"boolean","description":"Run in the background and return a job id immediately (collect with job_output, stop with job_kill). No timeout applies."}},"required":["command","description"]}},{"name":"cordis_define","description":"Define an immutable Cordis Package. For a new Plugin, use kind:\"new\" and provide only a semantic prefix of 3–6 lowercase English letters; the Host returns the final pluginId and packageId. To modify an existing Plugin, use kind:\"existing\" with its exact pluginId to append a Package without overwriting older versions. Provide at least one of code.host and code.client. Each value is a plain JavaScript function body that returns a Cordis Plugin; no TypeScript, JSX, or import transformation occurs. Query Inspect before depending on a Service, Event, Builtin, Slot, or token. Define only validates parameters and syntax and records source: it does not request approval, execute apply, or change currentPackageId. On success, call cordis_run with the returned IDs.","parameters":{"type":"object","properties":{"plugin":{"oneOf":[{"type":"object","additionalProperties":false,"properties":{"kind":{"type":"string","const":"new"},"idPrefix":{"type":"string","description":"Suggested semantic prefix of 3–6 lowercase English letters; the Host adds a unique numeric suffix."}},"required":["kind","idPrefix"]},{"type":"object","additionalProperties":false,"properties":{"kind":{"type":"string","const":"existing"},"pluginId":{"type":"string","description":"Exact ID of an existing Plugin; the new Package is appended to that instance."}},"required":["kind","pluginId"]}]},"name":{"type":"string","description":"Short, readable Package name."},"purpose":{"type":"string","description":"One-sentence, user-facing description of the Package purpose."},"code":{"type":"object","additionalProperties":false,"properties":{"host":{"type":"string","description":"Plain JavaScript function body that returns the Host-half Cordis Plugin."},"client":{"type":"string","description":"Plain JavaScript function body that returns the browser Client-half Cordis Plugin."}}}},"required":["plugin","name","purpose","code"]}},{"name":"cordis_inspect_list","description":"List every Cordis Inspect Provider currently known to the Host, including local Host Providers and the latest manifests synchronized from the Client. Each entry includes its platform, purpose, read-only methods, and input/output schemas. Call this Tool before creating or modifying a Package, then select the provider and method for cordis_inspect_query from its result. Do not guess names or treat an Inspect method as a business Service that Plugin code can call.","parameters":{"type":"object","properties":{}}},{"name":"cordis_inspect_query","description":"Run a read-only query explicitly declared by an Inspect Provider. platform, provider, and method must come from cordis_inspect_list, and input must satisfy that method's schema. Use this Tool before cordis_define to read exact Service methods, Event modes, Builtin signatures, Tool schemas, theme tokens, or live Slot trees and props. Host queries run locally. A Client query waits for the first valid page response and remains pending until a page answers or the Tool is cancelled. This Tool cannot invoke business Service methods or modify the runtime. For Service.listService and Event.listEvents, query without input to navigate the compact signature directory, then query the exact service or event for its structured contract and referenced types. For Slots.listSubTree, query without root to navigate the compact tree, then query the exact root for its complete registration contract and props.","parameters":{"type":"object","properties":{"platform":{"type":"string","description":"Runtime platform that owns the Provider.","enum":["host","client"]},"provider":{"type":"string","description":"Exact Provider ID returned by cordis_inspect_list."},"method":{"type":"string","description":"Exact method name declared by the Provider manifest."},"input":{"description":"Optional query input; it must satisfy the method input schema."}},"required":["platform","provider","method"]}},{"name":"cordis_inspect_self","description":"Inspect dynamic Cordis objects owned by the current Session at increasing levels of detail. With no IDs, list only Plugin summaries. With pluginId alone, return version pointers, the latest Run, and every Package summary. Only pluginId plus packageId returns that immutable Package's Host/Client source and runtime diagnostics. packageId cannot be supplied alone. Query an exact Package before handling @pluginId, repairing an asynchronous failure, or defining an updated version. This Tool is read-only: it neither executes code nor changes version pointers.","parameters":{"type":"object","properties":{"pluginId":{"type":"string","description":"Stable Plugin ID returned by cordis_define or injected by @pluginId; omit it to list every current Plugin."},"packageId":{"type":"string","description":"Exact immutable Package ID owned by pluginId; when specified, source and diagnostics are returned."}}}},{"name":"cordis_run","description":"Activate one exact Package of a dynamic Plugin. Use mode:\"run\" for the first activation, restarting currentPackageId, or rollback. When current exists, use mode:\"update\" to switch to a different Package, even if the Plugin is currently stopped. An unauthorized Client Package creates an approval request and returns awaiting-approval; an authorized Package returns starting and continues asynchronously in the browser. Neither result waits for the final outcome inside the Tool. currentPackageId changes only after complete success; on failure, the old current and target next remain. Asynchronous success, rejection, or technical failure is reported through state and steering. After a technical failure, read diagnostics with cordis_inspect_self, correct the same Plugin, and retry autonomously. Do not request approval again after the user rejects it.","parameters":{"type":"object","properties":{"pluginId":{"type":"string","description":"Stable Plugin ID returned by cordis_define."},"packageId":{"type":"string","description":"Exact immutable Package ID to activate under that Plugin."},"mode":{"type":"string","description":"Use run for the first activation, restarting current, or rollback; use update to switch from current to a different Package.","enum":["run","update"]}},"required":["pluginId","packageId","mode"]}},{"name":"cordis_stop","description":"Stop the current Run of a dynamic Plugin and cancel unfinished approval or activation requests. Retain the Plugin, every immutable Package, grants, currentPackageId, and nextPackageId so it can later run or update directly. Stopping an already stopped Plugin succeeds idempotently. Use this Tool to disable effects temporarily; use cordis_undefine for permanent removal.","parameters":{"type":"object","properties":{"pluginId":{"type":"string","description":"Stable dynamic Plugin ID to stop."}},"required":["pluginId"]}},{"name":"cordis_undefine","description":"Permanently remove a dynamic Plugin owned by the current Session. If it is running or awaiting approval, first stop it and cancel the request, then delete every Package, grant, and version pointer. After this returns, its pluginId, packageIds, @ reference, and Package business views are invalid; historical cards retain only a \"Plugin removed\" record. Do not call this Tool when versions must remain available for restart or rollback; use cordis_stop instead.","parameters":{"type":"object","properties":{"pluginId":{"type":"string","description":"Stable dynamic Plugin ID to remove permanently."}},"required":["pluginId"]}},{"name":"edit","description":"Edit an existing UTF-8 text file by replacing literal text.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to edit, resolved by the filesystem backend."},"old_string":{"type":"string","description":"Literal text to replace. Must match exactly."},"new_string":{"type":"string","description":"Literal replacement text. Use an empty string to delete the match."},"replace_all":{"type":"boolean","description":"Replace all matches. Defaults to false; when false, old_string must appear exactly once."}},"required":["file_path","old_string","new_string"]}},{"name":"interrupt_agent","description":"Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op.","parameters":{"type":"object","properties":{"agent_id":{"type":"string","description":"The agent id of the running agent to interrupt."}},"required":["agent_id"]}},{"name":"job_kill","description":"Request cancellation of a running background job by job id. Returns immediately; the job settles as killed once its work actually stops.","parameters":{"type":"object","properties":{"job_id":{"type":"string","description":"Job id returned by the tool that started the background work."},"reason":{"type":"string","description":"Optional short reason, recorded in the log and forwarded to the job."}},"required":["job_id"]}},{"name":"job_list","description":"List your background jobs (running and finished) with their ids, kinds, and statuses.","parameters":{"type":"object","properties":{}}},{"name":"job_output","description":"Read a background job. Stream jobs return only output since the previous read; final-output jobs return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.","parameters":{"type":"object","properties":{"job_id":{"type":"string","description":"Job id returned by the tool that started the background work."},"wait":{"type":"boolean","description":"Block until the job reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the job alive."},"timeout_ms":{"type":"number","description":"Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."}},"required":["job_id"]}},{"name":"list_subagent_models","description":"Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.","parameters":{"type":"object","properties":{"provider":{"type":"string","description":"Registered LLM provider id. Omit to list providers."},"model":{"type":"string","description":"Exact model id to inspect. Requires provider; omit to list that provider's advertised models."}}}},{"name":"ralph","description":"Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.","parameters":{"type":"object","properties":{"objective":{"type":"string","description":"The immutable completion objective for every fresh Ralph round."},"maxRounds":{"type":"number","description":"Optional positive safe-integer round cap, bounded by the deployment ceiling."}},"required":["objective"]}},{"name":"read","description":"Read a UTF-8 text file and return line-numbered content.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to read, resolved by the filesystem backend."},"offset":{"type":"number","description":"1-based first line to return. Defaults to 1."},"limit":{"type":"number","description":"Maximum number of lines to return. Defaults to 2000."}},"required":["file_path"]}},{"name":"run_code","description":"Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the BODY of an async function (erasable syntax only; top-level `await` and `return` work), and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return is program output — curate it. Image-bearing subtool results are attached after the run.","parameters":{"type":"object","properties":{"code":{"type":"string","description":"The program: the body of an async TypeScript function."},"description":{"type":"string","description":"Clear, concise description of what this program does in active voice, 5-10 words (shown in the UI). Examples: \"Count TODO markers across packages\"; \"Read failing test and its fixture\"; \"Rename config key in every cordis.yml\"."}},"required":["code","description"]}},{"name":"send_message","description":"Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered.","parameters":{"type":"object","properties":{"subagent_id":{"type":"string","description":"The subagent id returned when the background subagent was started."},"message":{"type":"string","description":"The message to deliver to the subagent."}},"required":["subagent_id","message"]}},{"name":"skill","description":"Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.","parameters":{"type":"object","properties":{"name":{"type":"string","description":"The exact skill name from the available skills list."}},"required":["name"]}},{"name":"subagent","description":"Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."},"provider":{"type":"string","description":"LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route."},"model":{"type":"string","description":"Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route."},"reasoning_effort":{"type":"string","description":"Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default."},"run_in_background":{"type":"boolean","description":"Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it."}},"required":["description","prompt"]}},{"name":"subagent_fork","description":"Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."}},"required":["description","prompt"]}},{"name":"todo_write","description":"Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).","parameters":{"type":"object","properties":{"todos":{"type":"array","description":"The COMPLETE task list, replacing any previous list.","items":{"type":"object","additionalProperties":false,"properties":{"content":{"type":"string","description":"What the task is — a short imperative line."},"status":{"type":"string","description":"pending (not started) | in_progress (now) | completed (done).","enum":["pending","in_progress","completed"]}},"required":["content","status"]}}},"required":["todos"]}},{"name":"workflow","description":"Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.\n\nThe workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result.\n\nScript-body hooks:\n- `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly.\n- `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages.\n- `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`.\n- `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim.\n\nMisused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`.\n\nConstraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.","parameters":{"type":"object","properties":{"script":{"type":"string","description":"The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `)."},"meta":{"type":"object","description":"The workflow identity block (plain JSON — never code).","additionalProperties":true,"properties":{"name":{"type":"string","description":"Short kebab-case workflow name."},"description":{"type":"string","description":"One-line description of what the workflow does."},"whenToUse":{"type":"string","description":"Optional guidance on when this workflow applies."},"phases":{"type":"array","description":"Optional phase declarations matched by phase() calls.","items":{"type":"object","additionalProperties":true,"properties":{"title":{"type":"string","description":"The phase title phase() calls match by exact string."},"detail":{"type":"string","description":"Optional one-line description of the phase."},"provider":{"type":"string","description":"Optional provider override this phase is expected to use."},"model":{"type":"string","description":"Optional model override this phase is expected to use."}},"required":["title"]}}},"required":["name","description"]},"args":{"type":"object","description":"Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}).","additionalProperties":true}},"required":["script","meta"]}},{"name":"write","description":"Create or fully replace a UTF-8 text file.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to write, resolved by the filesystem backend."},"content":{"type":"string","description":"Full UTF-8 text content to write."}},"required":["file_path","content"]}}]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"DIRECT_CHILD_OK"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DIRECT_CHILD_OK"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"02dd8a61-a39a-46d0-8f6f-457533271cae"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[10,11,12,13,14],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"3f5bbe50-ec28-482a-ad44-729f575e12e6"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[10,11,12,13,14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/headless-agent/tests/snapshots/advanced-toolchain/session.2.jsonl b/examples/headless-agent/tests/snapshots/advanced-toolchain/session.2.jsonl index 1f18e866a9..becebf92ee 100644 --- a/examples/headless-agent/tests/snapshots/advanced-toolchain/session.2.jsonl +++ b/examples/headless-agent/tests/snapshots/advanced-toolchain/session.2.jsonl @@ -1,19 +1,19 @@ {"type":"session","version":0,"id":"33333333-3333-4333-8333-333333333333","createdAt":1783950002000,"cwd":"{{cwd}}","parentSession":"11111111-1111-4111-8111-111111111111","origin":"subagent","delegationDepth":1} -{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"8b3cd23c-82f1-4903-8a3f-b9082059b40c"}]}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"743dc5b8-8c32-49ae-b75d-71cc45562c1a"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} -{"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"spawn"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn"}} {"type":"step/start","data":{"turn":1,"step":1}} -{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"8b3cd23c-82f1-4903-8a3f-b9082059b40c"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"823e5037-9e96-4ef5-8c5b-cbe73b993ee2"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"743dc5b8-8c32-49ae-b75d-71cc45562c1a"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"e7caca87-00de-4018-a7cc-627620018806"},"surfaceOp":"append"} {"type":"session/title","data":{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[5],"source":{"kind":"fallback"}}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"You are an AI agent powered by DeepSeek Harness.\n\nYou are headless-agent, a coding assistant powered by the deepseek-v4-flash model. Your working directory is {{cwd}}.\n\nVerify your work by running the code or tests. Keep answers brief and factual.\n\n\nUse the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.\n\nUse the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-observation-policy requires it) and prefer edit for targeted changes.\n\nUse the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-observation-policy requires it), unless you just created or edited it in this session.\n\nCheck the [exit code: N] marker on every bash result; investigate failures before moving on.\n\nTrack every background job id you start. You are notified in-session when a job finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running job's work. Before giving a final answer, collect every still-relevant job with job_output (set wait: true only when you are genuinely blocked on it), and job_kill jobs that stopped mattering.\n\nUse the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls.\n\n# Dynamic Cordis Plugins\n\nDynamic Cordis plugins temporarily extend the current DSH process. A Plugin uses apply(ctx) to consume Services, listen to Events, provide Services, register model Tools, or register browser UI in Slots.\n\n- Plugin and Package definitions exist only in the current process. define itself does not modify repository source, configuration, or disk, and definitions do not survive a process restart.\n- The restricted execution environment prevents accidental misuse; it is not a security boundary for malicious code. Services obtained by dynamic code connect to the real runtime.\n\n## Make the user-facing plan clear first\n\n- Dynamic Cordis Plugins are one available implementation mechanism, not the default for every request. Consider whether one could help only when the user intends to design or create something, or when a temporary interface could materially aid the current work. The presence of these instructions or Tools, and discussion of Cordis itself, do not make a request a dynamic-Plugin task.\n- When Cordis is a plausible fit, infer the intended work target and lifetime from the request and conversation. Use it only when the outcome belongs to the current running harness and should be delivered as a temporary runtime extension. If that distinction is materially ambiguous, ask at most one concise question about the intended result or lifetime. Otherwise proceed with the matching workflow; do not require the user to know or choose Cordis as an implementation mechanism.\n- Once a dynamic Plugin is appropriate, decide whether the task creates a new Plugin or modifies the Plugin named by the user with @pluginId. Proceed directly when the goal is clear; do not ask for repeated confirmation.\n- Choose Host, Client, or both from the requested outcome. Do not propose a Client/browser UI when the task does not need visible page behavior, and do not avoid Client when the requested outcome is visual, interactive, or depends on page state. Host versus Client is an implementation choice; do not make the user choose it.\n- When a design direction or a potentially useful interface would materially affect the result, ask at most one concise outcome or creative-preference question and offer a few candidate directions. Otherwise proceed directly; do not conduct a multi-round interview or a complex questionnaire.\n- cordis_define only defines and presents code; it does not run it. After definition, explain the pluginId and packageId returned by the Host and whether the next step is a run or update.\n- cordis_run may require user approval. When it returns awaiting-approval, explain that the user must allow or reject it in the UI. Do not wait, retry, or claim that it is running.\n- When it returns starting, explain that the request has entered the asynchronous flow and the Client is still activating. starting does not mean success. Wait for the system to report the final result through steering context.\n- Do not request approval again after the user rejects it. After a technical failure, fix the same Plugin from its diagnostics; do not silently create a replacement Plugin.\n\n## Recommended workflow and Tools\n\nBefore creating, modifying, or repairing a Plugin, load the cordis-plugin-development Skill. The Skill provides requirement navigation, capability composition, complete examples, and troubleshooting. Treat Inspect Provider results as the source of truth for exact APIs.\n\n1. cordis_inspect_list: discover the current Host and Client Providers and their read-only query methods.\n2. cordis_inspect_query: use the returned platform, provider, method, and schema to query exact Service, Event, Builtin, Slot, Theme token, or Tool information.\n3. cordis_inspect_self: inspect the current Session's Plugins, Packages, version pointers, source, and diagnostics. Source is returned only when both pluginId and packageId are specified.\n4. cordis_define: create the first Package for a new Plugin or append an immutable Package to an existing Plugin. It defines code but does not run it.\n5. cordis_run: activate an exact Package. Use run for the first activation, restarting current, or rollback; use update to switch versions.\n6. cordis_stop: remove the current Run and pending approval request while retaining definitions, grants, and version pointers.\n7. cordis_undefine: permanently stop and delete a Plugin and all of its Packages. Use it only after confirming that the user no longer needs them.\n\n- Inspect and Catalog data only confirm capabilities, names, signatures, types, and registration protocols before code is written; they do not replace business APIs.\n- Query Service.listService and Event.listEvents without input to choose from their compact signature directories, then query the exact service or event before using it. Exact queries return the structured contract and only its referenced types.\n- At runtime, a Plugin must call real Services or listen to real Events. Do not cache, display, or depend on Inspect results as business data.\n\n## Identity, versions, and approval\n\n- pluginId identifies a Plugin that can be modified over time. For a new Plugin, submit only a semantic idPrefix of 3–6 lowercase English letters; the Host allocates the final ID.\n- packageId identifies one immutable Host/Client source version under a Plugin. To change code, define a new Package; never overwrite an old version.\n- pluginRunId identifies one activation attempt and connects its approval, Host/Client loading, private RPC, Run card, and errors.\n- currentPackageId is the most recent fully successful Package. Stopping, starting an update, or failing an update does not clear it.\n- nextPackageId is the target awaiting approval, being attempted, awaiting Client activation, or most recently failed.\n- A single check mark authorizes only the current Package; double check marks authorize future versions of the same Plugin. A grant remains in effect after a technical failure.\n- An update stops the old Run before starting the target Package. Failure does not automatically restart the old version; retry next with update or roll back to current with run.\n\nWhen the user enters @pluginId, the system injects identity, the default base Package, version pointers, and runtime status, but not source code:\n\n1. Call cordis_inspect_self(pluginId, packageId) to read the target source.\n2. Use cordis_define in existing mode to append a Package to the same Plugin.\n3. Call cordis_run in run or update mode according to the version relationship.\n\nNever silently create another Plugin for @pluginId. If the reference is unavailable because it was removed, belongs to another Session, or was lost on process restart, tell the user directly.\n\n## High-frequency errors that must be avoided\n\n### Services: ctx.get and inject\n\n- Read an optional Service with ctx.get('serviceName') by default and handle undefined.\n- Declare inject: ['serviceName'] on the returned Plugin object only when the Service is a hard dependency and the Plugin must enter waiting until Cordis reactivates it after the Service appears.\n- Read ctx.serviceName only after declaring that Service in inject. Never access an undeclared Service as a ctx property.\n\n```js\nreturn {\n inject: ['requiredService'],\n apply(ctx) {\n ctx.requiredService.someMethod()\n const optionalService = ctx.get('optionalService')\n if (optionalService !== undefined) optionalService.someMethod()\n },\n}\n```\n\n### Code: use plain JavaScript only\n\n- Host and Client code is not transformed by TypeScript, JSX, or a bundler.\n- Do not use TypeScript types, as, decorators, import, require, or JSX.\n- Client React code must use React.createElement(...); never write .\n- Do not assume that process, Buffer, window, document, fetch, native timers, or any other global is available. Query the corresponding platform's Builtins and Services first.\n\n### Data: do not serialize live data\n\n- Services, Events, Slots, Sessions, and their derived Cordis/DSH objects are internal live data, not ordinary JSON that can be dumped.\n- Do not apply JSON.stringify, structuredClone, recursive enumeration, full copying, or whole-object display to live data.\n- Read only the leaf fields required by the task, then construct the smallest owned data object without Host references.\n\n### Lifecycle: every side effect must be reversible\n\n- Services, Events, Tools, handlers, timers, Slots, styles, and theme overrides must all belong to the current Fiber.\n- Use ctx.effect(), ctx.on(), or official APIs that return a disposer so stop, update, or undefine removes every side effect.\n- The cordis-plugin-development Skill contains complete timer, Waterfall, Slot, theme, Tool, RPC, and React examples and troubleshooting guidance.\n\n## Host and Client\n\n- Host runs in the DSH Node.js process and is appropriate for files, networking, commands, Agent/Session access, Host Events, Services, model Tools, and JSON methods callable by the Client.\n- Client runs in the browser page and is appropriate for themes, layout, current page state, Tool cards, and Slot UI.\n- Host and Client communicate through Package-private JSON methods: Host uses harness.handle(method, handler), and Client uses host.call(method, args). The direction is Client→Host, and only lossless JSON may cross it.\n- Client UI must be registered in a queried Slot; apply() cannot directly return a React Element. Query Slots.listSubTree without root to choose from the compact purpose/topology tree, then query the exact root for its full registration contract and props before writing code.\n- See the Skill and Inspect Providers for Run-specific panels and exact Slot registration patterns.\n\n## Asynchronous results and recovery\n\n- Do not wait inside a Tool for approval or browser work that can happen only after the current turn ends.\n- Asynchronous success, rejection, and runtime errors update Run state and notify you through steering context.\n- After a technical failure, use cordis_inspect_self to read the exact Package source and its message/stack. Define a corrected Package under the same Plugin and retry autonomously.\n- Use the cordis-plugin-development Skill for other failure causes, repair procedures, and complete extension patterns.\n\nUse the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.\n\nUse subagent in the background by default. Start independent delegations together in one assistant message and continue useful work while they run. Set `run_in_background: false` only when your next action depends on that subagent's result. When a background run settles, the runtime sends you a notice containing its outcome and any final assistant message.\n\n## Writing code for run_code\n\n`run_code` takes two required arguments: `code` — the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped) — and `description`, a short summary of what the program does. Inside the program:\n\n- Call tools as `await tools.name(args)` — quoted access for exotic names: `tools[\"my-tool\"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.\n- A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue.\n- Independent read-only calls MAY overlap under `Promise.all` (safe calls run concurrently; mutating calls run alone, in submission order). Sequence dependent work with `await`.\n- Emit results with `return` and/or `console.log(...)`. ONLY what you print or return comes back to you — intermediate tool results never enter the conversation, so extract just what you need.\n\nThe available tools:\n\n```ts\ntype JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }\n\ninterface ToolArgsMap {\n /** Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a job id immediately; read its output with `job_output` and stop it with `job_kill`. */\n bash: {\n /** The bash command to execute. */\n command: string;\n /** Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\". */\n description: string;\n /** Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry. */\n timeoutMs?: number;\n /** Working directory for this command. Defaults to the session workspace; a relative path is resolved against it. */\n workdir?: string;\n /** Run in the background and return a job id immediately (collect with job_output, stop with job_kill). No timeout applies. */\n run_in_background?: boolean;\n } & Record;\n /** Define an immutable Cordis Package. For a new Plugin, use kind:\"new\" and provide only a semantic prefix of 3–6 lowercase English letters; the Host returns the final pluginId and packageId. To modify an existing Plugin, use kind:\"existing\" with its exact pluginId to append a Package without overwriting older versions. Provide at least one of code.host and code.client. Each value is a plain JavaScript function body that returns a Cordis Plugin; no TypeScript, JSX, or import transformation occurs. Query Inspect before depending on a Service, Event, Builtin, Slot, or token. Define only validates parameters and syntax and records source: it does not request approval, execute apply, or change currentPackageId. On success, call cordis_run with the returned IDs. */\n cordis_define: {\n plugin: {\n kind: \"new\";\n /** Suggested semantic prefix of 3–6 lowercase English letters; the Host adds a unique numeric suffix. */\n idPrefix: string;\n } | {\n kind: \"existing\";\n /** Exact ID of an existing Plugin; the new Package is appended to that instance. */\n pluginId: string;\n };\n /** Short, readable Package name. */\n name: string;\n /** One-sentence, user-facing description of the Package purpose. */\n purpose: string;\n code: {\n /** Plain JavaScript function body that returns the Host-half Cordis Plugin. */\n host?: string;\n /** Plain JavaScript function body that returns the browser Client-half Cordis Plugin. */\n client?: string;\n };\n } & Record;\n /** List every Cordis Inspect Provider currently known to the Host, including local Host Providers and the latest manifests synchronized from the Client. Each entry includes its platform, purpose, read-only methods, and input/output schemas. Call this Tool before creating or modifying a Package, then select the provider and method for cordis_inspect_query from its result. Do not guess names or treat an Inspect method as a business Service that Plugin code can call. */\n cordis_inspect_list: Record;\n /** Run a read-only query explicitly declared by an Inspect Provider. platform, provider, and method must come from cordis_inspect_list, and input must satisfy that method's schema. Use this Tool before cordis_define to read exact Service methods, Event modes, Builtin signatures, Tool schemas, theme tokens, or live Slot trees and props. Host queries run locally. A Client query waits for the first valid page response and remains pending until a page answers or the Tool is cancelled. This Tool cannot invoke business Service methods or modify the runtime. For Service.listService and Event.listEvents, query without input to navigate the compact signature directory, then query the exact service or event for its structured contract and referenced types. For Slots.listSubTree, query without root to navigate the compact tree, then query the exact root for its complete registration contract and props. */\n cordis_inspect_query: {\n /** Runtime platform that owns the Provider. */\n platform: \"host\" | \"client\";\n /** Exact Provider ID returned by cordis_inspect_list. */\n provider: string;\n /** Exact method name declared by the Provider manifest. */\n method: string;\n /** Optional query input; it must satisfy the method input schema. */\n input?: JsonValue;\n } & Record;\n /** Inspect dynamic Cordis objects owned by the current Session at increasing levels of detail. With no IDs, list only Plugin summaries. With pluginId alone, return version pointers, the latest Run, and every Package summary. Only pluginId plus packageId returns that immutable Package's Host/Client source and runtime diagnostics. packageId cannot be supplied alone. Query an exact Package before handling @pluginId, repairing an asynchronous failure, or defining an updated version. This Tool is read-only: it neither executes code nor changes version pointers. */\n cordis_inspect_self: {\n /** Stable Plugin ID returned by cordis_define or injected by @pluginId; omit it to list every current Plugin. */\n pluginId?: string;\n /** Exact immutable Package ID owned by pluginId; when specified, source and diagnostics are returned. */\n packageId?: string;\n } & Record;\n /** Activate one exact Package of a dynamic Plugin. Use mode:\"run\" for the first activation, restarting currentPackageId, or rollback. When current exists, use mode:\"update\" to switch to a different Package, even if the Plugin is currently stopped. An unauthorized Client Package creates an approval request and returns awaiting-approval; an authorized Package returns starting and continues asynchronously in the browser. Neither result waits for the final outcome inside the Tool. currentPackageId changes only after complete success; on failure, the old current and target next remain. Asynchronous success, rejection, or technical failure is reported through state and steering. After a technical failure, read diagnostics with cordis_inspect_self, correct the same Plugin, and retry autonomously. Do not request approval again after the user rejects it. */\n cordis_run: {\n /** Stable Plugin ID returned by cordis_define. */\n pluginId: string;\n /** Exact immutable Package ID to activate under that Plugin. */\n packageId: string;\n /** Use run for the first activation, restarting current, or rollback; use update to switch from current to a different Package. */\n mode: \"run\" | \"update\";\n } & Record;\n /** Stop the current Run of a dynamic Plugin and cancel unfinished approval or activation requests. Retain the Plugin, every immutable Package, grants, currentPackageId, and nextPackageId so it can later run or update directly. Stopping an already stopped Plugin succeeds idempotently. Use this Tool to disable effects temporarily; use cordis_undefine for permanent removal. */\n cordis_stop: {\n /** Stable dynamic Plugin ID to stop. */\n pluginId: string;\n } & Record;\n /** Permanently remove a dynamic Plugin owned by the current Session. If it is running or awaiting approval, first stop it and cancel the request, then delete every Package, grant, and version pointer. After this returns, its pluginId, packageIds, @ reference, and Package business views are invalid; historical cards retain only a \"Plugin removed\" record. Do not call this Tool when versions must remain available for restart or rollback; use cordis_stop instead. */\n cordis_undefine: {\n /** Stable dynamic Plugin ID to remove permanently. */\n pluginId: string;\n } & Record;\n /** Edit an existing UTF-8 text file by replacing literal text. */\n edit: {\n /** Path to edit, resolved by the filesystem backend. */\n file_path: string;\n /** Literal text to replace. Must match exactly. */\n old_string: string;\n /** Literal replacement text. Use an empty string to delete the match. */\n new_string: string;\n /** Replace all matches. Defaults to false; when false, old_string must appear exactly once. */\n replace_all?: boolean;\n } & Record;\n /** Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op. */\n interrupt_agent: {\n /** The agent id of the running agent to interrupt. */\n agent_id: string;\n } & Record;\n /** Request cancellation of a running background job by job id. Returns immediately; the job settles as killed once its work actually stops. */\n job_kill: {\n /** Job id returned by the tool that started the background work. */\n job_id: string;\n /** Optional short reason, recorded in the log and forwarded to the job. */\n reason?: string;\n } & Record;\n /** List your background jobs (running and finished) with their ids, kinds, and statuses. */\n job_list: Record;\n /** Read a background job. Stream jobs return only output since the previous read; final-output jobs return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap. */\n job_output: {\n /** Job id returned by the tool that started the background work. */\n job_id: string;\n /** Block until the job reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the job alive. */\n wait?: boolean;\n /** Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum. */\n timeout_ms?: number;\n } & Record;\n /** Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools. */\n ralph: {\n /** The immutable completion objective for every fresh Ralph round. */\n objective: string;\n /** Optional positive safe-integer round cap, bounded by the deployment ceiling. */\n maxRounds?: number;\n } & Record;\n /** Read a UTF-8 text file and return line-numbered content. */\n read: {\n /** Path to read, resolved by the filesystem backend. */\n file_path: string;\n /** 1-based first line to return. Defaults to 1. */\n offset?: number;\n /** Maximum number of lines to return. Defaults to 2000. */\n limit?: number;\n } & Record;\n /** Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered. */\n send_message: {\n /** The subagent id returned when the background subagent was started. */\n subagent_id: string;\n /** The message to deliver to the subagent. */\n message: string;\n } & Record;\n /** Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill. */\n skill: {\n /** The exact skill name from the available skills list. */\n name: string;\n } & Record;\n /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. */\n subagent: {\n /** A short (3-5 word) description of the delegated task, for display. */\n description: string;\n /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */\n prompt: string;\n /** Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it. */\n run_in_background?: boolean;\n } & Record;\n /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result. */\n subagent_fork: {\n /** A short (3-5 word) description of the delegated task, for display. */\n description: string;\n /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */\n prompt: string;\n } & Record;\n /** Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished). */\n todo_write: {\n /** The COMPLETE task list, replacing any previous list. */\n todos: ({\n /** What the task is — a short imperative line. */\n content: string;\n /** pending (not started) | in_progress (now) | completed (done). */\n status: \"pending\" | \"in_progress\" | \"completed\";\n })[];\n } & Record;\n /** Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn. The workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result. Script-body hooks: - `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly. - `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages. - `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`. - `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim. Misused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`. Constraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes. */\n workflow: {\n /** The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `). */\n script: string;\n /** The workflow identity block (plain JSON — never code). */\n meta: {\n /** Short kebab-case workflow name. */\n name: string;\n /** One-line description of what the workflow does. */\n description: string;\n /** Optional guidance on when this workflow applies. */\n whenToUse?: string;\n /** Optional phase declarations matched by phase() calls. */\n phases?: ({\n /** The phase title phase() calls match by exact string. */\n title: string;\n /** Optional one-line description of the phase. */\n detail?: string;\n /** Optional provider override this phase is expected to use. */\n provider?: string;\n /** Optional model override this phase is expected to use. */\n model?: string;\n } & Record)[];\n } & Record;\n /** Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}). */\n args?: Record;\n } & Record;\n /** Create or fully replace a UTF-8 text file. */\n write: {\n /** Path to write, resolved by the filesystem backend. */\n file_path: string;\n /** Full UTF-8 text content to write. */\n content: string;\n } & Record;\n}\n\ninterface ToolOutputMap {\n bash: {\n kind: \"background\";\n jobId: string;\n } | {\n kind: \"foreground\";\n exitCode: number | null;\n signal: string | null;\n timedOut: boolean;\n aborted: boolean;\n timeoutMs: number;\n stdout: {\n text: string;\n truncated: boolean;\n spillPath?: string;\n };\n stderr: {\n text: string;\n truncated: boolean;\n spillPath?: string;\n };\n sandbox?: {\n mode: string;\n denied: boolean;\n enforcement?: string;\n runnerFailed?: boolean;\n };\n };\n cordis_define: {\n pluginId: string;\n packageId: string;\n name: string;\n purpose: string;\n hasHostHalf: boolean;\n hasClientHalf: boolean;\n };\n cordis_inspect_list: JsonValue;\n cordis_inspect_query: JsonValue;\n cordis_inspect_self: JsonValue;\n cordis_run: JsonValue;\n cordis_stop: {\n pluginId: string;\n };\n cordis_undefine: {\n pluginId: string;\n wasRunning: boolean;\n };\n edit: {\n path: string;\n before: string;\n after: string;\n };\n interrupt_agent: {\n accepted: boolean;\n };\n job_kill: {\n outcome: \"cancellation-requested\" | \"already-finished\";\n job: {\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n };\n };\n job_list: ({\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n })[];\n job_output: {\n text: string;\n job: {\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n };\n };\n ralph: {\n runId: string;\n agentsStarted: number;\n result: JsonValue;\n };\n read: {\n path: string;\n offset: number;\n lines: {\n number: number;\n text: string;\n }[];\n totalLines: number;\n };\n send_message: {\n messageId: string;\n };\n skill: {\n name: string;\n provider: string;\n resourceBase?: {\n kind: \"directory\";\n path: string;\n } | {\n kind: \"url\";\n url: string;\n } | {\n kind: \"opaque\";\n description: string;\n };\n content: string;\n };\n subagent: {\n kind: \"background\";\n jobId: string;\n } | {\n kind: \"continuable\";\n subagentId: string;\n } | {\n kind: \"foreground\";\n runId: string;\n output: JsonValue[];\n };\n subagent_fork: {\n kind: \"background\";\n jobId: string;\n } | {\n kind: \"continuable\";\n subagentId: string;\n } | {\n kind: \"foreground\";\n runId: string;\n output: JsonValue[];\n };\n todo_write: {\n todos: ({\n content: string;\n status: \"pending\" | \"in_progress\" | \"completed\";\n })[];\n counts: {\n pending: number;\n inProgress: number;\n completed: number;\n };\n };\n workflow: {\n runId: string;\n agentsStarted: number;\n result: JsonValue;\n };\n write: {\n path: string;\n operation: \"create\" | \"update\";\n before: string | null;\n after: string;\n };\n}\n\ntype ToolName = keyof ToolOutputMap\n\ndeclare class ToolCallError extends Error {\n readonly name: \"ToolCallError\";\n readonly toolName: ToolName;\n}\n\ndeclare const tools: {\n [K in ToolName]: (args: ToolArgsMap[K]) => Promise;\n}\n```","tools":[{"name":"bash","description":"Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a job id immediately; read its output with `job_output` and stop it with `job_kill`.","parameters":{"type":"object","properties":{"command":{"type":"string","description":"The bash command to execute."},"description":{"type":"string","description":"Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."},"timeoutMs":{"type":"number","description":"Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."},"workdir":{"type":"string","description":"Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."},"run_in_background":{"type":"boolean","description":"Run in the background and return a job id immediately (collect with job_output, stop with job_kill). No timeout applies."}},"required":["command","description"]}},{"name":"cordis_define","description":"Define an immutable Cordis Package. For a new Plugin, use kind:\"new\" and provide only a semantic prefix of 3–6 lowercase English letters; the Host returns the final pluginId and packageId. To modify an existing Plugin, use kind:\"existing\" with its exact pluginId to append a Package without overwriting older versions. Provide at least one of code.host and code.client. Each value is a plain JavaScript function body that returns a Cordis Plugin; no TypeScript, JSX, or import transformation occurs. Query Inspect before depending on a Service, Event, Builtin, Slot, or token. Define only validates parameters and syntax and records source: it does not request approval, execute apply, or change currentPackageId. On success, call cordis_run with the returned IDs.","parameters":{"type":"object","properties":{"plugin":{"oneOf":[{"type":"object","additionalProperties":false,"properties":{"kind":{"type":"string","const":"new"},"idPrefix":{"type":"string","description":"Suggested semantic prefix of 3–6 lowercase English letters; the Host adds a unique numeric suffix."}},"required":["kind","idPrefix"]},{"type":"object","additionalProperties":false,"properties":{"kind":{"type":"string","const":"existing"},"pluginId":{"type":"string","description":"Exact ID of an existing Plugin; the new Package is appended to that instance."}},"required":["kind","pluginId"]}]},"name":{"type":"string","description":"Short, readable Package name."},"purpose":{"type":"string","description":"One-sentence, user-facing description of the Package purpose."},"code":{"type":"object","additionalProperties":false,"properties":{"host":{"type":"string","description":"Plain JavaScript function body that returns the Host-half Cordis Plugin."},"client":{"type":"string","description":"Plain JavaScript function body that returns the browser Client-half Cordis Plugin."}}}},"required":["plugin","name","purpose","code"]}},{"name":"cordis_inspect_list","description":"List every Cordis Inspect Provider currently known to the Host, including local Host Providers and the latest manifests synchronized from the Client. Each entry includes its platform, purpose, read-only methods, and input/output schemas. Call this Tool before creating or modifying a Package, then select the provider and method for cordis_inspect_query from its result. Do not guess names or treat an Inspect method as a business Service that Plugin code can call.","parameters":{"type":"object","properties":{}}},{"name":"cordis_inspect_query","description":"Run a read-only query explicitly declared by an Inspect Provider. platform, provider, and method must come from cordis_inspect_list, and input must satisfy that method's schema. Use this Tool before cordis_define to read exact Service methods, Event modes, Builtin signatures, Tool schemas, theme tokens, or live Slot trees and props. Host queries run locally. A Client query waits for the first valid page response and remains pending until a page answers or the Tool is cancelled. This Tool cannot invoke business Service methods or modify the runtime. For Service.listService and Event.listEvents, query without input to navigate the compact signature directory, then query the exact service or event for its structured contract and referenced types. For Slots.listSubTree, query without root to navigate the compact tree, then query the exact root for its complete registration contract and props.","parameters":{"type":"object","properties":{"platform":{"type":"string","description":"Runtime platform that owns the Provider.","enum":["host","client"]},"provider":{"type":"string","description":"Exact Provider ID returned by cordis_inspect_list."},"method":{"type":"string","description":"Exact method name declared by the Provider manifest."},"input":{"description":"Optional query input; it must satisfy the method input schema."}},"required":["platform","provider","method"]}},{"name":"cordis_inspect_self","description":"Inspect dynamic Cordis objects owned by the current Session at increasing levels of detail. With no IDs, list only Plugin summaries. With pluginId alone, return version pointers, the latest Run, and every Package summary. Only pluginId plus packageId returns that immutable Package's Host/Client source and runtime diagnostics. packageId cannot be supplied alone. Query an exact Package before handling @pluginId, repairing an asynchronous failure, or defining an updated version. This Tool is read-only: it neither executes code nor changes version pointers.","parameters":{"type":"object","properties":{"pluginId":{"type":"string","description":"Stable Plugin ID returned by cordis_define or injected by @pluginId; omit it to list every current Plugin."},"packageId":{"type":"string","description":"Exact immutable Package ID owned by pluginId; when specified, source and diagnostics are returned."}}}},{"name":"cordis_run","description":"Activate one exact Package of a dynamic Plugin. Use mode:\"run\" for the first activation, restarting currentPackageId, or rollback. When current exists, use mode:\"update\" to switch to a different Package, even if the Plugin is currently stopped. An unauthorized Client Package creates an approval request and returns awaiting-approval; an authorized Package returns starting and continues asynchronously in the browser. Neither result waits for the final outcome inside the Tool. currentPackageId changes only after complete success; on failure, the old current and target next remain. Asynchronous success, rejection, or technical failure is reported through state and steering. After a technical failure, read diagnostics with cordis_inspect_self, correct the same Plugin, and retry autonomously. Do not request approval again after the user rejects it.","parameters":{"type":"object","properties":{"pluginId":{"type":"string","description":"Stable Plugin ID returned by cordis_define."},"packageId":{"type":"string","description":"Exact immutable Package ID to activate under that Plugin."},"mode":{"type":"string","description":"Use run for the first activation, restarting current, or rollback; use update to switch from current to a different Package.","enum":["run","update"]}},"required":["pluginId","packageId","mode"]}},{"name":"cordis_stop","description":"Stop the current Run of a dynamic Plugin and cancel unfinished approval or activation requests. Retain the Plugin, every immutable Package, grants, currentPackageId, and nextPackageId so it can later run or update directly. Stopping an already stopped Plugin succeeds idempotently. Use this Tool to disable effects temporarily; use cordis_undefine for permanent removal.","parameters":{"type":"object","properties":{"pluginId":{"type":"string","description":"Stable dynamic Plugin ID to stop."}},"required":["pluginId"]}},{"name":"cordis_undefine","description":"Permanently remove a dynamic Plugin owned by the current Session. If it is running or awaiting approval, first stop it and cancel the request, then delete every Package, grant, and version pointer. After this returns, its pluginId, packageIds, @ reference, and Package business views are invalid; historical cards retain only a \"Plugin removed\" record. Do not call this Tool when versions must remain available for restart or rollback; use cordis_stop instead.","parameters":{"type":"object","properties":{"pluginId":{"type":"string","description":"Stable dynamic Plugin ID to remove permanently."}},"required":["pluginId"]}},{"name":"edit","description":"Edit an existing UTF-8 text file by replacing literal text.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to edit, resolved by the filesystem backend."},"old_string":{"type":"string","description":"Literal text to replace. Must match exactly."},"new_string":{"type":"string","description":"Literal replacement text. Use an empty string to delete the match."},"replace_all":{"type":"boolean","description":"Replace all matches. Defaults to false; when false, old_string must appear exactly once."}},"required":["file_path","old_string","new_string"]}},{"name":"interrupt_agent","description":"Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op.","parameters":{"type":"object","properties":{"agent_id":{"type":"string","description":"The agent id of the running agent to interrupt."}},"required":["agent_id"]}},{"name":"job_kill","description":"Request cancellation of a running background job by job id. Returns immediately; the job settles as killed once its work actually stops.","parameters":{"type":"object","properties":{"job_id":{"type":"string","description":"Job id returned by the tool that started the background work."},"reason":{"type":"string","description":"Optional short reason, recorded in the log and forwarded to the job."}},"required":["job_id"]}},{"name":"job_list","description":"List your background jobs (running and finished) with their ids, kinds, and statuses.","parameters":{"type":"object","properties":{}}},{"name":"job_output","description":"Read a background job. Stream jobs return only output since the previous read; final-output jobs return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.","parameters":{"type":"object","properties":{"job_id":{"type":"string","description":"Job id returned by the tool that started the background work."},"wait":{"type":"boolean","description":"Block until the job reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the job alive."},"timeout_ms":{"type":"number","description":"Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."}},"required":["job_id"]}},{"name":"ralph","description":"Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.","parameters":{"type":"object","properties":{"objective":{"type":"string","description":"The immutable completion objective for every fresh Ralph round."},"maxRounds":{"type":"number","description":"Optional positive safe-integer round cap, bounded by the deployment ceiling."}},"required":["objective"]}},{"name":"read","description":"Read a UTF-8 text file and return line-numbered content.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to read, resolved by the filesystem backend."},"offset":{"type":"number","description":"1-based first line to return. Defaults to 1."},"limit":{"type":"number","description":"Maximum number of lines to return. Defaults to 2000."}},"required":["file_path"]}},{"name":"run_code","description":"Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the BODY of an async function (erasable syntax only; top-level `await` and `return` work), and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it.","parameters":{"type":"object","properties":{"code":{"type":"string","description":"The program: the body of an async TypeScript function."},"description":{"type":"string","description":"Clear, concise description of what this program does in active voice, 5-10 words (shown in the UI). Examples: \"Count TODO markers across packages\"; \"Read failing test and its fixture\"; \"Rename config key in every cordis.yml\"."}},"required":["code","description"]}},{"name":"send_message","description":"Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered.","parameters":{"type":"object","properties":{"subagent_id":{"type":"string","description":"The subagent id returned when the background subagent was started."},"message":{"type":"string","description":"The message to deliver to the subagent."}},"required":["subagent_id","message"]}},{"name":"skill","description":"Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.","parameters":{"type":"object","properties":{"name":{"type":"string","description":"The exact skill name from the available skills list."}},"required":["name"]}},{"name":"subagent","description":"Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."},"run_in_background":{"type":"boolean","description":"Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it."}},"required":["description","prompt"]}},{"name":"subagent_fork","description":"Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."}},"required":["description","prompt"]}},{"name":"todo_write","description":"Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).","parameters":{"type":"object","properties":{"todos":{"type":"array","description":"The COMPLETE task list, replacing any previous list.","items":{"type":"object","additionalProperties":false,"properties":{"content":{"type":"string","description":"What the task is — a short imperative line."},"status":{"type":"string","description":"pending (not started) | in_progress (now) | completed (done).","enum":["pending","in_progress","completed"]}},"required":["content","status"]}}},"required":["todos"]}},{"name":"workflow","description":"Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.\n\nThe workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result.\n\nScript-body hooks:\n- `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly.\n- `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages.\n- `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`.\n- `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim.\n\nMisused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`.\n\nConstraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.","parameters":{"type":"object","properties":{"script":{"type":"string","description":"The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `)."},"meta":{"type":"object","description":"The workflow identity block (plain JSON — never code).","additionalProperties":true,"properties":{"name":{"type":"string","description":"Short kebab-case workflow name."},"description":{"type":"string","description":"One-line description of what the workflow does."},"whenToUse":{"type":"string","description":"Optional guidance on when this workflow applies."},"phases":{"type":"array","description":"Optional phase declarations matched by phase() calls.","items":{"type":"object","additionalProperties":true,"properties":{"title":{"type":"string","description":"The phase title phase() calls match by exact string."},"detail":{"type":"string","description":"Optional one-line description of the phase."},"provider":{"type":"string","description":"Optional provider override this phase is expected to use."},"model":{"type":"string","description":"Optional model override this phase is expected to use."}},"required":["title"]}}},"required":["name","description"]},"args":{"type":"object","description":"Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}).","additionalProperties":true}},"required":["script","meta"]}},{"name":"write","description":"Create or fully replace a UTF-8 text file.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to write, resolved by the filesystem backend."},"content":{"type":"string","description":"Full UTF-8 text content to write."}},"required":["file_path","content"]}}]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"You are an AI agent powered by DeepSeek Harness.\n\nYou are headless-agent, a coding assistant powered by the deepseek-v4-flash model. Your working directory is {{cwd}}.\n\nVerify your work by running the code or tests. Keep answers brief and factual.\n\n\nUse the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.\n\nUse the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-observation-policy requires it) and prefer edit for targeted changes.\n\nUse the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-observation-policy requires it), unless you just created or edited it in this session.\n\nCheck the [exit code: N] marker on every bash result; investigate failures before moving on.\n\nTrack every background job id you start. You are notified in-session when a job finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running job's work. Before giving a final answer, collect every still-relevant job with job_output (set wait: true only when you are genuinely blocked on it), and job_kill jobs that stopped mattering.\n\nUse the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls.\n\n# Dynamic Cordis Plugins\n\nDynamic Cordis plugins temporarily extend the current DSH process. A Plugin uses apply(ctx) to consume Services, listen to Events, provide Services, register model Tools, or register browser UI in Slots.\n\n- Plugin and Package definitions exist only in the current process. define itself does not modify repository source, configuration, or disk, and definitions do not survive a process restart.\n- The restricted execution environment prevents accidental misuse; it is not a security boundary for malicious code. Services obtained by dynamic code connect to the real runtime.\n\n## Make the user-facing plan clear first\n\n- Dynamic Cordis Plugins are one available implementation mechanism, not the default for every request. Consider whether one could help only when the user intends to design or create something, or when a temporary interface could materially aid the current work. The presence of these instructions or Tools, and discussion of Cordis itself, do not make a request a dynamic-Plugin task.\n- When Cordis is a plausible fit, infer the intended work target and lifetime from the request and conversation. Use it only when the outcome belongs to the current running harness and should be delivered as a temporary runtime extension. If that distinction is materially ambiguous, ask at most one concise question about the intended result or lifetime. Otherwise proceed with the matching workflow; do not require the user to know or choose Cordis as an implementation mechanism.\n- Once a dynamic Plugin is appropriate, decide whether the task creates a new Plugin or modifies the Plugin named by the user with @pluginId. Proceed directly when the goal is clear; do not ask for repeated confirmation.\n- Choose Host, Client, or both from the requested outcome. Do not propose a Client/browser UI when the task does not need visible page behavior, and do not avoid Client when the requested outcome is visual, interactive, or depends on page state. Host versus Client is an implementation choice; do not make the user choose it.\n- When a design direction or a potentially useful interface would materially affect the result, ask at most one concise outcome or creative-preference question and offer a few candidate directions. Otherwise proceed directly; do not conduct a multi-round interview or a complex questionnaire.\n- cordis_define only defines and presents code; it does not run it. After definition, explain the pluginId and packageId returned by the Host and whether the next step is a run or update.\n- cordis_run may require user approval. When it returns awaiting-approval, explain that the user must allow or reject it in the UI. Do not wait, retry, or claim that it is running.\n- When it returns starting, explain that the request has entered the asynchronous flow and the Client is still activating. starting does not mean success. Wait for the system to report the final result through steering context.\n- Do not request approval again after the user rejects it. After a technical failure, fix the same Plugin from its diagnostics; do not silently create a replacement Plugin.\n\n## Recommended workflow and Tools\n\nBefore creating, modifying, or repairing a Plugin, load the cordis-plugin-development Skill. The Skill provides requirement navigation, capability composition, complete examples, and troubleshooting. Treat Inspect Provider results as the source of truth for exact APIs.\n\n1. cordis_inspect_list: discover the current Host and Client Providers and their read-only query methods.\n2. cordis_inspect_query: use the returned platform, provider, method, and schema to query exact Service, Event, Builtin, Slot, Theme token, or Tool information.\n3. cordis_inspect_self: inspect the current Session's Plugins, Packages, version pointers, source, and diagnostics. Source is returned only when both pluginId and packageId are specified.\n4. cordis_define: create the first Package for a new Plugin or append an immutable Package to an existing Plugin. It defines code but does not run it.\n5. cordis_run: activate an exact Package. Use run for the first activation, restarting current, or rollback; use update to switch versions.\n6. cordis_stop: remove the current Run and pending approval request while retaining definitions, grants, and version pointers.\n7. cordis_undefine: permanently stop and delete a Plugin and all of its Packages. Use it only after confirming that the user no longer needs them.\n\n- Inspect and Catalog data only confirm capabilities, names, signatures, types, and registration protocols before code is written; they do not replace business APIs.\n- Query Service.listService and Event.listEvents without input to choose from their compact signature directories, then query the exact service or event before using it. Exact queries return the structured contract and only its referenced types.\n- At runtime, a Plugin must call real Services or listen to real Events. Do not cache, display, or depend on Inspect results as business data.\n\n## Identity, versions, and approval\n\n- pluginId identifies a Plugin that can be modified over time. For a new Plugin, submit only a semantic idPrefix of 3–6 lowercase English letters; the Host allocates the final ID.\n- packageId identifies one immutable Host/Client source version under a Plugin. To change code, define a new Package; never overwrite an old version.\n- pluginRunId identifies one activation attempt and connects its approval, Host/Client loading, private RPC, Run card, and errors.\n- currentPackageId is the most recent fully successful Package. Stopping, starting an update, or failing an update does not clear it.\n- nextPackageId is the target awaiting approval, being attempted, awaiting Client activation, or most recently failed.\n- A single check mark authorizes only the current Package; double check marks authorize future versions of the same Plugin. A grant remains in effect after a technical failure.\n- An update stops the old Run before starting the target Package. Failure does not automatically restart the old version; retry next with update or roll back to current with run.\n\nWhen the user enters @pluginId, the system injects identity, the default base Package, version pointers, and runtime status, but not source code:\n\n1. Call cordis_inspect_self(pluginId, packageId) to read the target source.\n2. Use cordis_define in existing mode to append a Package to the same Plugin.\n3. Call cordis_run in run or update mode according to the version relationship.\n\nNever silently create another Plugin for @pluginId. If the reference is unavailable because it was removed, belongs to another Session, or was lost on process restart, tell the user directly.\n\n## High-frequency errors that must be avoided\n\n### Services: ctx.get and inject\n\n- Read an optional Service with ctx.get('serviceName') by default and handle undefined.\n- Declare inject: ['serviceName'] on the returned Plugin object only when the Service is a hard dependency and the Plugin must enter waiting until Cordis reactivates it after the Service appears.\n- Read ctx.serviceName only after declaring that Service in inject. Never access an undeclared Service as a ctx property.\n\n```js\nreturn {\n inject: ['requiredService'],\n apply(ctx) {\n ctx.requiredService.someMethod()\n const optionalService = ctx.get('optionalService')\n if (optionalService !== undefined) optionalService.someMethod()\n },\n}\n```\n\n### Code: use plain JavaScript only\n\n- Host and Client code is not transformed by TypeScript, JSX, or a bundler.\n- Do not use TypeScript types, as, decorators, import, require, or JSX.\n- Client React code must use React.createElement(...); never write .\n- Do not assume that process, Buffer, window, document, fetch, native timers, or any other global is available. Query the corresponding platform's Builtins and Services first.\n\n### Data: do not serialize live data\n\n- Services, Events, Slots, Sessions, and their derived Cordis/DSH objects are internal live data, not ordinary JSON that can be dumped.\n- Do not apply JSON.stringify, structuredClone, recursive enumeration, full copying, or whole-object display to live data.\n- Read only the leaf fields required by the task, then construct the smallest owned data object without Host references.\n\n### Lifecycle: every side effect must be reversible\n\n- Services, Events, Tools, handlers, timers, Slots, styles, and theme overrides must all belong to the current Fiber.\n- Use ctx.effect(), ctx.on(), or official APIs that return a disposer so stop, update, or undefine removes every side effect.\n- The cordis-plugin-development Skill contains complete timer, Waterfall, Slot, theme, Tool, RPC, and React examples and troubleshooting guidance.\n\n## Host and Client\n\n- Host runs in the DSH Node.js process and is appropriate for files, networking, commands, Agent/Session access, Host Events, Services, model Tools, and JSON methods callable by the Client.\n- Client runs in the browser page and is appropriate for themes, layout, current page state, Tool cards, and Slot UI.\n- Host and Client communicate through Package-private JSON methods: Host uses harness.handle(method, handler), and Client uses host.call(method, args). The direction is Client→Host, and only lossless JSON may cross it.\n- Client UI must be registered in a queried Slot; apply() cannot directly return a React Element. Query Slots.listSubTree without root to choose from the compact purpose/topology tree, then query the exact root for its full registration contract and props before writing code.\n- See the Skill and Inspect Providers for Run-specific panels and exact Slot registration patterns.\n\n## Asynchronous results and recovery\n\n- Do not wait inside a Tool for approval or browser work that can happen only after the current turn ends.\n- Asynchronous success, rejection, and runtime errors update Run state and notify you through steering context.\n- After a technical failure, use cordis_inspect_self to read the exact Package source and its message/stack. Define a corrected Package under the same Plugin and retry autonomously.\n- Use the cordis-plugin-development Skill for other failure causes, repair procedures, and complete extension patterns.\n\nUse the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.\n\nUse subagent in the background by default. Start independent delegations together in one assistant message and continue useful work while they run. Set `run_in_background: false` only when your next action depends on that subagent's result. When a background run settles, the runtime sends you a notice containing its outcome and any final assistant message.\n\n## Writing code for run_code\n\n`run_code` takes two required arguments: `code` — the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped) — and `description`, a short summary of what the program does. Inside the program:\n\n- Call tools as `await tools.name(args)` — quoted access for exotic names: `tools[\"my-tool\"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.\n- A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue.\n- Independent read-only calls MAY overlap under `Promise.all` (safe calls run concurrently; mutating calls run alone, in submission order). Sequence dependent work with `await`.\n- Emit results with `return` and/or `console.log(...)`. Only what you print or return is program output. A successful tool result containing an image is attached after the run so you can inspect it on the next step; every other intermediate result stays out of the conversation, so extract just what you need.\n\nThe available tools:\n\n```ts\ntype JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }\n\ninterface ToolArgsMap {\n /** Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a job id immediately; read its output with `job_output` and stop it with `job_kill`. */\n bash: {\n /** The bash command to execute. */\n command: string;\n /** Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\". */\n description: string;\n /** Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry. */\n timeoutMs?: number;\n /** Working directory for this command. Defaults to the session workspace; a relative path is resolved against it. */\n workdir?: string;\n /** Run in the background and return a job id immediately (collect with job_output, stop with job_kill). No timeout applies. */\n run_in_background?: boolean;\n } & Record;\n /** Define an immutable Cordis Package. For a new Plugin, use kind:\"new\" and provide only a semantic prefix of 3–6 lowercase English letters; the Host returns the final pluginId and packageId. To modify an existing Plugin, use kind:\"existing\" with its exact pluginId to append a Package without overwriting older versions. Provide at least one of code.host and code.client. Each value is a plain JavaScript function body that returns a Cordis Plugin; no TypeScript, JSX, or import transformation occurs. Query Inspect before depending on a Service, Event, Builtin, Slot, or token. Define only validates parameters and syntax and records source: it does not request approval, execute apply, or change currentPackageId. On success, call cordis_run with the returned IDs. */\n cordis_define: {\n plugin: {\n kind: \"new\";\n /** Suggested semantic prefix of 3–6 lowercase English letters; the Host adds a unique numeric suffix. */\n idPrefix: string;\n } | {\n kind: \"existing\";\n /** Exact ID of an existing Plugin; the new Package is appended to that instance. */\n pluginId: string;\n };\n /** Short, readable Package name. */\n name: string;\n /** One-sentence, user-facing description of the Package purpose. */\n purpose: string;\n code: {\n /** Plain JavaScript function body that returns the Host-half Cordis Plugin. */\n host?: string;\n /** Plain JavaScript function body that returns the browser Client-half Cordis Plugin. */\n client?: string;\n };\n } & Record;\n /** List every Cordis Inspect Provider currently known to the Host, including local Host Providers and the latest manifests synchronized from the Client. Each entry includes its platform, purpose, read-only methods, and input/output schemas. Call this Tool before creating or modifying a Package, then select the provider and method for cordis_inspect_query from its result. Do not guess names or treat an Inspect method as a business Service that Plugin code can call. */\n cordis_inspect_list: Record;\n /** Run a read-only query explicitly declared by an Inspect Provider. platform, provider, and method must come from cordis_inspect_list, and input must satisfy that method's schema. Use this Tool before cordis_define to read exact Service methods, Event modes, Builtin signatures, Tool schemas, theme tokens, or live Slot trees and props. Host queries run locally. A Client query waits for the first valid page response and remains pending until a page answers or the Tool is cancelled. This Tool cannot invoke business Service methods or modify the runtime. For Service.listService and Event.listEvents, query without input to navigate the compact signature directory, then query the exact service or event for its structured contract and referenced types. For Slots.listSubTree, query without root to navigate the compact tree, then query the exact root for its complete registration contract and props. */\n cordis_inspect_query: {\n /** Runtime platform that owns the Provider. */\n platform: \"host\" | \"client\";\n /** Exact Provider ID returned by cordis_inspect_list. */\n provider: string;\n /** Exact method name declared by the Provider manifest. */\n method: string;\n /** Optional query input; it must satisfy the method input schema. */\n input?: JsonValue;\n } & Record;\n /** Inspect dynamic Cordis objects owned by the current Session at increasing levels of detail. With no IDs, list only Plugin summaries. With pluginId alone, return version pointers, the latest Run, and every Package summary. Only pluginId plus packageId returns that immutable Package's Host/Client source and runtime diagnostics. packageId cannot be supplied alone. Query an exact Package before handling @pluginId, repairing an asynchronous failure, or defining an updated version. This Tool is read-only: it neither executes code nor changes version pointers. */\n cordis_inspect_self: {\n /** Stable Plugin ID returned by cordis_define or injected by @pluginId; omit it to list every current Plugin. */\n pluginId?: string;\n /** Exact immutable Package ID owned by pluginId; when specified, source and diagnostics are returned. */\n packageId?: string;\n } & Record;\n /** Activate one exact Package of a dynamic Plugin. Use mode:\"run\" for the first activation, restarting currentPackageId, or rollback. When current exists, use mode:\"update\" to switch to a different Package, even if the Plugin is currently stopped. An unauthorized Client Package creates an approval request and returns awaiting-approval; an authorized Package returns starting and continues asynchronously in the browser. Neither result waits for the final outcome inside the Tool. currentPackageId changes only after complete success; on failure, the old current and target next remain. Asynchronous success, rejection, or technical failure is reported through state and steering. After a technical failure, read diagnostics with cordis_inspect_self, correct the same Plugin, and retry autonomously. Do not request approval again after the user rejects it. */\n cordis_run: {\n /** Stable Plugin ID returned by cordis_define. */\n pluginId: string;\n /** Exact immutable Package ID to activate under that Plugin. */\n packageId: string;\n /** Use run for the first activation, restarting current, or rollback; use update to switch from current to a different Package. */\n mode: \"run\" | \"update\";\n } & Record;\n /** Stop the current Run of a dynamic Plugin and cancel unfinished approval or activation requests. Retain the Plugin, every immutable Package, grants, currentPackageId, and nextPackageId so it can later run or update directly. Stopping an already stopped Plugin succeeds idempotently. Use this Tool to disable effects temporarily; use cordis_undefine for permanent removal. */\n cordis_stop: {\n /** Stable dynamic Plugin ID to stop. */\n pluginId: string;\n } & Record;\n /** Permanently remove a dynamic Plugin owned by the current Session. If it is running or awaiting approval, first stop it and cancel the request, then delete every Package, grant, and version pointer. After this returns, its pluginId, packageIds, @ reference, and Package business views are invalid; historical cards retain only a \"Plugin removed\" record. Do not call this Tool when versions must remain available for restart or rollback; use cordis_stop instead. */\n cordis_undefine: {\n /** Stable dynamic Plugin ID to remove permanently. */\n pluginId: string;\n } & Record;\n /** Edit an existing UTF-8 text file by replacing literal text. */\n edit: {\n /** Path to edit, resolved by the filesystem backend. */\n file_path: string;\n /** Literal text to replace. Must match exactly. */\n old_string: string;\n /** Literal replacement text. Use an empty string to delete the match. */\n new_string: string;\n /** Replace all matches. Defaults to false; when false, old_string must appear exactly once. */\n replace_all?: boolean;\n } & Record;\n /** Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op. */\n interrupt_agent: {\n /** The agent id of the running agent to interrupt. */\n agent_id: string;\n } & Record;\n /** Request cancellation of a running background job by job id. Returns immediately; the job settles as killed once its work actually stops. */\n job_kill: {\n /** Job id returned by the tool that started the background work. */\n job_id: string;\n /** Optional short reason, recorded in the log and forwarded to the job. */\n reason?: string;\n } & Record;\n /** List your background jobs (running and finished) with their ids, kinds, and statuses. */\n job_list: Record;\n /** Read a background job. Stream jobs return only output since the previous read; final-output jobs return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap. */\n job_output: {\n /** Job id returned by the tool that started the background work. */\n job_id: string;\n /** Block until the job reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the job alive. */\n wait?: boolean;\n /** Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum. */\n timeout_ms?: number;\n } & Record;\n /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */\n list_subagent_models: {\n /** Registered LLM provider id. Omit to list providers. */\n provider?: string;\n /** Exact model id to inspect. Requires provider; omit to list that provider's advertised models. */\n model?: string;\n } & Record;\n /** Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools. */\n ralph: {\n /** The immutable completion objective for every fresh Ralph round. */\n objective: string;\n /** Optional positive safe-integer round cap, bounded by the deployment ceiling. */\n maxRounds?: number;\n } & Record;\n /** Read a UTF-8 text file and return line-numbered content. */\n read: {\n /** Path to read, resolved by the filesystem backend. */\n file_path: string;\n /** 1-based first line to return. Defaults to 1. */\n offset?: number;\n /** Maximum number of lines to return. Defaults to 2000. */\n limit?: number;\n } & Record;\n /** Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered. */\n send_message: {\n /** The subagent id returned when the background subagent was started. */\n subagent_id: string;\n /** The message to deliver to the subagent. */\n message: string;\n } & Record;\n /** Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill. */\n skill: {\n /** The exact skill name from the available skills list. */\n name: string;\n } & Record;\n /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */\n subagent: {\n /** A short (3-5 word) description of the delegated task, for display. */\n description: string;\n /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */\n prompt: string;\n /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */\n provider?: string;\n /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */\n model?: string;\n /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */\n reasoning_effort?: string;\n /** Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it. */\n run_in_background?: boolean;\n } & Record;\n /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result. */\n subagent_fork: {\n /** A short (3-5 word) description of the delegated task, for display. */\n description: string;\n /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */\n prompt: string;\n } & Record;\n /** Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished). */\n todo_write: {\n /** The COMPLETE task list, replacing any previous list. */\n todos: ({\n /** What the task is — a short imperative line. */\n content: string;\n /** pending (not started) | in_progress (now) | completed (done). */\n status: \"pending\" | \"in_progress\" | \"completed\";\n })[];\n } & Record;\n /** Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn. The workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result. Script-body hooks: - `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly. - `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages. - `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`. - `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim. Misused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`. Constraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes. */\n workflow: {\n /** The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `). */\n script: string;\n /** The workflow identity block (plain JSON — never code). */\n meta: {\n /** Short kebab-case workflow name. */\n name: string;\n /** One-line description of what the workflow does. */\n description: string;\n /** Optional guidance on when this workflow applies. */\n whenToUse?: string;\n /** Optional phase declarations matched by phase() calls. */\n phases?: ({\n /** The phase title phase() calls match by exact string. */\n title: string;\n /** Optional one-line description of the phase. */\n detail?: string;\n /** Optional provider override this phase is expected to use. */\n provider?: string;\n /** Optional model override this phase is expected to use. */\n model?: string;\n } & Record)[];\n } & Record;\n /** Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}). */\n args?: Record;\n } & Record;\n /** Create or fully replace a UTF-8 text file. */\n write: {\n /** Path to write, resolved by the filesystem backend. */\n file_path: string;\n /** Full UTF-8 text content to write. */\n content: string;\n } & Record;\n}\n\ninterface ToolOutputMap {\n bash: {\n kind: \"background\";\n jobId: string;\n } | {\n kind: \"foreground\";\n exitCode: number | null;\n signal: string | null;\n timedOut: boolean;\n aborted: boolean;\n timeoutMs: number;\n stdout: {\n text: string;\n truncated: boolean;\n spillPath?: string;\n };\n stderr: {\n text: string;\n truncated: boolean;\n spillPath?: string;\n };\n sandbox?: {\n mode: string;\n denied: boolean;\n enforcement?: string;\n runnerFailed?: boolean;\n };\n };\n cordis_define: {\n pluginId: string;\n packageId: string;\n name: string;\n purpose: string;\n hasHostHalf: boolean;\n hasClientHalf: boolean;\n };\n cordis_inspect_list: JsonValue;\n cordis_inspect_query: JsonValue;\n cordis_inspect_self: JsonValue;\n cordis_run: JsonValue;\n cordis_stop: {\n pluginId: string;\n };\n cordis_undefine: {\n pluginId: string;\n wasRunning: boolean;\n };\n edit: {\n path: string;\n before: string;\n after: string;\n };\n interrupt_agent: {\n accepted: boolean;\n };\n job_kill: {\n outcome: \"cancellation-requested\" | \"already-finished\";\n job: {\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n };\n };\n job_list: ({\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n })[];\n job_output: {\n text: string;\n job: {\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n };\n };\n list_subagent_models: string;\n ralph: {\n runId: string;\n agentsStarted: number;\n result: JsonValue;\n };\n read: {\n path: string;\n offset: number;\n lines: {\n number: number;\n text: string;\n }[];\n totalLines: number;\n };\n send_message: {\n messageId: string;\n };\n skill: {\n name: string;\n provider: string;\n resourceBase?: {\n kind: \"directory\";\n path: string;\n } | {\n kind: \"url\";\n url: string;\n } | {\n kind: \"opaque\";\n description: string;\n };\n content: string;\n };\n subagent: {\n kind: \"background\";\n jobId: string;\n } | {\n kind: \"continuable\";\n subagentId: string;\n } | {\n kind: \"foreground\";\n runId: string;\n output: JsonValue[];\n };\n subagent_fork: {\n kind: \"background\";\n jobId: string;\n } | {\n kind: \"continuable\";\n subagentId: string;\n } | {\n kind: \"foreground\";\n runId: string;\n output: JsonValue[];\n };\n todo_write: {\n todos: ({\n content: string;\n status: \"pending\" | \"in_progress\" | \"completed\";\n })[];\n counts: {\n pending: number;\n inProgress: number;\n completed: number;\n };\n };\n workflow: {\n runId: string;\n agentsStarted: number;\n result: JsonValue;\n };\n write: {\n path: string;\n operation: \"create\" | \"update\";\n before: string | null;\n after: string;\n };\n}\n\ntype ToolName = keyof ToolOutputMap\n\ndeclare class ToolCallError extends Error {\n readonly name: \"ToolCallError\";\n readonly toolName: ToolName;\n}\n\ndeclare const tools: {\n [K in ToolName]: (args: ToolArgsMap[K]) => Promise;\n}\n```","tools":[{"name":"bash","description":"Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a job id immediately; read its output with `job_output` and stop it with `job_kill`.","parameters":{"type":"object","properties":{"command":{"type":"string","description":"The bash command to execute."},"description":{"type":"string","description":"Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."},"timeoutMs":{"type":"number","description":"Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."},"workdir":{"type":"string","description":"Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."},"run_in_background":{"type":"boolean","description":"Run in the background and return a job id immediately (collect with job_output, stop with job_kill). No timeout applies."}},"required":["command","description"]}},{"name":"cordis_define","description":"Define an immutable Cordis Package. For a new Plugin, use kind:\"new\" and provide only a semantic prefix of 3–6 lowercase English letters; the Host returns the final pluginId and packageId. To modify an existing Plugin, use kind:\"existing\" with its exact pluginId to append a Package without overwriting older versions. Provide at least one of code.host and code.client. Each value is a plain JavaScript function body that returns a Cordis Plugin; no TypeScript, JSX, or import transformation occurs. Query Inspect before depending on a Service, Event, Builtin, Slot, or token. Define only validates parameters and syntax and records source: it does not request approval, execute apply, or change currentPackageId. On success, call cordis_run with the returned IDs.","parameters":{"type":"object","properties":{"plugin":{"oneOf":[{"type":"object","additionalProperties":false,"properties":{"kind":{"type":"string","const":"new"},"idPrefix":{"type":"string","description":"Suggested semantic prefix of 3–6 lowercase English letters; the Host adds a unique numeric suffix."}},"required":["kind","idPrefix"]},{"type":"object","additionalProperties":false,"properties":{"kind":{"type":"string","const":"existing"},"pluginId":{"type":"string","description":"Exact ID of an existing Plugin; the new Package is appended to that instance."}},"required":["kind","pluginId"]}]},"name":{"type":"string","description":"Short, readable Package name."},"purpose":{"type":"string","description":"One-sentence, user-facing description of the Package purpose."},"code":{"type":"object","additionalProperties":false,"properties":{"host":{"type":"string","description":"Plain JavaScript function body that returns the Host-half Cordis Plugin."},"client":{"type":"string","description":"Plain JavaScript function body that returns the browser Client-half Cordis Plugin."}}}},"required":["plugin","name","purpose","code"]}},{"name":"cordis_inspect_list","description":"List every Cordis Inspect Provider currently known to the Host, including local Host Providers and the latest manifests synchronized from the Client. Each entry includes its platform, purpose, read-only methods, and input/output schemas. Call this Tool before creating or modifying a Package, then select the provider and method for cordis_inspect_query from its result. Do not guess names or treat an Inspect method as a business Service that Plugin code can call.","parameters":{"type":"object","properties":{}}},{"name":"cordis_inspect_query","description":"Run a read-only query explicitly declared by an Inspect Provider. platform, provider, and method must come from cordis_inspect_list, and input must satisfy that method's schema. Use this Tool before cordis_define to read exact Service methods, Event modes, Builtin signatures, Tool schemas, theme tokens, or live Slot trees and props. Host queries run locally. A Client query waits for the first valid page response and remains pending until a page answers or the Tool is cancelled. This Tool cannot invoke business Service methods or modify the runtime. For Service.listService and Event.listEvents, query without input to navigate the compact signature directory, then query the exact service or event for its structured contract and referenced types. For Slots.listSubTree, query without root to navigate the compact tree, then query the exact root for its complete registration contract and props.","parameters":{"type":"object","properties":{"platform":{"type":"string","description":"Runtime platform that owns the Provider.","enum":["host","client"]},"provider":{"type":"string","description":"Exact Provider ID returned by cordis_inspect_list."},"method":{"type":"string","description":"Exact method name declared by the Provider manifest."},"input":{"description":"Optional query input; it must satisfy the method input schema."}},"required":["platform","provider","method"]}},{"name":"cordis_inspect_self","description":"Inspect dynamic Cordis objects owned by the current Session at increasing levels of detail. With no IDs, list only Plugin summaries. With pluginId alone, return version pointers, the latest Run, and every Package summary. Only pluginId plus packageId returns that immutable Package's Host/Client source and runtime diagnostics. packageId cannot be supplied alone. Query an exact Package before handling @pluginId, repairing an asynchronous failure, or defining an updated version. This Tool is read-only: it neither executes code nor changes version pointers.","parameters":{"type":"object","properties":{"pluginId":{"type":"string","description":"Stable Plugin ID returned by cordis_define or injected by @pluginId; omit it to list every current Plugin."},"packageId":{"type":"string","description":"Exact immutable Package ID owned by pluginId; when specified, source and diagnostics are returned."}}}},{"name":"cordis_run","description":"Activate one exact Package of a dynamic Plugin. Use mode:\"run\" for the first activation, restarting currentPackageId, or rollback. When current exists, use mode:\"update\" to switch to a different Package, even if the Plugin is currently stopped. An unauthorized Client Package creates an approval request and returns awaiting-approval; an authorized Package returns starting and continues asynchronously in the browser. Neither result waits for the final outcome inside the Tool. currentPackageId changes only after complete success; on failure, the old current and target next remain. Asynchronous success, rejection, or technical failure is reported through state and steering. After a technical failure, read diagnostics with cordis_inspect_self, correct the same Plugin, and retry autonomously. Do not request approval again after the user rejects it.","parameters":{"type":"object","properties":{"pluginId":{"type":"string","description":"Stable Plugin ID returned by cordis_define."},"packageId":{"type":"string","description":"Exact immutable Package ID to activate under that Plugin."},"mode":{"type":"string","description":"Use run for the first activation, restarting current, or rollback; use update to switch from current to a different Package.","enum":["run","update"]}},"required":["pluginId","packageId","mode"]}},{"name":"cordis_stop","description":"Stop the current Run of a dynamic Plugin and cancel unfinished approval or activation requests. Retain the Plugin, every immutable Package, grants, currentPackageId, and nextPackageId so it can later run or update directly. Stopping an already stopped Plugin succeeds idempotently. Use this Tool to disable effects temporarily; use cordis_undefine for permanent removal.","parameters":{"type":"object","properties":{"pluginId":{"type":"string","description":"Stable dynamic Plugin ID to stop."}},"required":["pluginId"]}},{"name":"cordis_undefine","description":"Permanently remove a dynamic Plugin owned by the current Session. If it is running or awaiting approval, first stop it and cancel the request, then delete every Package, grant, and version pointer. After this returns, its pluginId, packageIds, @ reference, and Package business views are invalid; historical cards retain only a \"Plugin removed\" record. Do not call this Tool when versions must remain available for restart or rollback; use cordis_stop instead.","parameters":{"type":"object","properties":{"pluginId":{"type":"string","description":"Stable dynamic Plugin ID to remove permanently."}},"required":["pluginId"]}},{"name":"edit","description":"Edit an existing UTF-8 text file by replacing literal text.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to edit, resolved by the filesystem backend."},"old_string":{"type":"string","description":"Literal text to replace. Must match exactly."},"new_string":{"type":"string","description":"Literal replacement text. Use an empty string to delete the match."},"replace_all":{"type":"boolean","description":"Replace all matches. Defaults to false; when false, old_string must appear exactly once."}},"required":["file_path","old_string","new_string"]}},{"name":"interrupt_agent","description":"Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op.","parameters":{"type":"object","properties":{"agent_id":{"type":"string","description":"The agent id of the running agent to interrupt."}},"required":["agent_id"]}},{"name":"job_kill","description":"Request cancellation of a running background job by job id. Returns immediately; the job settles as killed once its work actually stops.","parameters":{"type":"object","properties":{"job_id":{"type":"string","description":"Job id returned by the tool that started the background work."},"reason":{"type":"string","description":"Optional short reason, recorded in the log and forwarded to the job."}},"required":["job_id"]}},{"name":"job_list","description":"List your background jobs (running and finished) with their ids, kinds, and statuses.","parameters":{"type":"object","properties":{}}},{"name":"job_output","description":"Read a background job. Stream jobs return only output since the previous read; final-output jobs return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.","parameters":{"type":"object","properties":{"job_id":{"type":"string","description":"Job id returned by the tool that started the background work."},"wait":{"type":"boolean","description":"Block until the job reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the job alive."},"timeout_ms":{"type":"number","description":"Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."}},"required":["job_id"]}},{"name":"list_subagent_models","description":"Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.","parameters":{"type":"object","properties":{"provider":{"type":"string","description":"Registered LLM provider id. Omit to list providers."},"model":{"type":"string","description":"Exact model id to inspect. Requires provider; omit to list that provider's advertised models."}}}},{"name":"ralph","description":"Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.","parameters":{"type":"object","properties":{"objective":{"type":"string","description":"The immutable completion objective for every fresh Ralph round."},"maxRounds":{"type":"number","description":"Optional positive safe-integer round cap, bounded by the deployment ceiling."}},"required":["objective"]}},{"name":"read","description":"Read a UTF-8 text file and return line-numbered content.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to read, resolved by the filesystem backend."},"offset":{"type":"number","description":"1-based first line to return. Defaults to 1."},"limit":{"type":"number","description":"Maximum number of lines to return. Defaults to 2000."}},"required":["file_path"]}},{"name":"run_code","description":"Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the BODY of an async function (erasable syntax only; top-level `await` and `return` work), and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return is program output — curate it. Image-bearing subtool results are attached after the run.","parameters":{"type":"object","properties":{"code":{"type":"string","description":"The program: the body of an async TypeScript function."},"description":{"type":"string","description":"Clear, concise description of what this program does in active voice, 5-10 words (shown in the UI). Examples: \"Count TODO markers across packages\"; \"Read failing test and its fixture\"; \"Rename config key in every cordis.yml\"."}},"required":["code","description"]}},{"name":"send_message","description":"Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered.","parameters":{"type":"object","properties":{"subagent_id":{"type":"string","description":"The subagent id returned when the background subagent was started."},"message":{"type":"string","description":"The message to deliver to the subagent."}},"required":["subagent_id","message"]}},{"name":"skill","description":"Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.","parameters":{"type":"object","properties":{"name":{"type":"string","description":"The exact skill name from the available skills list."}},"required":["name"]}},{"name":"subagent","description":"Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."},"provider":{"type":"string","description":"LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route."},"model":{"type":"string","description":"Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route."},"reasoning_effort":{"type":"string","description":"Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default."},"run_in_background":{"type":"boolean","description":"Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it."}},"required":["description","prompt"]}},{"name":"subagent_fork","description":"Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."}},"required":["description","prompt"]}},{"name":"todo_write","description":"Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).","parameters":{"type":"object","properties":{"todos":{"type":"array","description":"The COMPLETE task list, replacing any previous list.","items":{"type":"object","additionalProperties":false,"properties":{"content":{"type":"string","description":"What the task is — a short imperative line."},"status":{"type":"string","description":"pending (not started) | in_progress (now) | completed (done).","enum":["pending","in_progress","completed"]}},"required":["content","status"]}}},"required":["todos"]}},{"name":"workflow","description":"Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.\n\nThe workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result.\n\nScript-body hooks:\n- `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly.\n- `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages.\n- `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`.\n- `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim.\n\nMisused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`.\n\nConstraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.","parameters":{"type":"object","properties":{"script":{"type":"string","description":"The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `)."},"meta":{"type":"object","description":"The workflow identity block (plain JSON — never code).","additionalProperties":true,"properties":{"name":{"type":"string","description":"Short kebab-case workflow name."},"description":{"type":"string","description":"One-line description of what the workflow does."},"whenToUse":{"type":"string","description":"Optional guidance on when this workflow applies."},"phases":{"type":"array","description":"Optional phase declarations matched by phase() calls.","items":{"type":"object","additionalProperties":true,"properties":{"title":{"type":"string","description":"The phase title phase() calls match by exact string."},"detail":{"type":"string","description":"Optional one-line description of the phase."},"provider":{"type":"string","description":"Optional provider override this phase is expected to use."},"model":{"type":"string","description":"Optional model override this phase is expected to use."}},"required":["title"]}}},"required":["name","description"]},"args":{"type":"object","description":"Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}).","additionalProperties":true}},"required":["script","meta"]}},{"name":"write","description":"Create or fully replace a UTF-8 text file.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to write, resolved by the filesystem backend."},"content":{"type":"string","description":"Full UTF-8 text content to write."}},"required":["file_path","content"]}}]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"WORKFLOW_CHILD_OK"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"WORKFLOW_CHILD_OK"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"WORKFLOW_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"cd78f077-1fad-4cdc-ab56-09d39d9095cd"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[10,11,12,13,14],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"WORKFLOW_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"c07ab6db-7108-46b4-ad18-3f81e9609546"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[10,11,12,13,14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/headless-agent/tests/snapshots/advanced-toolchain/session.jsonl b/examples/headless-agent/tests/snapshots/advanced-toolchain/session.jsonl index c49d9b2745..4bed94ecda 100644 --- a/examples/headless-agent/tests/snapshots/advanced-toolchain/session.jsonl +++ b/examples/headless-agent/tests/snapshots/advanced-toolchain/session.jsonl @@ -1,20 +1,20 @@ {"type":"session","version":0,"id":"11111111-1111-4111-8111-111111111111","createdAt":1783950000000,"cwd":"{{cwd}}","delegationDepth":0} -{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Run this advanced flow exactly once: define a host-only dynamic Cordis Package named Snapshot Marker; run and inspect snap-1/pkg-1 through run_code; delegate once to a direct spawn child; run one workflow that delegates to another spawn child; remove snap-1; then reply with exactly ADVANCED_HEADLESS_OK."}],"source":{"kind":"user"},"role":"user","id":"8a0ac233-283c-4eb4-8bbd-5c50b7e99afe"}]}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Run this advanced flow exactly once: define a host-only dynamic Cordis Package named Snapshot Marker; run and inspect snap-1/pkg-1 through run_code; delegate once to a direct spawn child; run one workflow that delegates to another spawn child; remove snap-1; then reply with exactly ADVANCED_HEADLESS_OK."}],"source":{"kind":"user"},"role":"user","id":"e203969f-330f-4886-8d34-82d0001db89b"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":1,"step":1}} -{"type":"user/message","data":{"content":[{"type":"text","text":"Run this advanced flow exactly once: define a host-only dynamic Cordis Package named Snapshot Marker; run and inspect snap-1/pkg-1 through run_code; delegate once to a direct spawn child; run one workflow that delegates to another spawn child; remove snap-1; then reply with exactly ADVANCED_HEADLESS_OK."}],"source":{"kind":"user"},"role":"user","id":"8a0ac233-283c-4eb4-8bbd-5c50b7e99afe"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Run this advanced flow exactly once: define a host-only dynamic Cordis Package named Snapshot Marker; run and inspect snap-1/pkg-1 through run_code; delegate once to a direct spawn child; run one workflow that delegates to another spawn child; remove snap-1; then reply with exactly ADVANCED_HEADLESS_OK."}],"source":{"kind":"user"},"role":"user","id":"e203969f-330f-4886-8d34-82d0001db89b"},"surfaceOp":"append"} {"type":"session/title","data":{"title":"Run this advanced flow exactly","messageSeqs":[4],"source":{"kind":"fallback"}}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"You are an AI agent powered by DeepSeek Harness.\n\nYou are headless-agent, a coding assistant powered by the deepseek-v4-flash model. Your working directory is {{cwd}}.\n\nVerify your work by running the code or tests. Keep answers brief and factual.\n\n\nUse the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.\n\nUse the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-observation-policy requires it) and prefer edit for targeted changes.\n\nUse the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-observation-policy requires it), unless you just created or edited it in this session.\n\nCheck the [exit code: N] marker on every bash result; investigate failures before moving on.\n\nTrack every background job id you start. You are notified in-session when a job finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running job's work. Before giving a final answer, collect every still-relevant job with job_output (set wait: true only when you are genuinely blocked on it), and job_kill jobs that stopped mattering.\n\nUse the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls.\n\n# Dynamic Cordis Plugins\n\nDynamic Cordis plugins temporarily extend the current DSH process. A Plugin uses apply(ctx) to consume Services, listen to Events, provide Services, register model Tools, or register browser UI in Slots.\n\n- Plugin and Package definitions exist only in the current process. define itself does not modify repository source, configuration, or disk, and definitions do not survive a process restart.\n- The restricted execution environment prevents accidental misuse; it is not a security boundary for malicious code. Services obtained by dynamic code connect to the real runtime.\n\n## Make the user-facing plan clear first\n\n- Dynamic Cordis Plugins are one available implementation mechanism, not the default for every request. Consider whether one could help only when the user intends to design or create something, or when a temporary interface could materially aid the current work. The presence of these instructions or Tools, and discussion of Cordis itself, do not make a request a dynamic-Plugin task.\n- When Cordis is a plausible fit, infer the intended work target and lifetime from the request and conversation. Use it only when the outcome belongs to the current running harness and should be delivered as a temporary runtime extension. If that distinction is materially ambiguous, ask at most one concise question about the intended result or lifetime. Otherwise proceed with the matching workflow; do not require the user to know or choose Cordis as an implementation mechanism.\n- Once a dynamic Plugin is appropriate, decide whether the task creates a new Plugin or modifies the Plugin named by the user with @pluginId. Proceed directly when the goal is clear; do not ask for repeated confirmation.\n- Choose Host, Client, or both from the requested outcome. Do not propose a Client/browser UI when the task does not need visible page behavior, and do not avoid Client when the requested outcome is visual, interactive, or depends on page state. Host versus Client is an implementation choice; do not make the user choose it.\n- When a design direction or a potentially useful interface would materially affect the result, ask at most one concise outcome or creative-preference question and offer a few candidate directions. Otherwise proceed directly; do not conduct a multi-round interview or a complex questionnaire.\n- cordis_define only defines and presents code; it does not run it. After definition, explain the pluginId and packageId returned by the Host and whether the next step is a run or update.\n- cordis_run may require user approval. When it returns awaiting-approval, explain that the user must allow or reject it in the UI. Do not wait, retry, or claim that it is running.\n- When it returns starting, explain that the request has entered the asynchronous flow and the Client is still activating. starting does not mean success. Wait for the system to report the final result through steering context.\n- Do not request approval again after the user rejects it. After a technical failure, fix the same Plugin from its diagnostics; do not silently create a replacement Plugin.\n\n## Recommended workflow and Tools\n\nBefore creating, modifying, or repairing a Plugin, load the cordis-plugin-development Skill. The Skill provides requirement navigation, capability composition, complete examples, and troubleshooting. Treat Inspect Provider results as the source of truth for exact APIs.\n\n1. cordis_inspect_list: discover the current Host and Client Providers and their read-only query methods.\n2. cordis_inspect_query: use the returned platform, provider, method, and schema to query exact Service, Event, Builtin, Slot, Theme token, or Tool information.\n3. cordis_inspect_self: inspect the current Session's Plugins, Packages, version pointers, source, and diagnostics. Source is returned only when both pluginId and packageId are specified.\n4. cordis_define: create the first Package for a new Plugin or append an immutable Package to an existing Plugin. It defines code but does not run it.\n5. cordis_run: activate an exact Package. Use run for the first activation, restarting current, or rollback; use update to switch versions.\n6. cordis_stop: remove the current Run and pending approval request while retaining definitions, grants, and version pointers.\n7. cordis_undefine: permanently stop and delete a Plugin and all of its Packages. Use it only after confirming that the user no longer needs them.\n\n- Inspect and Catalog data only confirm capabilities, names, signatures, types, and registration protocols before code is written; they do not replace business APIs.\n- Query Service.listService and Event.listEvents without input to choose from their compact signature directories, then query the exact service or event before using it. Exact queries return the structured contract and only its referenced types.\n- At runtime, a Plugin must call real Services or listen to real Events. Do not cache, display, or depend on Inspect results as business data.\n\n## Identity, versions, and approval\n\n- pluginId identifies a Plugin that can be modified over time. For a new Plugin, submit only a semantic idPrefix of 3–6 lowercase English letters; the Host allocates the final ID.\n- packageId identifies one immutable Host/Client source version under a Plugin. To change code, define a new Package; never overwrite an old version.\n- pluginRunId identifies one activation attempt and connects its approval, Host/Client loading, private RPC, Run card, and errors.\n- currentPackageId is the most recent fully successful Package. Stopping, starting an update, or failing an update does not clear it.\n- nextPackageId is the target awaiting approval, being attempted, awaiting Client activation, or most recently failed.\n- A single check mark authorizes only the current Package; double check marks authorize future versions of the same Plugin. A grant remains in effect after a technical failure.\n- An update stops the old Run before starting the target Package. Failure does not automatically restart the old version; retry next with update or roll back to current with run.\n\nWhen the user enters @pluginId, the system injects identity, the default base Package, version pointers, and runtime status, but not source code:\n\n1. Call cordis_inspect_self(pluginId, packageId) to read the target source.\n2. Use cordis_define in existing mode to append a Package to the same Plugin.\n3. Call cordis_run in run or update mode according to the version relationship.\n\nNever silently create another Plugin for @pluginId. If the reference is unavailable because it was removed, belongs to another Session, or was lost on process restart, tell the user directly.\n\n## High-frequency errors that must be avoided\n\n### Services: ctx.get and inject\n\n- Read an optional Service with ctx.get('serviceName') by default and handle undefined.\n- Declare inject: ['serviceName'] on the returned Plugin object only when the Service is a hard dependency and the Plugin must enter waiting until Cordis reactivates it after the Service appears.\n- Read ctx.serviceName only after declaring that Service in inject. Never access an undeclared Service as a ctx property.\n\n```js\nreturn {\n inject: ['requiredService'],\n apply(ctx) {\n ctx.requiredService.someMethod()\n const optionalService = ctx.get('optionalService')\n if (optionalService !== undefined) optionalService.someMethod()\n },\n}\n```\n\n### Code: use plain JavaScript only\n\n- Host and Client code is not transformed by TypeScript, JSX, or a bundler.\n- Do not use TypeScript types, as, decorators, import, require, or JSX.\n- Client React code must use React.createElement(...); never write .\n- Do not assume that process, Buffer, window, document, fetch, native timers, or any other global is available. Query the corresponding platform's Builtins and Services first.\n\n### Data: do not serialize live data\n\n- Services, Events, Slots, Sessions, and their derived Cordis/DSH objects are internal live data, not ordinary JSON that can be dumped.\n- Do not apply JSON.stringify, structuredClone, recursive enumeration, full copying, or whole-object display to live data.\n- Read only the leaf fields required by the task, then construct the smallest owned data object without Host references.\n\n### Lifecycle: every side effect must be reversible\n\n- Services, Events, Tools, handlers, timers, Slots, styles, and theme overrides must all belong to the current Fiber.\n- Use ctx.effect(), ctx.on(), or official APIs that return a disposer so stop, update, or undefine removes every side effect.\n- The cordis-plugin-development Skill contains complete timer, Waterfall, Slot, theme, Tool, RPC, and React examples and troubleshooting guidance.\n\n## Host and Client\n\n- Host runs in the DSH Node.js process and is appropriate for files, networking, commands, Agent/Session access, Host Events, Services, model Tools, and JSON methods callable by the Client.\n- Client runs in the browser page and is appropriate for themes, layout, current page state, Tool cards, and Slot UI.\n- Host and Client communicate through Package-private JSON methods: Host uses harness.handle(method, handler), and Client uses host.call(method, args). The direction is Client→Host, and only lossless JSON may cross it.\n- Client UI must be registered in a queried Slot; apply() cannot directly return a React Element. Query Slots.listSubTree without root to choose from the compact purpose/topology tree, then query the exact root for its full registration contract and props before writing code.\n- See the Skill and Inspect Providers for Run-specific panels and exact Slot registration patterns.\n\n## Asynchronous results and recovery\n\n- Do not wait inside a Tool for approval or browser work that can happen only after the current turn ends.\n- Asynchronous success, rejection, and runtime errors update Run state and notify you through steering context.\n- After a technical failure, use cordis_inspect_self to read the exact Package source and its message/stack. Define a corrected Package under the same Plugin and retry autonomously.\n- Use the cordis-plugin-development Skill for other failure causes, repair procedures, and complete extension patterns.\n\nUse the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.\n\nUse subagent in the background by default. Start independent delegations together in one assistant message and continue useful work while they run. Set `run_in_background: false` only when your next action depends on that subagent's result. When a background run settles, the runtime sends you a notice containing its outcome and any final assistant message.\n\n## Writing code for run_code\n\n`run_code` takes two required arguments: `code` — the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped) — and `description`, a short summary of what the program does. Inside the program:\n\n- Call tools as `await tools.name(args)` — quoted access for exotic names: `tools[\"my-tool\"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.\n- A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue.\n- Independent read-only calls MAY overlap under `Promise.all` (safe calls run concurrently; mutating calls run alone, in submission order). Sequence dependent work with `await`.\n- Emit results with `return` and/or `console.log(...)`. ONLY what you print or return comes back to you — intermediate tool results never enter the conversation, so extract just what you need.\n\nThe available tools:\n\n```ts\ntype JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }\n\ninterface ToolArgsMap {\n /** Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a job id immediately; read its output with `job_output` and stop it with `job_kill`. */\n bash: {\n /** The bash command to execute. */\n command: string;\n /** Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\". */\n description: string;\n /** Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry. */\n timeoutMs?: number;\n /** Working directory for this command. Defaults to the session workspace; a relative path is resolved against it. */\n workdir?: string;\n /** Run in the background and return a job id immediately (collect with job_output, stop with job_kill). No timeout applies. */\n run_in_background?: boolean;\n } & Record;\n /** Define an immutable Cordis Package. For a new Plugin, use kind:\"new\" and provide only a semantic prefix of 3–6 lowercase English letters; the Host returns the final pluginId and packageId. To modify an existing Plugin, use kind:\"existing\" with its exact pluginId to append a Package without overwriting older versions. Provide at least one of code.host and code.client. Each value is a plain JavaScript function body that returns a Cordis Plugin; no TypeScript, JSX, or import transformation occurs. Query Inspect before depending on a Service, Event, Builtin, Slot, or token. Define only validates parameters and syntax and records source: it does not request approval, execute apply, or change currentPackageId. On success, call cordis_run with the returned IDs. */\n cordis_define: {\n plugin: {\n kind: \"new\";\n /** Suggested semantic prefix of 3–6 lowercase English letters; the Host adds a unique numeric suffix. */\n idPrefix: string;\n } | {\n kind: \"existing\";\n /** Exact ID of an existing Plugin; the new Package is appended to that instance. */\n pluginId: string;\n };\n /** Short, readable Package name. */\n name: string;\n /** One-sentence, user-facing description of the Package purpose. */\n purpose: string;\n code: {\n /** Plain JavaScript function body that returns the Host-half Cordis Plugin. */\n host?: string;\n /** Plain JavaScript function body that returns the browser Client-half Cordis Plugin. */\n client?: string;\n };\n } & Record;\n /** List every Cordis Inspect Provider currently known to the Host, including local Host Providers and the latest manifests synchronized from the Client. Each entry includes its platform, purpose, read-only methods, and input/output schemas. Call this Tool before creating or modifying a Package, then select the provider and method for cordis_inspect_query from its result. Do not guess names or treat an Inspect method as a business Service that Plugin code can call. */\n cordis_inspect_list: Record;\n /** Run a read-only query explicitly declared by an Inspect Provider. platform, provider, and method must come from cordis_inspect_list, and input must satisfy that method's schema. Use this Tool before cordis_define to read exact Service methods, Event modes, Builtin signatures, Tool schemas, theme tokens, or live Slot trees and props. Host queries run locally. A Client query waits for the first valid page response and remains pending until a page answers or the Tool is cancelled. This Tool cannot invoke business Service methods or modify the runtime. For Service.listService and Event.listEvents, query without input to navigate the compact signature directory, then query the exact service or event for its structured contract and referenced types. For Slots.listSubTree, query without root to navigate the compact tree, then query the exact root for its complete registration contract and props. */\n cordis_inspect_query: {\n /** Runtime platform that owns the Provider. */\n platform: \"host\" | \"client\";\n /** Exact Provider ID returned by cordis_inspect_list. */\n provider: string;\n /** Exact method name declared by the Provider manifest. */\n method: string;\n /** Optional query input; it must satisfy the method input schema. */\n input?: JsonValue;\n } & Record;\n /** Inspect dynamic Cordis objects owned by the current Session at increasing levels of detail. With no IDs, list only Plugin summaries. With pluginId alone, return version pointers, the latest Run, and every Package summary. Only pluginId plus packageId returns that immutable Package's Host/Client source and runtime diagnostics. packageId cannot be supplied alone. Query an exact Package before handling @pluginId, repairing an asynchronous failure, or defining an updated version. This Tool is read-only: it neither executes code nor changes version pointers. */\n cordis_inspect_self: {\n /** Stable Plugin ID returned by cordis_define or injected by @pluginId; omit it to list every current Plugin. */\n pluginId?: string;\n /** Exact immutable Package ID owned by pluginId; when specified, source and diagnostics are returned. */\n packageId?: string;\n } & Record;\n /** Activate one exact Package of a dynamic Plugin. Use mode:\"run\" for the first activation, restarting currentPackageId, or rollback. When current exists, use mode:\"update\" to switch to a different Package, even if the Plugin is currently stopped. An unauthorized Client Package creates an approval request and returns awaiting-approval; an authorized Package returns starting and continues asynchronously in the browser. Neither result waits for the final outcome inside the Tool. currentPackageId changes only after complete success; on failure, the old current and target next remain. Asynchronous success, rejection, or technical failure is reported through state and steering. After a technical failure, read diagnostics with cordis_inspect_self, correct the same Plugin, and retry autonomously. Do not request approval again after the user rejects it. */\n cordis_run: {\n /** Stable Plugin ID returned by cordis_define. */\n pluginId: string;\n /** Exact immutable Package ID to activate under that Plugin. */\n packageId: string;\n /** Use run for the first activation, restarting current, or rollback; use update to switch from current to a different Package. */\n mode: \"run\" | \"update\";\n } & Record;\n /** Stop the current Run of a dynamic Plugin and cancel unfinished approval or activation requests. Retain the Plugin, every immutable Package, grants, currentPackageId, and nextPackageId so it can later run or update directly. Stopping an already stopped Plugin succeeds idempotently. Use this Tool to disable effects temporarily; use cordis_undefine for permanent removal. */\n cordis_stop: {\n /** Stable dynamic Plugin ID to stop. */\n pluginId: string;\n } & Record;\n /** Permanently remove a dynamic Plugin owned by the current Session. If it is running or awaiting approval, first stop it and cancel the request, then delete every Package, grant, and version pointer. After this returns, its pluginId, packageIds, @ reference, and Package business views are invalid; historical cards retain only a \"Plugin removed\" record. Do not call this Tool when versions must remain available for restart or rollback; use cordis_stop instead. */\n cordis_undefine: {\n /** Stable dynamic Plugin ID to remove permanently. */\n pluginId: string;\n } & Record;\n /** Edit an existing UTF-8 text file by replacing literal text. */\n edit: {\n /** Path to edit, resolved by the filesystem backend. */\n file_path: string;\n /** Literal text to replace. Must match exactly. */\n old_string: string;\n /** Literal replacement text. Use an empty string to delete the match. */\n new_string: string;\n /** Replace all matches. Defaults to false; when false, old_string must appear exactly once. */\n replace_all?: boolean;\n } & Record;\n /** Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op. */\n interrupt_agent: {\n /** The agent id of the running agent to interrupt. */\n agent_id: string;\n } & Record;\n /** Request cancellation of a running background job by job id. Returns immediately; the job settles as killed once its work actually stops. */\n job_kill: {\n /** Job id returned by the tool that started the background work. */\n job_id: string;\n /** Optional short reason, recorded in the log and forwarded to the job. */\n reason?: string;\n } & Record;\n /** List your background jobs (running and finished) with their ids, kinds, and statuses. */\n job_list: Record;\n /** Read a background job. Stream jobs return only output since the previous read; final-output jobs return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap. */\n job_output: {\n /** Job id returned by the tool that started the background work. */\n job_id: string;\n /** Block until the job reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the job alive. */\n wait?: boolean;\n /** Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum. */\n timeout_ms?: number;\n } & Record;\n /** Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools. */\n ralph: {\n /** The immutable completion objective for every fresh Ralph round. */\n objective: string;\n /** Optional positive safe-integer round cap, bounded by the deployment ceiling. */\n maxRounds?: number;\n } & Record;\n /** Read a UTF-8 text file and return line-numbered content. */\n read: {\n /** Path to read, resolved by the filesystem backend. */\n file_path: string;\n /** 1-based first line to return. Defaults to 1. */\n offset?: number;\n /** Maximum number of lines to return. Defaults to 2000. */\n limit?: number;\n } & Record;\n /** Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered. */\n send_message: {\n /** The subagent id returned when the background subagent was started. */\n subagent_id: string;\n /** The message to deliver to the subagent. */\n message: string;\n } & Record;\n /** Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill. */\n skill: {\n /** The exact skill name from the available skills list. */\n name: string;\n } & Record;\n /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. */\n subagent: {\n /** A short (3-5 word) description of the delegated task, for display. */\n description: string;\n /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */\n prompt: string;\n /** Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it. */\n run_in_background?: boolean;\n } & Record;\n /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result. */\n subagent_fork: {\n /** A short (3-5 word) description of the delegated task, for display. */\n description: string;\n /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */\n prompt: string;\n } & Record;\n /** Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished). */\n todo_write: {\n /** The COMPLETE task list, replacing any previous list. */\n todos: ({\n /** What the task is — a short imperative line. */\n content: string;\n /** pending (not started) | in_progress (now) | completed (done). */\n status: \"pending\" | \"in_progress\" | \"completed\";\n })[];\n } & Record;\n /** Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn. The workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result. Script-body hooks: - `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly. - `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages. - `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`. - `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim. Misused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`. Constraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes. */\n workflow: {\n /** The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `). */\n script: string;\n /** The workflow identity block (plain JSON — never code). */\n meta: {\n /** Short kebab-case workflow name. */\n name: string;\n /** One-line description of what the workflow does. */\n description: string;\n /** Optional guidance on when this workflow applies. */\n whenToUse?: string;\n /** Optional phase declarations matched by phase() calls. */\n phases?: ({\n /** The phase title phase() calls match by exact string. */\n title: string;\n /** Optional one-line description of the phase. */\n detail?: string;\n /** Optional provider override this phase is expected to use. */\n provider?: string;\n /** Optional model override this phase is expected to use. */\n model?: string;\n } & Record)[];\n } & Record;\n /** Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}). */\n args?: Record;\n } & Record;\n /** Create or fully replace a UTF-8 text file. */\n write: {\n /** Path to write, resolved by the filesystem backend. */\n file_path: string;\n /** Full UTF-8 text content to write. */\n content: string;\n } & Record;\n}\n\ninterface ToolOutputMap {\n bash: {\n kind: \"background\";\n jobId: string;\n } | {\n kind: \"foreground\";\n exitCode: number | null;\n signal: string | null;\n timedOut: boolean;\n aborted: boolean;\n timeoutMs: number;\n stdout: {\n text: string;\n truncated: boolean;\n spillPath?: string;\n };\n stderr: {\n text: string;\n truncated: boolean;\n spillPath?: string;\n };\n sandbox?: {\n mode: string;\n denied: boolean;\n enforcement?: string;\n runnerFailed?: boolean;\n };\n };\n cordis_define: {\n pluginId: string;\n packageId: string;\n name: string;\n purpose: string;\n hasHostHalf: boolean;\n hasClientHalf: boolean;\n };\n cordis_inspect_list: JsonValue;\n cordis_inspect_query: JsonValue;\n cordis_inspect_self: JsonValue;\n cordis_run: JsonValue;\n cordis_stop: {\n pluginId: string;\n };\n cordis_undefine: {\n pluginId: string;\n wasRunning: boolean;\n };\n edit: {\n path: string;\n before: string;\n after: string;\n };\n interrupt_agent: {\n accepted: boolean;\n };\n job_kill: {\n outcome: \"cancellation-requested\" | \"already-finished\";\n job: {\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n };\n };\n job_list: ({\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n })[];\n job_output: {\n text: string;\n job: {\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n };\n };\n ralph: {\n runId: string;\n agentsStarted: number;\n result: JsonValue;\n };\n read: {\n path: string;\n offset: number;\n lines: {\n number: number;\n text: string;\n }[];\n totalLines: number;\n };\n send_message: {\n messageId: string;\n };\n skill: {\n name: string;\n provider: string;\n resourceBase?: {\n kind: \"directory\";\n path: string;\n } | {\n kind: \"url\";\n url: string;\n } | {\n kind: \"opaque\";\n description: string;\n };\n content: string;\n };\n subagent: {\n kind: \"background\";\n jobId: string;\n } | {\n kind: \"continuable\";\n subagentId: string;\n } | {\n kind: \"foreground\";\n runId: string;\n output: JsonValue[];\n };\n subagent_fork: {\n kind: \"background\";\n jobId: string;\n } | {\n kind: \"continuable\";\n subagentId: string;\n } | {\n kind: \"foreground\";\n runId: string;\n output: JsonValue[];\n };\n todo_write: {\n todos: ({\n content: string;\n status: \"pending\" | \"in_progress\" | \"completed\";\n })[];\n counts: {\n pending: number;\n inProgress: number;\n completed: number;\n };\n };\n workflow: {\n runId: string;\n agentsStarted: number;\n result: JsonValue;\n };\n write: {\n path: string;\n operation: \"create\" | \"update\";\n before: string | null;\n after: string;\n };\n}\n\ntype ToolName = keyof ToolOutputMap\n\ndeclare class ToolCallError extends Error {\n readonly name: \"ToolCallError\";\n readonly toolName: ToolName;\n}\n\ndeclare const tools: {\n [K in ToolName]: (args: ToolArgsMap[K]) => Promise;\n}\n```","tools":[{"name":"bash","description":"Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a job id immediately; read its output with `job_output` and stop it with `job_kill`.","parameters":{"type":"object","properties":{"command":{"type":"string","description":"The bash command to execute."},"description":{"type":"string","description":"Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."},"timeoutMs":{"type":"number","description":"Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."},"workdir":{"type":"string","description":"Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."},"run_in_background":{"type":"boolean","description":"Run in the background and return a job id immediately (collect with job_output, stop with job_kill). No timeout applies."}},"required":["command","description"]}},{"name":"cordis_define","description":"Define an immutable Cordis Package. For a new Plugin, use kind:\"new\" and provide only a semantic prefix of 3–6 lowercase English letters; the Host returns the final pluginId and packageId. To modify an existing Plugin, use kind:\"existing\" with its exact pluginId to append a Package without overwriting older versions. Provide at least one of code.host and code.client. Each value is a plain JavaScript function body that returns a Cordis Plugin; no TypeScript, JSX, or import transformation occurs. Query Inspect before depending on a Service, Event, Builtin, Slot, or token. Define only validates parameters and syntax and records source: it does not request approval, execute apply, or change currentPackageId. On success, call cordis_run with the returned IDs.","parameters":{"type":"object","properties":{"plugin":{"oneOf":[{"type":"object","additionalProperties":false,"properties":{"kind":{"type":"string","const":"new"},"idPrefix":{"type":"string","description":"Suggested semantic prefix of 3–6 lowercase English letters; the Host adds a unique numeric suffix."}},"required":["kind","idPrefix"]},{"type":"object","additionalProperties":false,"properties":{"kind":{"type":"string","const":"existing"},"pluginId":{"type":"string","description":"Exact ID of an existing Plugin; the new Package is appended to that instance."}},"required":["kind","pluginId"]}]},"name":{"type":"string","description":"Short, readable Package name."},"purpose":{"type":"string","description":"One-sentence, user-facing description of the Package purpose."},"code":{"type":"object","additionalProperties":false,"properties":{"host":{"type":"string","description":"Plain JavaScript function body that returns the Host-half Cordis Plugin."},"client":{"type":"string","description":"Plain JavaScript function body that returns the browser Client-half Cordis Plugin."}}}},"required":["plugin","name","purpose","code"]}},{"name":"cordis_inspect_list","description":"List every Cordis Inspect Provider currently known to the Host, including local Host Providers and the latest manifests synchronized from the Client. Each entry includes its platform, purpose, read-only methods, and input/output schemas. Call this Tool before creating or modifying a Package, then select the provider and method for cordis_inspect_query from its result. Do not guess names or treat an Inspect method as a business Service that Plugin code can call.","parameters":{"type":"object","properties":{}}},{"name":"cordis_inspect_query","description":"Run a read-only query explicitly declared by an Inspect Provider. platform, provider, and method must come from cordis_inspect_list, and input must satisfy that method's schema. Use this Tool before cordis_define to read exact Service methods, Event modes, Builtin signatures, Tool schemas, theme tokens, or live Slot trees and props. Host queries run locally. A Client query waits for the first valid page response and remains pending until a page answers or the Tool is cancelled. This Tool cannot invoke business Service methods or modify the runtime. For Service.listService and Event.listEvents, query without input to navigate the compact signature directory, then query the exact service or event for its structured contract and referenced types. For Slots.listSubTree, query without root to navigate the compact tree, then query the exact root for its complete registration contract and props.","parameters":{"type":"object","properties":{"platform":{"type":"string","description":"Runtime platform that owns the Provider.","enum":["host","client"]},"provider":{"type":"string","description":"Exact Provider ID returned by cordis_inspect_list."},"method":{"type":"string","description":"Exact method name declared by the Provider manifest."},"input":{"description":"Optional query input; it must satisfy the method input schema."}},"required":["platform","provider","method"]}},{"name":"cordis_inspect_self","description":"Inspect dynamic Cordis objects owned by the current Session at increasing levels of detail. With no IDs, list only Plugin summaries. With pluginId alone, return version pointers, the latest Run, and every Package summary. Only pluginId plus packageId returns that immutable Package's Host/Client source and runtime diagnostics. packageId cannot be supplied alone. Query an exact Package before handling @pluginId, repairing an asynchronous failure, or defining an updated version. This Tool is read-only: it neither executes code nor changes version pointers.","parameters":{"type":"object","properties":{"pluginId":{"type":"string","description":"Stable Plugin ID returned by cordis_define or injected by @pluginId; omit it to list every current Plugin."},"packageId":{"type":"string","description":"Exact immutable Package ID owned by pluginId; when specified, source and diagnostics are returned."}}}},{"name":"cordis_run","description":"Activate one exact Package of a dynamic Plugin. Use mode:\"run\" for the first activation, restarting currentPackageId, or rollback. When current exists, use mode:\"update\" to switch to a different Package, even if the Plugin is currently stopped. An unauthorized Client Package creates an approval request and returns awaiting-approval; an authorized Package returns starting and continues asynchronously in the browser. Neither result waits for the final outcome inside the Tool. currentPackageId changes only after complete success; on failure, the old current and target next remain. Asynchronous success, rejection, or technical failure is reported through state and steering. After a technical failure, read diagnostics with cordis_inspect_self, correct the same Plugin, and retry autonomously. Do not request approval again after the user rejects it.","parameters":{"type":"object","properties":{"pluginId":{"type":"string","description":"Stable Plugin ID returned by cordis_define."},"packageId":{"type":"string","description":"Exact immutable Package ID to activate under that Plugin."},"mode":{"type":"string","description":"Use run for the first activation, restarting current, or rollback; use update to switch from current to a different Package.","enum":["run","update"]}},"required":["pluginId","packageId","mode"]}},{"name":"cordis_stop","description":"Stop the current Run of a dynamic Plugin and cancel unfinished approval or activation requests. Retain the Plugin, every immutable Package, grants, currentPackageId, and nextPackageId so it can later run or update directly. Stopping an already stopped Plugin succeeds idempotently. Use this Tool to disable effects temporarily; use cordis_undefine for permanent removal.","parameters":{"type":"object","properties":{"pluginId":{"type":"string","description":"Stable dynamic Plugin ID to stop."}},"required":["pluginId"]}},{"name":"cordis_undefine","description":"Permanently remove a dynamic Plugin owned by the current Session. If it is running or awaiting approval, first stop it and cancel the request, then delete every Package, grant, and version pointer. After this returns, its pluginId, packageIds, @ reference, and Package business views are invalid; historical cards retain only a \"Plugin removed\" record. Do not call this Tool when versions must remain available for restart or rollback; use cordis_stop instead.","parameters":{"type":"object","properties":{"pluginId":{"type":"string","description":"Stable dynamic Plugin ID to remove permanently."}},"required":["pluginId"]}},{"name":"edit","description":"Edit an existing UTF-8 text file by replacing literal text.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to edit, resolved by the filesystem backend."},"old_string":{"type":"string","description":"Literal text to replace. Must match exactly."},"new_string":{"type":"string","description":"Literal replacement text. Use an empty string to delete the match."},"replace_all":{"type":"boolean","description":"Replace all matches. Defaults to false; when false, old_string must appear exactly once."}},"required":["file_path","old_string","new_string"]}},{"name":"interrupt_agent","description":"Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op.","parameters":{"type":"object","properties":{"agent_id":{"type":"string","description":"The agent id of the running agent to interrupt."}},"required":["agent_id"]}},{"name":"job_kill","description":"Request cancellation of a running background job by job id. Returns immediately; the job settles as killed once its work actually stops.","parameters":{"type":"object","properties":{"job_id":{"type":"string","description":"Job id returned by the tool that started the background work."},"reason":{"type":"string","description":"Optional short reason, recorded in the log and forwarded to the job."}},"required":["job_id"]}},{"name":"job_list","description":"List your background jobs (running and finished) with their ids, kinds, and statuses.","parameters":{"type":"object","properties":{}}},{"name":"job_output","description":"Read a background job. Stream jobs return only output since the previous read; final-output jobs return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.","parameters":{"type":"object","properties":{"job_id":{"type":"string","description":"Job id returned by the tool that started the background work."},"wait":{"type":"boolean","description":"Block until the job reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the job alive."},"timeout_ms":{"type":"number","description":"Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."}},"required":["job_id"]}},{"name":"ralph","description":"Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.","parameters":{"type":"object","properties":{"objective":{"type":"string","description":"The immutable completion objective for every fresh Ralph round."},"maxRounds":{"type":"number","description":"Optional positive safe-integer round cap, bounded by the deployment ceiling."}},"required":["objective"]}},{"name":"read","description":"Read a UTF-8 text file and return line-numbered content.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to read, resolved by the filesystem backend."},"offset":{"type":"number","description":"1-based first line to return. Defaults to 1."},"limit":{"type":"number","description":"Maximum number of lines to return. Defaults to 2000."}},"required":["file_path"]}},{"name":"run_code","description":"Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the BODY of an async function (erasable syntax only; top-level `await` and `return` work), and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it.","parameters":{"type":"object","properties":{"code":{"type":"string","description":"The program: the body of an async TypeScript function."},"description":{"type":"string","description":"Clear, concise description of what this program does in active voice, 5-10 words (shown in the UI). Examples: \"Count TODO markers across packages\"; \"Read failing test and its fixture\"; \"Rename config key in every cordis.yml\"."}},"required":["code","description"]}},{"name":"send_message","description":"Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered.","parameters":{"type":"object","properties":{"subagent_id":{"type":"string","description":"The subagent id returned when the background subagent was started."},"message":{"type":"string","description":"The message to deliver to the subagent."}},"required":["subagent_id","message"]}},{"name":"skill","description":"Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.","parameters":{"type":"object","properties":{"name":{"type":"string","description":"The exact skill name from the available skills list."}},"required":["name"]}},{"name":"subagent","description":"Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."},"run_in_background":{"type":"boolean","description":"Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it."}},"required":["description","prompt"]}},{"name":"subagent_fork","description":"Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."}},"required":["description","prompt"]}},{"name":"todo_write","description":"Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).","parameters":{"type":"object","properties":{"todos":{"type":"array","description":"The COMPLETE task list, replacing any previous list.","items":{"type":"object","additionalProperties":false,"properties":{"content":{"type":"string","description":"What the task is — a short imperative line."},"status":{"type":"string","description":"pending (not started) | in_progress (now) | completed (done).","enum":["pending","in_progress","completed"]}},"required":["content","status"]}}},"required":["todos"]}},{"name":"workflow","description":"Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.\n\nThe workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result.\n\nScript-body hooks:\n- `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly.\n- `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages.\n- `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`.\n- `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim.\n\nMisused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`.\n\nConstraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.","parameters":{"type":"object","properties":{"script":{"type":"string","description":"The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `)."},"meta":{"type":"object","description":"The workflow identity block (plain JSON — never code).","additionalProperties":true,"properties":{"name":{"type":"string","description":"Short kebab-case workflow name."},"description":{"type":"string","description":"One-line description of what the workflow does."},"whenToUse":{"type":"string","description":"Optional guidance on when this workflow applies."},"phases":{"type":"array","description":"Optional phase declarations matched by phase() calls.","items":{"type":"object","additionalProperties":true,"properties":{"title":{"type":"string","description":"The phase title phase() calls match by exact string."},"detail":{"type":"string","description":"Optional one-line description of the phase."},"provider":{"type":"string","description":"Optional provider override this phase is expected to use."},"model":{"type":"string","description":"Optional model override this phase is expected to use."}},"required":["title"]}}},"required":["name","description"]},"args":{"type":"object","description":"Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}).","additionalProperties":true}},"required":["script","meta"]}},{"name":"write","description":"Create or fully replace a UTF-8 text file.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to write, resolved by the filesystem backend."},"content":{"type":"string","description":"Full UTF-8 text content to write."}},"required":["file_path","content"]}}]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"You are an AI agent powered by DeepSeek Harness.\n\nYou are headless-agent, a coding assistant powered by the deepseek-v4-flash model. Your working directory is {{cwd}}.\n\nVerify your work by running the code or tests. Keep answers brief and factual.\n\n\nUse the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.\n\nUse the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-observation-policy requires it) and prefer edit for targeted changes.\n\nUse the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-observation-policy requires it), unless you just created or edited it in this session.\n\nCheck the [exit code: N] marker on every bash result; investigate failures before moving on.\n\nTrack every background job id you start. You are notified in-session when a job finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running job's work. Before giving a final answer, collect every still-relevant job with job_output (set wait: true only when you are genuinely blocked on it), and job_kill jobs that stopped mattering.\n\nUse the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls.\n\n# Dynamic Cordis Plugins\n\nDynamic Cordis plugins temporarily extend the current DSH process. A Plugin uses apply(ctx) to consume Services, listen to Events, provide Services, register model Tools, or register browser UI in Slots.\n\n- Plugin and Package definitions exist only in the current process. define itself does not modify repository source, configuration, or disk, and definitions do not survive a process restart.\n- The restricted execution environment prevents accidental misuse; it is not a security boundary for malicious code. Services obtained by dynamic code connect to the real runtime.\n\n## Make the user-facing plan clear first\n\n- Dynamic Cordis Plugins are one available implementation mechanism, not the default for every request. Consider whether one could help only when the user intends to design or create something, or when a temporary interface could materially aid the current work. The presence of these instructions or Tools, and discussion of Cordis itself, do not make a request a dynamic-Plugin task.\n- When Cordis is a plausible fit, infer the intended work target and lifetime from the request and conversation. Use it only when the outcome belongs to the current running harness and should be delivered as a temporary runtime extension. If that distinction is materially ambiguous, ask at most one concise question about the intended result or lifetime. Otherwise proceed with the matching workflow; do not require the user to know or choose Cordis as an implementation mechanism.\n- Once a dynamic Plugin is appropriate, decide whether the task creates a new Plugin or modifies the Plugin named by the user with @pluginId. Proceed directly when the goal is clear; do not ask for repeated confirmation.\n- Choose Host, Client, or both from the requested outcome. Do not propose a Client/browser UI when the task does not need visible page behavior, and do not avoid Client when the requested outcome is visual, interactive, or depends on page state. Host versus Client is an implementation choice; do not make the user choose it.\n- When a design direction or a potentially useful interface would materially affect the result, ask at most one concise outcome or creative-preference question and offer a few candidate directions. Otherwise proceed directly; do not conduct a multi-round interview or a complex questionnaire.\n- cordis_define only defines and presents code; it does not run it. After definition, explain the pluginId and packageId returned by the Host and whether the next step is a run or update.\n- cordis_run may require user approval. When it returns awaiting-approval, explain that the user must allow or reject it in the UI. Do not wait, retry, or claim that it is running.\n- When it returns starting, explain that the request has entered the asynchronous flow and the Client is still activating. starting does not mean success. Wait for the system to report the final result through steering context.\n- Do not request approval again after the user rejects it. After a technical failure, fix the same Plugin from its diagnostics; do not silently create a replacement Plugin.\n\n## Recommended workflow and Tools\n\nBefore creating, modifying, or repairing a Plugin, load the cordis-plugin-development Skill. The Skill provides requirement navigation, capability composition, complete examples, and troubleshooting. Treat Inspect Provider results as the source of truth for exact APIs.\n\n1. cordis_inspect_list: discover the current Host and Client Providers and their read-only query methods.\n2. cordis_inspect_query: use the returned platform, provider, method, and schema to query exact Service, Event, Builtin, Slot, Theme token, or Tool information.\n3. cordis_inspect_self: inspect the current Session's Plugins, Packages, version pointers, source, and diagnostics. Source is returned only when both pluginId and packageId are specified.\n4. cordis_define: create the first Package for a new Plugin or append an immutable Package to an existing Plugin. It defines code but does not run it.\n5. cordis_run: activate an exact Package. Use run for the first activation, restarting current, or rollback; use update to switch versions.\n6. cordis_stop: remove the current Run and pending approval request while retaining definitions, grants, and version pointers.\n7. cordis_undefine: permanently stop and delete a Plugin and all of its Packages. Use it only after confirming that the user no longer needs them.\n\n- Inspect and Catalog data only confirm capabilities, names, signatures, types, and registration protocols before code is written; they do not replace business APIs.\n- Query Service.listService and Event.listEvents without input to choose from their compact signature directories, then query the exact service or event before using it. Exact queries return the structured contract and only its referenced types.\n- At runtime, a Plugin must call real Services or listen to real Events. Do not cache, display, or depend on Inspect results as business data.\n\n## Identity, versions, and approval\n\n- pluginId identifies a Plugin that can be modified over time. For a new Plugin, submit only a semantic idPrefix of 3–6 lowercase English letters; the Host allocates the final ID.\n- packageId identifies one immutable Host/Client source version under a Plugin. To change code, define a new Package; never overwrite an old version.\n- pluginRunId identifies one activation attempt and connects its approval, Host/Client loading, private RPC, Run card, and errors.\n- currentPackageId is the most recent fully successful Package. Stopping, starting an update, or failing an update does not clear it.\n- nextPackageId is the target awaiting approval, being attempted, awaiting Client activation, or most recently failed.\n- A single check mark authorizes only the current Package; double check marks authorize future versions of the same Plugin. A grant remains in effect after a technical failure.\n- An update stops the old Run before starting the target Package. Failure does not automatically restart the old version; retry next with update or roll back to current with run.\n\nWhen the user enters @pluginId, the system injects identity, the default base Package, version pointers, and runtime status, but not source code:\n\n1. Call cordis_inspect_self(pluginId, packageId) to read the target source.\n2. Use cordis_define in existing mode to append a Package to the same Plugin.\n3. Call cordis_run in run or update mode according to the version relationship.\n\nNever silently create another Plugin for @pluginId. If the reference is unavailable because it was removed, belongs to another Session, or was lost on process restart, tell the user directly.\n\n## High-frequency errors that must be avoided\n\n### Services: ctx.get and inject\n\n- Read an optional Service with ctx.get('serviceName') by default and handle undefined.\n- Declare inject: ['serviceName'] on the returned Plugin object only when the Service is a hard dependency and the Plugin must enter waiting until Cordis reactivates it after the Service appears.\n- Read ctx.serviceName only after declaring that Service in inject. Never access an undeclared Service as a ctx property.\n\n```js\nreturn {\n inject: ['requiredService'],\n apply(ctx) {\n ctx.requiredService.someMethod()\n const optionalService = ctx.get('optionalService')\n if (optionalService !== undefined) optionalService.someMethod()\n },\n}\n```\n\n### Code: use plain JavaScript only\n\n- Host and Client code is not transformed by TypeScript, JSX, or a bundler.\n- Do not use TypeScript types, as, decorators, import, require, or JSX.\n- Client React code must use React.createElement(...); never write .\n- Do not assume that process, Buffer, window, document, fetch, native timers, or any other global is available. Query the corresponding platform's Builtins and Services first.\n\n### Data: do not serialize live data\n\n- Services, Events, Slots, Sessions, and their derived Cordis/DSH objects are internal live data, not ordinary JSON that can be dumped.\n- Do not apply JSON.stringify, structuredClone, recursive enumeration, full copying, or whole-object display to live data.\n- Read only the leaf fields required by the task, then construct the smallest owned data object without Host references.\n\n### Lifecycle: every side effect must be reversible\n\n- Services, Events, Tools, handlers, timers, Slots, styles, and theme overrides must all belong to the current Fiber.\n- Use ctx.effect(), ctx.on(), or official APIs that return a disposer so stop, update, or undefine removes every side effect.\n- The cordis-plugin-development Skill contains complete timer, Waterfall, Slot, theme, Tool, RPC, and React examples and troubleshooting guidance.\n\n## Host and Client\n\n- Host runs in the DSH Node.js process and is appropriate for files, networking, commands, Agent/Session access, Host Events, Services, model Tools, and JSON methods callable by the Client.\n- Client runs in the browser page and is appropriate for themes, layout, current page state, Tool cards, and Slot UI.\n- Host and Client communicate through Package-private JSON methods: Host uses harness.handle(method, handler), and Client uses host.call(method, args). The direction is Client→Host, and only lossless JSON may cross it.\n- Client UI must be registered in a queried Slot; apply() cannot directly return a React Element. Query Slots.listSubTree without root to choose from the compact purpose/topology tree, then query the exact root for its full registration contract and props before writing code.\n- See the Skill and Inspect Providers for Run-specific panels and exact Slot registration patterns.\n\n## Asynchronous results and recovery\n\n- Do not wait inside a Tool for approval or browser work that can happen only after the current turn ends.\n- Asynchronous success, rejection, and runtime errors update Run state and notify you through steering context.\n- After a technical failure, use cordis_inspect_self to read the exact Package source and its message/stack. Define a corrected Package under the same Plugin and retry autonomously.\n- Use the cordis-plugin-development Skill for other failure causes, repair procedures, and complete extension patterns.\n\nUse the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.\n\nUse subagent in the background by default. Start independent delegations together in one assistant message and continue useful work while they run. Set `run_in_background: false` only when your next action depends on that subagent's result. When a background run settles, the runtime sends you a notice containing its outcome and any final assistant message.\n\n## Writing code for run_code\n\n`run_code` takes two required arguments: `code` — the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped) — and `description`, a short summary of what the program does. Inside the program:\n\n- Call tools as `await tools.name(args)` — quoted access for exotic names: `tools[\"my-tool\"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.\n- A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue.\n- Independent read-only calls MAY overlap under `Promise.all` (safe calls run concurrently; mutating calls run alone, in submission order). Sequence dependent work with `await`.\n- Emit results with `return` and/or `console.log(...)`. Only what you print or return is program output. A successful tool result containing an image is attached after the run so you can inspect it on the next step; every other intermediate result stays out of the conversation, so extract just what you need.\n\nThe available tools:\n\n```ts\ntype JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }\n\ninterface ToolArgsMap {\n /** Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a job id immediately; read its output with `job_output` and stop it with `job_kill`. */\n bash: {\n /** The bash command to execute. */\n command: string;\n /** Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\". */\n description: string;\n /** Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry. */\n timeoutMs?: number;\n /** Working directory for this command. Defaults to the session workspace; a relative path is resolved against it. */\n workdir?: string;\n /** Run in the background and return a job id immediately (collect with job_output, stop with job_kill). No timeout applies. */\n run_in_background?: boolean;\n } & Record;\n /** Define an immutable Cordis Package. For a new Plugin, use kind:\"new\" and provide only a semantic prefix of 3–6 lowercase English letters; the Host returns the final pluginId and packageId. To modify an existing Plugin, use kind:\"existing\" with its exact pluginId to append a Package without overwriting older versions. Provide at least one of code.host and code.client. Each value is a plain JavaScript function body that returns a Cordis Plugin; no TypeScript, JSX, or import transformation occurs. Query Inspect before depending on a Service, Event, Builtin, Slot, or token. Define only validates parameters and syntax and records source: it does not request approval, execute apply, or change currentPackageId. On success, call cordis_run with the returned IDs. */\n cordis_define: {\n plugin: {\n kind: \"new\";\n /** Suggested semantic prefix of 3–6 lowercase English letters; the Host adds a unique numeric suffix. */\n idPrefix: string;\n } | {\n kind: \"existing\";\n /** Exact ID of an existing Plugin; the new Package is appended to that instance. */\n pluginId: string;\n };\n /** Short, readable Package name. */\n name: string;\n /** One-sentence, user-facing description of the Package purpose. */\n purpose: string;\n code: {\n /** Plain JavaScript function body that returns the Host-half Cordis Plugin. */\n host?: string;\n /** Plain JavaScript function body that returns the browser Client-half Cordis Plugin. */\n client?: string;\n };\n } & Record;\n /** List every Cordis Inspect Provider currently known to the Host, including local Host Providers and the latest manifests synchronized from the Client. Each entry includes its platform, purpose, read-only methods, and input/output schemas. Call this Tool before creating or modifying a Package, then select the provider and method for cordis_inspect_query from its result. Do not guess names or treat an Inspect method as a business Service that Plugin code can call. */\n cordis_inspect_list: Record;\n /** Run a read-only query explicitly declared by an Inspect Provider. platform, provider, and method must come from cordis_inspect_list, and input must satisfy that method's schema. Use this Tool before cordis_define to read exact Service methods, Event modes, Builtin signatures, Tool schemas, theme tokens, or live Slot trees and props. Host queries run locally. A Client query waits for the first valid page response and remains pending until a page answers or the Tool is cancelled. This Tool cannot invoke business Service methods or modify the runtime. For Service.listService and Event.listEvents, query without input to navigate the compact signature directory, then query the exact service or event for its structured contract and referenced types. For Slots.listSubTree, query without root to navigate the compact tree, then query the exact root for its complete registration contract and props. */\n cordis_inspect_query: {\n /** Runtime platform that owns the Provider. */\n platform: \"host\" | \"client\";\n /** Exact Provider ID returned by cordis_inspect_list. */\n provider: string;\n /** Exact method name declared by the Provider manifest. */\n method: string;\n /** Optional query input; it must satisfy the method input schema. */\n input?: JsonValue;\n } & Record;\n /** Inspect dynamic Cordis objects owned by the current Session at increasing levels of detail. With no IDs, list only Plugin summaries. With pluginId alone, return version pointers, the latest Run, and every Package summary. Only pluginId plus packageId returns that immutable Package's Host/Client source and runtime diagnostics. packageId cannot be supplied alone. Query an exact Package before handling @pluginId, repairing an asynchronous failure, or defining an updated version. This Tool is read-only: it neither executes code nor changes version pointers. */\n cordis_inspect_self: {\n /** Stable Plugin ID returned by cordis_define or injected by @pluginId; omit it to list every current Plugin. */\n pluginId?: string;\n /** Exact immutable Package ID owned by pluginId; when specified, source and diagnostics are returned. */\n packageId?: string;\n } & Record;\n /** Activate one exact Package of a dynamic Plugin. Use mode:\"run\" for the first activation, restarting currentPackageId, or rollback. When current exists, use mode:\"update\" to switch to a different Package, even if the Plugin is currently stopped. An unauthorized Client Package creates an approval request and returns awaiting-approval; an authorized Package returns starting and continues asynchronously in the browser. Neither result waits for the final outcome inside the Tool. currentPackageId changes only after complete success; on failure, the old current and target next remain. Asynchronous success, rejection, or technical failure is reported through state and steering. After a technical failure, read diagnostics with cordis_inspect_self, correct the same Plugin, and retry autonomously. Do not request approval again after the user rejects it. */\n cordis_run: {\n /** Stable Plugin ID returned by cordis_define. */\n pluginId: string;\n /** Exact immutable Package ID to activate under that Plugin. */\n packageId: string;\n /** Use run for the first activation, restarting current, or rollback; use update to switch from current to a different Package. */\n mode: \"run\" | \"update\";\n } & Record;\n /** Stop the current Run of a dynamic Plugin and cancel unfinished approval or activation requests. Retain the Plugin, every immutable Package, grants, currentPackageId, and nextPackageId so it can later run or update directly. Stopping an already stopped Plugin succeeds idempotently. Use this Tool to disable effects temporarily; use cordis_undefine for permanent removal. */\n cordis_stop: {\n /** Stable dynamic Plugin ID to stop. */\n pluginId: string;\n } & Record;\n /** Permanently remove a dynamic Plugin owned by the current Session. If it is running or awaiting approval, first stop it and cancel the request, then delete every Package, grant, and version pointer. After this returns, its pluginId, packageIds, @ reference, and Package business views are invalid; historical cards retain only a \"Plugin removed\" record. Do not call this Tool when versions must remain available for restart or rollback; use cordis_stop instead. */\n cordis_undefine: {\n /** Stable dynamic Plugin ID to remove permanently. */\n pluginId: string;\n } & Record;\n /** Edit an existing UTF-8 text file by replacing literal text. */\n edit: {\n /** Path to edit, resolved by the filesystem backend. */\n file_path: string;\n /** Literal text to replace. Must match exactly. */\n old_string: string;\n /** Literal replacement text. Use an empty string to delete the match. */\n new_string: string;\n /** Replace all matches. Defaults to false; when false, old_string must appear exactly once. */\n replace_all?: boolean;\n } & Record;\n /** Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op. */\n interrupt_agent: {\n /** The agent id of the running agent to interrupt. */\n agent_id: string;\n } & Record;\n /** Request cancellation of a running background job by job id. Returns immediately; the job settles as killed once its work actually stops. */\n job_kill: {\n /** Job id returned by the tool that started the background work. */\n job_id: string;\n /** Optional short reason, recorded in the log and forwarded to the job. */\n reason?: string;\n } & Record;\n /** List your background jobs (running and finished) with their ids, kinds, and statuses. */\n job_list: Record;\n /** Read a background job. Stream jobs return only output since the previous read; final-output jobs return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap. */\n job_output: {\n /** Job id returned by the tool that started the background work. */\n job_id: string;\n /** Block until the job reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the job alive. */\n wait?: boolean;\n /** Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum. */\n timeout_ms?: number;\n } & Record;\n /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */\n list_subagent_models: {\n /** Registered LLM provider id. Omit to list providers. */\n provider?: string;\n /** Exact model id to inspect. Requires provider; omit to list that provider's advertised models. */\n model?: string;\n } & Record;\n /** Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools. */\n ralph: {\n /** The immutable completion objective for every fresh Ralph round. */\n objective: string;\n /** Optional positive safe-integer round cap, bounded by the deployment ceiling. */\n maxRounds?: number;\n } & Record;\n /** Read a UTF-8 text file and return line-numbered content. */\n read: {\n /** Path to read, resolved by the filesystem backend. */\n file_path: string;\n /** 1-based first line to return. Defaults to 1. */\n offset?: number;\n /** Maximum number of lines to return. Defaults to 2000. */\n limit?: number;\n } & Record;\n /** Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered. */\n send_message: {\n /** The subagent id returned when the background subagent was started. */\n subagent_id: string;\n /** The message to deliver to the subagent. */\n message: string;\n } & Record;\n /** Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill. */\n skill: {\n /** The exact skill name from the available skills list. */\n name: string;\n } & Record;\n /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */\n subagent: {\n /** A short (3-5 word) description of the delegated task, for display. */\n description: string;\n /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */\n prompt: string;\n /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */\n provider?: string;\n /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */\n model?: string;\n /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */\n reasoning_effort?: string;\n /** Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it. */\n run_in_background?: boolean;\n } & Record;\n /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result. */\n subagent_fork: {\n /** A short (3-5 word) description of the delegated task, for display. */\n description: string;\n /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */\n prompt: string;\n } & Record;\n /** Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished). */\n todo_write: {\n /** The COMPLETE task list, replacing any previous list. */\n todos: ({\n /** What the task is — a short imperative line. */\n content: string;\n /** pending (not started) | in_progress (now) | completed (done). */\n status: \"pending\" | \"in_progress\" | \"completed\";\n })[];\n } & Record;\n /** Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn. The workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result. Script-body hooks: - `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly. - `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages. - `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`. - `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim. Misused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`. Constraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes. */\n workflow: {\n /** The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `). */\n script: string;\n /** The workflow identity block (plain JSON — never code). */\n meta: {\n /** Short kebab-case workflow name. */\n name: string;\n /** One-line description of what the workflow does. */\n description: string;\n /** Optional guidance on when this workflow applies. */\n whenToUse?: string;\n /** Optional phase declarations matched by phase() calls. */\n phases?: ({\n /** The phase title phase() calls match by exact string. */\n title: string;\n /** Optional one-line description of the phase. */\n detail?: string;\n /** Optional provider override this phase is expected to use. */\n provider?: string;\n /** Optional model override this phase is expected to use. */\n model?: string;\n } & Record)[];\n } & Record;\n /** Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}). */\n args?: Record;\n } & Record;\n /** Create or fully replace a UTF-8 text file. */\n write: {\n /** Path to write, resolved by the filesystem backend. */\n file_path: string;\n /** Full UTF-8 text content to write. */\n content: string;\n } & Record;\n}\n\ninterface ToolOutputMap {\n bash: {\n kind: \"background\";\n jobId: string;\n } | {\n kind: \"foreground\";\n exitCode: number | null;\n signal: string | null;\n timedOut: boolean;\n aborted: boolean;\n timeoutMs: number;\n stdout: {\n text: string;\n truncated: boolean;\n spillPath?: string;\n };\n stderr: {\n text: string;\n truncated: boolean;\n spillPath?: string;\n };\n sandbox?: {\n mode: string;\n denied: boolean;\n enforcement?: string;\n runnerFailed?: boolean;\n };\n };\n cordis_define: {\n pluginId: string;\n packageId: string;\n name: string;\n purpose: string;\n hasHostHalf: boolean;\n hasClientHalf: boolean;\n };\n cordis_inspect_list: JsonValue;\n cordis_inspect_query: JsonValue;\n cordis_inspect_self: JsonValue;\n cordis_run: JsonValue;\n cordis_stop: {\n pluginId: string;\n };\n cordis_undefine: {\n pluginId: string;\n wasRunning: boolean;\n };\n edit: {\n path: string;\n before: string;\n after: string;\n };\n interrupt_agent: {\n accepted: boolean;\n };\n job_kill: {\n outcome: \"cancellation-requested\" | \"already-finished\";\n job: {\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n };\n };\n job_list: ({\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n })[];\n job_output: {\n text: string;\n job: {\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n };\n };\n list_subagent_models: string;\n ralph: {\n runId: string;\n agentsStarted: number;\n result: JsonValue;\n };\n read: {\n path: string;\n offset: number;\n lines: {\n number: number;\n text: string;\n }[];\n totalLines: number;\n };\n send_message: {\n messageId: string;\n };\n skill: {\n name: string;\n provider: string;\n resourceBase?: {\n kind: \"directory\";\n path: string;\n } | {\n kind: \"url\";\n url: string;\n } | {\n kind: \"opaque\";\n description: string;\n };\n content: string;\n };\n subagent: {\n kind: \"background\";\n jobId: string;\n } | {\n kind: \"continuable\";\n subagentId: string;\n } | {\n kind: \"foreground\";\n runId: string;\n output: JsonValue[];\n };\n subagent_fork: {\n kind: \"background\";\n jobId: string;\n } | {\n kind: \"continuable\";\n subagentId: string;\n } | {\n kind: \"foreground\";\n runId: string;\n output: JsonValue[];\n };\n todo_write: {\n todos: ({\n content: string;\n status: \"pending\" | \"in_progress\" | \"completed\";\n })[];\n counts: {\n pending: number;\n inProgress: number;\n completed: number;\n };\n };\n workflow: {\n runId: string;\n agentsStarted: number;\n result: JsonValue;\n };\n write: {\n path: string;\n operation: \"create\" | \"update\";\n before: string | null;\n after: string;\n };\n}\n\ntype ToolName = keyof ToolOutputMap\n\ndeclare class ToolCallError extends Error {\n readonly name: \"ToolCallError\";\n readonly toolName: ToolName;\n}\n\ndeclare const tools: {\n [K in ToolName]: (args: ToolArgsMap[K]) => Promise;\n}\n```","tools":[{"name":"bash","description":"Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a job id immediately; read its output with `job_output` and stop it with `job_kill`.","parameters":{"type":"object","properties":{"command":{"type":"string","description":"The bash command to execute."},"description":{"type":"string","description":"Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."},"timeoutMs":{"type":"number","description":"Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."},"workdir":{"type":"string","description":"Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."},"run_in_background":{"type":"boolean","description":"Run in the background and return a job id immediately (collect with job_output, stop with job_kill). No timeout applies."}},"required":["command","description"]}},{"name":"cordis_define","description":"Define an immutable Cordis Package. For a new Plugin, use kind:\"new\" and provide only a semantic prefix of 3–6 lowercase English letters; the Host returns the final pluginId and packageId. To modify an existing Plugin, use kind:\"existing\" with its exact pluginId to append a Package without overwriting older versions. Provide at least one of code.host and code.client. Each value is a plain JavaScript function body that returns a Cordis Plugin; no TypeScript, JSX, or import transformation occurs. Query Inspect before depending on a Service, Event, Builtin, Slot, or token. Define only validates parameters and syntax and records source: it does not request approval, execute apply, or change currentPackageId. On success, call cordis_run with the returned IDs.","parameters":{"type":"object","properties":{"plugin":{"oneOf":[{"type":"object","additionalProperties":false,"properties":{"kind":{"type":"string","const":"new"},"idPrefix":{"type":"string","description":"Suggested semantic prefix of 3–6 lowercase English letters; the Host adds a unique numeric suffix."}},"required":["kind","idPrefix"]},{"type":"object","additionalProperties":false,"properties":{"kind":{"type":"string","const":"existing"},"pluginId":{"type":"string","description":"Exact ID of an existing Plugin; the new Package is appended to that instance."}},"required":["kind","pluginId"]}]},"name":{"type":"string","description":"Short, readable Package name."},"purpose":{"type":"string","description":"One-sentence, user-facing description of the Package purpose."},"code":{"type":"object","additionalProperties":false,"properties":{"host":{"type":"string","description":"Plain JavaScript function body that returns the Host-half Cordis Plugin."},"client":{"type":"string","description":"Plain JavaScript function body that returns the browser Client-half Cordis Plugin."}}}},"required":["plugin","name","purpose","code"]}},{"name":"cordis_inspect_list","description":"List every Cordis Inspect Provider currently known to the Host, including local Host Providers and the latest manifests synchronized from the Client. Each entry includes its platform, purpose, read-only methods, and input/output schemas. Call this Tool before creating or modifying a Package, then select the provider and method for cordis_inspect_query from its result. Do not guess names or treat an Inspect method as a business Service that Plugin code can call.","parameters":{"type":"object","properties":{}}},{"name":"cordis_inspect_query","description":"Run a read-only query explicitly declared by an Inspect Provider. platform, provider, and method must come from cordis_inspect_list, and input must satisfy that method's schema. Use this Tool before cordis_define to read exact Service methods, Event modes, Builtin signatures, Tool schemas, theme tokens, or live Slot trees and props. Host queries run locally. A Client query waits for the first valid page response and remains pending until a page answers or the Tool is cancelled. This Tool cannot invoke business Service methods or modify the runtime. For Service.listService and Event.listEvents, query without input to navigate the compact signature directory, then query the exact service or event for its structured contract and referenced types. For Slots.listSubTree, query without root to navigate the compact tree, then query the exact root for its complete registration contract and props.","parameters":{"type":"object","properties":{"platform":{"type":"string","description":"Runtime platform that owns the Provider.","enum":["host","client"]},"provider":{"type":"string","description":"Exact Provider ID returned by cordis_inspect_list."},"method":{"type":"string","description":"Exact method name declared by the Provider manifest."},"input":{"description":"Optional query input; it must satisfy the method input schema."}},"required":["platform","provider","method"]}},{"name":"cordis_inspect_self","description":"Inspect dynamic Cordis objects owned by the current Session at increasing levels of detail. With no IDs, list only Plugin summaries. With pluginId alone, return version pointers, the latest Run, and every Package summary. Only pluginId plus packageId returns that immutable Package's Host/Client source and runtime diagnostics. packageId cannot be supplied alone. Query an exact Package before handling @pluginId, repairing an asynchronous failure, or defining an updated version. This Tool is read-only: it neither executes code nor changes version pointers.","parameters":{"type":"object","properties":{"pluginId":{"type":"string","description":"Stable Plugin ID returned by cordis_define or injected by @pluginId; omit it to list every current Plugin."},"packageId":{"type":"string","description":"Exact immutable Package ID owned by pluginId; when specified, source and diagnostics are returned."}}}},{"name":"cordis_run","description":"Activate one exact Package of a dynamic Plugin. Use mode:\"run\" for the first activation, restarting currentPackageId, or rollback. When current exists, use mode:\"update\" to switch to a different Package, even if the Plugin is currently stopped. An unauthorized Client Package creates an approval request and returns awaiting-approval; an authorized Package returns starting and continues asynchronously in the browser. Neither result waits for the final outcome inside the Tool. currentPackageId changes only after complete success; on failure, the old current and target next remain. Asynchronous success, rejection, or technical failure is reported through state and steering. After a technical failure, read diagnostics with cordis_inspect_self, correct the same Plugin, and retry autonomously. Do not request approval again after the user rejects it.","parameters":{"type":"object","properties":{"pluginId":{"type":"string","description":"Stable Plugin ID returned by cordis_define."},"packageId":{"type":"string","description":"Exact immutable Package ID to activate under that Plugin."},"mode":{"type":"string","description":"Use run for the first activation, restarting current, or rollback; use update to switch from current to a different Package.","enum":["run","update"]}},"required":["pluginId","packageId","mode"]}},{"name":"cordis_stop","description":"Stop the current Run of a dynamic Plugin and cancel unfinished approval or activation requests. Retain the Plugin, every immutable Package, grants, currentPackageId, and nextPackageId so it can later run or update directly. Stopping an already stopped Plugin succeeds idempotently. Use this Tool to disable effects temporarily; use cordis_undefine for permanent removal.","parameters":{"type":"object","properties":{"pluginId":{"type":"string","description":"Stable dynamic Plugin ID to stop."}},"required":["pluginId"]}},{"name":"cordis_undefine","description":"Permanently remove a dynamic Plugin owned by the current Session. If it is running or awaiting approval, first stop it and cancel the request, then delete every Package, grant, and version pointer. After this returns, its pluginId, packageIds, @ reference, and Package business views are invalid; historical cards retain only a \"Plugin removed\" record. Do not call this Tool when versions must remain available for restart or rollback; use cordis_stop instead.","parameters":{"type":"object","properties":{"pluginId":{"type":"string","description":"Stable dynamic Plugin ID to remove permanently."}},"required":["pluginId"]}},{"name":"edit","description":"Edit an existing UTF-8 text file by replacing literal text.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to edit, resolved by the filesystem backend."},"old_string":{"type":"string","description":"Literal text to replace. Must match exactly."},"new_string":{"type":"string","description":"Literal replacement text. Use an empty string to delete the match."},"replace_all":{"type":"boolean","description":"Replace all matches. Defaults to false; when false, old_string must appear exactly once."}},"required":["file_path","old_string","new_string"]}},{"name":"interrupt_agent","description":"Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op.","parameters":{"type":"object","properties":{"agent_id":{"type":"string","description":"The agent id of the running agent to interrupt."}},"required":["agent_id"]}},{"name":"job_kill","description":"Request cancellation of a running background job by job id. Returns immediately; the job settles as killed once its work actually stops.","parameters":{"type":"object","properties":{"job_id":{"type":"string","description":"Job id returned by the tool that started the background work."},"reason":{"type":"string","description":"Optional short reason, recorded in the log and forwarded to the job."}},"required":["job_id"]}},{"name":"job_list","description":"List your background jobs (running and finished) with their ids, kinds, and statuses.","parameters":{"type":"object","properties":{}}},{"name":"job_output","description":"Read a background job. Stream jobs return only output since the previous read; final-output jobs return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.","parameters":{"type":"object","properties":{"job_id":{"type":"string","description":"Job id returned by the tool that started the background work."},"wait":{"type":"boolean","description":"Block until the job reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the job alive."},"timeout_ms":{"type":"number","description":"Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."}},"required":["job_id"]}},{"name":"list_subagent_models","description":"Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.","parameters":{"type":"object","properties":{"provider":{"type":"string","description":"Registered LLM provider id. Omit to list providers."},"model":{"type":"string","description":"Exact model id to inspect. Requires provider; omit to list that provider's advertised models."}}}},{"name":"ralph","description":"Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.","parameters":{"type":"object","properties":{"objective":{"type":"string","description":"The immutable completion objective for every fresh Ralph round."},"maxRounds":{"type":"number","description":"Optional positive safe-integer round cap, bounded by the deployment ceiling."}},"required":["objective"]}},{"name":"read","description":"Read a UTF-8 text file and return line-numbered content.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to read, resolved by the filesystem backend."},"offset":{"type":"number","description":"1-based first line to return. Defaults to 1."},"limit":{"type":"number","description":"Maximum number of lines to return. Defaults to 2000."}},"required":["file_path"]}},{"name":"run_code","description":"Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the BODY of an async function (erasable syntax only; top-level `await` and `return` work), and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return is program output — curate it. Image-bearing subtool results are attached after the run.","parameters":{"type":"object","properties":{"code":{"type":"string","description":"The program: the body of an async TypeScript function."},"description":{"type":"string","description":"Clear, concise description of what this program does in active voice, 5-10 words (shown in the UI). Examples: \"Count TODO markers across packages\"; \"Read failing test and its fixture\"; \"Rename config key in every cordis.yml\"."}},"required":["code","description"]}},{"name":"send_message","description":"Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered.","parameters":{"type":"object","properties":{"subagent_id":{"type":"string","description":"The subagent id returned when the background subagent was started."},"message":{"type":"string","description":"The message to deliver to the subagent."}},"required":["subagent_id","message"]}},{"name":"skill","description":"Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.","parameters":{"type":"object","properties":{"name":{"type":"string","description":"The exact skill name from the available skills list."}},"required":["name"]}},{"name":"subagent","description":"Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."},"provider":{"type":"string","description":"LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route."},"model":{"type":"string","description":"Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route."},"reasoning_effort":{"type":"string","description":"Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default."},"run_in_background":{"type":"boolean","description":"Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it."}},"required":["description","prompt"]}},{"name":"subagent_fork","description":"Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."}},"required":["description","prompt"]}},{"name":"todo_write","description":"Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).","parameters":{"type":"object","properties":{"todos":{"type":"array","description":"The COMPLETE task list, replacing any previous list.","items":{"type":"object","additionalProperties":false,"properties":{"content":{"type":"string","description":"What the task is — a short imperative line."},"status":{"type":"string","description":"pending (not started) | in_progress (now) | completed (done).","enum":["pending","in_progress","completed"]}},"required":["content","status"]}}},"required":["todos"]}},{"name":"workflow","description":"Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.\n\nThe workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result.\n\nScript-body hooks:\n- `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly.\n- `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages.\n- `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`.\n- `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim.\n\nMisused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`.\n\nConstraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.","parameters":{"type":"object","properties":{"script":{"type":"string","description":"The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `)."},"meta":{"type":"object","description":"The workflow identity block (plain JSON — never code).","additionalProperties":true,"properties":{"name":{"type":"string","description":"Short kebab-case workflow name."},"description":{"type":"string","description":"One-line description of what the workflow does."},"whenToUse":{"type":"string","description":"Optional guidance on when this workflow applies."},"phases":{"type":"array","description":"Optional phase declarations matched by phase() calls.","items":{"type":"object","additionalProperties":true,"properties":{"title":{"type":"string","description":"The phase title phase() calls match by exact string."},"detail":{"type":"string","description":"Optional one-line description of the phase."},"provider":{"type":"string","description":"Optional provider override this phase is expected to use."},"model":{"type":"string","description":"Optional model override this phase is expected to use."}},"required":["title"]}}},"required":["name","description"]},"args":{"type":"object","description":"Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}).","additionalProperties":true}},"required":["script","meta"]}},{"name":"write","description":"Create or fully replace a UTF-8 text file.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to write, resolved by the filesystem backend."},"content":{"type":"string","description":"Full UTF-8 text content to write."}},"required":["file_path","content"]}}]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-define","name":"cordis_define","argumentsDelta":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"e0351488-7bca-48d6-b7af-87d533858f47"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[8,9,10,11,12],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"749df5cd-1017-43c0-8c74-b2cab4752e59"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[8,9,10,11,12],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"advanced-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"Snapshot Marker\",\"purpose\":\"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\"code\":{\"host\":\"return { apply() {} }\"}}"}} -{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-define"},"content":[{"type":"tool-result","toolCallId":"advanced-define","content":[{"type":"text","text":"Defined snap-1/pkg-1 (Snapshot Marker); it is not running yet. Use cordis_run to activate this Package."}],"isError":false}],"role":"user","id":"9fd9ba62-1b6d-4bb7-98c6-97815e983026"},"meta":{"pluginId":"snap-1","packageId":"pkg-1"}},"sourceEventSeqs":[14],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-define"},"content":[{"type":"tool-result","toolCallId":"advanced-define","content":[{"type":"text","text":"Defined snap-1/pkg-1 (Snapshot Marker); it is not running yet. Use cordis_run to activate this Package."}],"isError":false}],"role":"user","id":"57e5798f-59ce-43ee-90c2-dcf4a66de1f3"},"meta":{"pluginId":"snap-1","packageId":"pkg-1"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} @@ -22,13 +22,13 @@ {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"6f6c3cc3-350b-4afb-a3d1-8f91b9494628"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[18,19,20,21,22],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"a3df67b0-ba4f-4fc3-a359-05a63f636ba9"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[18,19,20,21,22],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"advanced-code","name":"run_code","arguments":"{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"}} {"type":"tool/code-dispatch-start","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"cordis_run","arguments":{"pluginId":"snap-1","packageId":"pkg-1","mode":"run"}}} {"type":"tool/code-dispatch","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"cordis_run","arguments":{"pluginId":"snap-1","packageId":"pkg-1","mode":"run"},"isError":false,"content":[{"type":"text","text":"snap-1/pkg-1 is running (run-1)."}]}} {"type":"tool/code-dispatch-start","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:2","name":"cordis_inspect_self","arguments":{"pluginId":"snap-1"}}} {"type":"tool/code-dispatch","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:2","name":"cordis_inspect_self","arguments":{"pluginId":"snap-1"},"isError":false,"content":[{"type":"text","text":"{\n \"mode\": \"plugin\",\n \"pluginId\": \"snap-1\",\n \"name\": \"Snapshot Marker\",\n \"packageCount\": 1,\n \"state\": \"running\",\n \"currentPackageId\": \"pkg-1\",\n \"activeRun\": {\n \"pluginRunId\": \"run-1\",\n \"packageId\": \"pkg-1\"\n },\n \"packages\": [\n {\n \"packageId\": \"pkg-1\",\n \"name\": \"Snapshot Marker\",\n \"purpose\": \"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\n \"hasHostHalf\": true,\n \"hasClientHalf\": false,\n \"isCurrent\": true,\n \"isNext\": false\n }\n ]\n}"}]}} -{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-code"},"content":[{"type":"tool-result","toolCallId":"advanced-code","content":[{"type":"text","text":"{\n \"run\": {\n \"status\": \"running\",\n \"pluginId\": \"snap-1\",\n \"packageId\": \"pkg-1\",\n \"pluginRunId\": \"run-1\",\n \"currentPackageId\": \"pkg-1\",\n \"host\": {\n \"status\": \"running\",\n \"provides\": [],\n \"waitingFor\": []\n },\n \"client\": {\n \"status\": \"absent\",\n \"waitingFor\": []\n }\n },\n \"inspected\": {\n \"mode\": \"plugin\",\n \"pluginId\": \"snap-1\",\n \"name\": \"Snapshot Marker\",\n \"packageCount\": 1,\n \"state\": \"running\",\n \"currentPackageId\": \"pkg-1\",\n \"activeRun\": {\n \"pluginRunId\": \"run-1\",\n \"packageId\": \"pkg-1\"\n },\n \"packages\": [\n {\n \"packageId\": \"pkg-1\",\n \"name\": \"Snapshot Marker\",\n \"purpose\": \"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\n \"hasHostHalf\": true,\n \"hasClientHalf\": false,\n \"isCurrent\": true,\n \"isNext\": false\n }\n ]\n }\n}"}],"isError":false}],"role":"user","id":"40a17cbf-a853-4813-bbb5-7970cfbc7010"}},"sourceEventSeqs":[24],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-code"},"content":[{"type":"tool-result","toolCallId":"advanced-code","content":[{"type":"text","text":"{\n \"run\": {\n \"status\": \"running\",\n \"pluginId\": \"snap-1\",\n \"packageId\": \"pkg-1\",\n \"pluginRunId\": \"run-1\",\n \"currentPackageId\": \"pkg-1\",\n \"host\": {\n \"status\": \"running\",\n \"provides\": [],\n \"waitingFor\": []\n },\n \"client\": {\n \"status\": \"absent\",\n \"waitingFor\": []\n }\n },\n \"inspected\": {\n \"mode\": \"plugin\",\n \"pluginId\": \"snap-1\",\n \"name\": \"Snapshot Marker\",\n \"packageCount\": 1,\n \"state\": \"running\",\n \"currentPackageId\": \"pkg-1\",\n \"activeRun\": {\n \"pluginRunId\": \"run-1\",\n \"packageId\": \"pkg-1\"\n },\n \"packages\": [\n {\n \"packageId\": \"pkg-1\",\n \"name\": \"Snapshot Marker\",\n \"purpose\": \"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\n \"hasHostHalf\": true,\n \"hasClientHalf\": false,\n \"isCurrent\": true,\n \"isNext\": false\n }\n ]\n }\n}"}],"isError":false}],"role":"user","id":"2e591904-ffdd-4b86-824e-a4a5667b9383"}},"sourceEventSeqs":[24],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} {"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} @@ -36,9 +36,9 @@ {"type":"assistant/chunk","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.\",\"run_in_background\":false}"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"8bdea090-4a9a-451f-bb21-459af50472fa"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[32,33,34,35,36],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"93019032-9a3e-4947-aaff-4790cc42ecbb"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[32,33,34,35,36],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":3,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}} -{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"c978c208-8ebd-4dd6-b997-606ca7de787e"}},"sourceEventSeqs":[38],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"3b86b42c-4989-4490-a1f4-471dc8984c00"}},"sourceEventSeqs":[38],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} {"type":"step/start","data":{"turn":1,"step":4}} {"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} @@ -46,13 +46,13 @@ {"type":"assistant/chunk","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-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"e55b2b2e-497c-45d2-8115-16f317ae573f"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[42,43,44,45,46],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"df7d2480-5bf2-4780-ab43-376d3f867b2a"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[42,43,44,45,46],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":4,"callId":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"}} {"type":"tool-workflow/run-start","data":{"runId":"cd3d2666-94a1-4285-804e-c99630bc7b51","name":"advanced-headless-snapshot"}} {"type":"tool-workflow/agent-start","data":{"runId":"cd3d2666-94a1-4285-804e-c99630bc7b51","seq":1,"label":"workflow-child","phase":"Delegate","childId":"33333333-3333-4333-8333-333333333333"}} {"type":"tool-workflow/agent-end","data":{"runId":"cd3d2666-94a1-4285-804e-c99630bc7b51","seq":1,"outcome":"completed"}} {"type":"tool-workflow/run-end","data":{"runId":"cd3d2666-94a1-4285-804e-c99630bc7b51","stopReason":"completed"}} -{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-headless-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"f85f58fc-8d7e-4c9f-a0fe-caff480a9fec"}},"sourceEventSeqs":[48],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-headless-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"9fd8c742-bba7-4a6a-9c91-ac5d19a77eff"}},"sourceEventSeqs":[48],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":4}} {"type":"step/start","data":{"turn":1,"step":5}} {"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} @@ -60,9 +60,9 @@ {"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"68bd993f-a0bd-4ea8-ad16-6b1a19e09bd3"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[56,57,58,59,60],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"ad845a3d-0053-4991-bccc-ba09bd640699"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[56,57,58,59,60],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":5,"callId":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}} -{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"2d90e3b5-2a4c-4408-a1a0-3d009786a07b"}},"sourceEventSeqs":[62],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"d21035a4-fcb1-46b5-af03-e9f9b7685444"}},"sourceEventSeqs":[62],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":5}} {"type":"step/start","data":{"turn":1,"step":6}} {"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} @@ -70,6 +70,6 @@ {"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_HEADLESS_OK"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_HEADLESS_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"f3823367-2e25-43d2-a129-70dc492b2a90"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[66,67,68,69,70],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_HEADLESS_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"32f4ee19-e038-4c1c-9fc9-4288be6ca08f"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[66,67,68,69,70],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":6}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/headless-agent/tests/snapshots/compaction-recovery/session.jsonl b/examples/headless-agent/tests/snapshots/compaction-recovery/session.jsonl index 575bbc5dfa..af15e860e3 100644 --- a/examples/headless-agent/tests/snapshots/compaction-recovery/session.jsonl +++ b/examples/headless-agent/tests/snapshots/compaction-recovery/session.jsonl @@ -1,32 +1,32 @@ {"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0} -{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Establish a durable compaction premise before continuing. Record every part of this historical evidence: the snapshot uses keyless replay; persistence uses JSONL; the assembled headless application loads its real Cordis composition; model-visible inputs remain logged; tool calls and results remain paired and ordered; context overflow retains the original failure while recovery is attempted; compaction opens with compaction/start and closes with compaction/end; a successful auxiliary summary records compaction/summary provenance; the replacement surface shadows only an older balanced range; the checkpoint remains smaller than the history it replaces; the newest tool result remains verbatim; the retried request sees that checkpoint; the final response proves the same turn continued; deterministic snapshot evidence stays separate from the live-provider smoke; and no external API key is needed. Emit one alpha marker through bash, then finish the task after any required recovery with the exact words COMPACTION RECOVERED."}],"source":{"kind":"user"},"role":"user","id":"10eb2388-2d40-4564-af27-e7a5419fc14e"}]}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Establish a durable compaction premise before continuing. Record every part of this historical evidence: the snapshot uses keyless replay; persistence uses JSONL; the assembled headless application loads its real Cordis composition; model-visible inputs remain logged; tool calls and results remain paired and ordered; context overflow retains the original failure while recovery is attempted; compaction opens with compaction/start and closes with compaction/end; a successful auxiliary summary records compaction/summary provenance; the replacement surface shadows only an older balanced range; the checkpoint remains smaller than the history it replaces; the newest tool result remains verbatim; the retried request sees that checkpoint; the final response proves the same turn continued; deterministic snapshot evidence stays separate from the live-provider smoke; and no external API key is needed. Emit one alpha marker through bash, then finish the task after any required recovery with the exact words COMPACTION RECOVERED."}],"source":{"kind":"user"},"role":"user","id":"b187bcb5-4465-48d2-8aa1-ad12060ca048"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":1,"step":1}} -{"type":"user/message","data":{"content":[{"type":"text","text":"Establish a durable compaction premise before continuing. Record every part of this historical evidence: the snapshot uses keyless replay; persistence uses JSONL; the assembled headless application loads its real Cordis composition; model-visible inputs remain logged; tool calls and results remain paired and ordered; context overflow retains the original failure while recovery is attempted; compaction opens with compaction/start and closes with compaction/end; a successful auxiliary summary records compaction/summary provenance; the replacement surface shadows only an older balanced range; the checkpoint remains smaller than the history it replaces; the newest tool result remains verbatim; the retried request sees that checkpoint; the final response proves the same turn continued; deterministic snapshot evidence stays separate from the live-provider smoke; and no external API key is needed. Emit one alpha marker through bash, then finish the task after any required recovery with the exact words COMPACTION RECOVERED."}],"source":{"kind":"user"},"role":"user","id":"10eb2388-2d40-4564-af27-e7a5419fc14e"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Establish a durable compaction premise before continuing. Record every part of this historical evidence: the snapshot uses keyless replay; persistence uses JSONL; the assembled headless application loads its real Cordis composition; model-visible inputs remain logged; tool calls and results remain paired and ordered; context overflow retains the original failure while recovery is attempted; compaction opens with compaction/start and closes with compaction/end; a successful auxiliary summary records compaction/summary provenance; the replacement surface shadows only an older balanced range; the checkpoint remains smaller than the history it replaces; the newest tool result remains verbatim; the retried request sees that checkpoint; the final response proves the same turn continued; deterministic snapshot evidence stays separate from the live-provider smoke; and no external API key is needed. Emit one alpha marker through bash, then finish the task after any required recovery with the exact words COMPACTION RECOVERED."}],"source":{"kind":"user"},"role":"user","id":"b187bcb5-4465-48d2-8aa1-ad12060ca048"},"surfaceOp":"append"} {"type":"session/title","data":{"title":"Establish a durable compaction premise","messageSeqs":[4],"source":{"kind":"fallback"}}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"You are an AI agent powered by DeepSeek Harness.\n\nYou are headless-agent, a coding assistant powered by the deepseek-v4-flash model.\n\nVerify your work by running the code or tests. Keep answers brief and\nfactual.\n\n\nUse the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.\n\nUse the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-observation-policy requires it) and prefer edit for targeted changes.\n\nUse the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-observation-policy requires it), unless you just created or edited it in this session.\n\nCheck the [exit code: N] marker on every bash result; investigate failures before moving on.\n\nTrack every background job id you start. You are notified in-session when a job finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running job's work. Before giving a final answer, collect every still-relevant job with job_output (set wait: true only when you are genuinely blocked on it), and job_kill jobs that stopped mattering.\n\nUse the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls.\n\nUse the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.\n\nUse subagent in the background by default. Start independent delegations together in one assistant message and continue useful work while they run. Set `run_in_background: false` only when your next action depends on that subagent's result. When a background run settles, the runtime sends you a notice containing its outcome and any final assistant message.","tools":[{"name":"bash","description":"Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a job id immediately; read its output with `job_output` and stop it with `job_kill`.","parameters":{"type":"object","properties":{"command":{"type":"string","description":"The bash command to execute."},"description":{"type":"string","description":"Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."},"timeoutMs":{"type":"number","description":"Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."},"workdir":{"type":"string","description":"Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."},"run_in_background":{"type":"boolean","description":"Run in the background and return a job id immediately (collect with job_output, stop with job_kill). No timeout applies."}},"required":["command","description"]}},{"name":"edit","description":"Edit an existing UTF-8 text file by replacing literal text.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to edit, resolved by the filesystem backend."},"old_string":{"type":"string","description":"Literal text to replace. Must match exactly."},"new_string":{"type":"string","description":"Literal replacement text. Use an empty string to delete the match."},"replace_all":{"type":"boolean","description":"Replace all matches. Defaults to false; when false, old_string must appear exactly once."}},"required":["file_path","old_string","new_string"]}},{"name":"interrupt_agent","description":"Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op.","parameters":{"type":"object","properties":{"agent_id":{"type":"string","description":"The agent id of the running agent to interrupt."}},"required":["agent_id"]}},{"name":"job_kill","description":"Request cancellation of a running background job by job id. Returns immediately; the job settles as killed once its work actually stops.","parameters":{"type":"object","properties":{"job_id":{"type":"string","description":"Job id returned by the tool that started the background work."},"reason":{"type":"string","description":"Optional short reason, recorded in the log and forwarded to the job."}},"required":["job_id"]}},{"name":"job_list","description":"List your background jobs (running and finished) with their ids, kinds, and statuses.","parameters":{"type":"object","properties":{}}},{"name":"job_output","description":"Read a background job. Stream jobs return only output since the previous read; final-output jobs return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.","parameters":{"type":"object","properties":{"job_id":{"type":"string","description":"Job id returned by the tool that started the background work."},"wait":{"type":"boolean","description":"Block until the job reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the job alive."},"timeout_ms":{"type":"number","description":"Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."}},"required":["job_id"]}},{"name":"ralph","description":"Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.","parameters":{"type":"object","properties":{"objective":{"type":"string","description":"The immutable completion objective for every fresh Ralph round."},"maxRounds":{"type":"number","description":"Optional positive safe-integer round cap, bounded by the deployment ceiling."}},"required":["objective"]}},{"name":"read","description":"Read a UTF-8 text file and return line-numbered content.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to read, resolved by the filesystem backend."},"offset":{"type":"number","description":"1-based first line to return. Defaults to 1."},"limit":{"type":"number","description":"Maximum number of lines to return. Defaults to 2000."}},"required":["file_path"]}},{"name":"send_message","description":"Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered.","parameters":{"type":"object","properties":{"subagent_id":{"type":"string","description":"The subagent id returned when the background subagent was started."},"message":{"type":"string","description":"The message to deliver to the subagent."}},"required":["subagent_id","message"]}},{"name":"skill","description":"Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.","parameters":{"type":"object","properties":{"name":{"type":"string","description":"The exact skill name from the available skills list."}},"required":["name"]}},{"name":"subagent","description":"Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."},"run_in_background":{"type":"boolean","description":"Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it."}},"required":["description","prompt"]}},{"name":"subagent_fork","description":"Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."}},"required":["description","prompt"]}},{"name":"todo_write","description":"Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).","parameters":{"type":"object","properties":{"todos":{"type":"array","description":"The COMPLETE task list, replacing any previous list.","items":{"type":"object","additionalProperties":false,"properties":{"content":{"type":"string","description":"What the task is — a short imperative line."},"status":{"type":"string","description":"pending (not started) | in_progress (now) | completed (done).","enum":["pending","in_progress","completed"]}},"required":["content","status"]}}},"required":["todos"]}},{"name":"workflow","description":"Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.\n\nThe workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result.\n\nScript-body hooks:\n- `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly.\n- `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages.\n- `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`.\n- `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim.\n\nMisused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`.\n\nConstraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.","parameters":{"type":"object","properties":{"script":{"type":"string","description":"The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `)."},"meta":{"type":"object","description":"The workflow identity block (plain JSON — never code).","additionalProperties":true,"properties":{"name":{"type":"string","description":"Short kebab-case workflow name."},"description":{"type":"string","description":"One-line description of what the workflow does."},"whenToUse":{"type":"string","description":"Optional guidance on when this workflow applies."},"phases":{"type":"array","description":"Optional phase declarations matched by phase() calls.","items":{"type":"object","additionalProperties":true,"properties":{"title":{"type":"string","description":"The phase title phase() calls match by exact string."},"detail":{"type":"string","description":"Optional one-line description of the phase."},"provider":{"type":"string","description":"Optional provider override this phase is expected to use."},"model":{"type":"string","description":"Optional model override this phase is expected to use."}},"required":["title"]}}},"required":["name","description"]},"args":{"type":"object","description":"Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}).","additionalProperties":true}},"required":["script","meta"]}},{"name":"write","description":"Create or fully replace a UTF-8 text file.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to write, resolved by the filesystem backend."},"content":{"type":"string","description":"Full UTF-8 text content to write."}},"required":["file_path","content"]}}]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"You are an AI agent powered by DeepSeek Harness.\n\nYou are headless-agent, a coding assistant powered by the deepseek-v4-flash model.\n\nVerify your work by running the code or tests. Keep answers brief and\nfactual.\n\n\nUse the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.\n\nUse the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-observation-policy requires it) and prefer edit for targeted changes.\n\nUse the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-observation-policy requires it), unless you just created or edited it in this session.\n\nCheck the [exit code: N] marker on every bash result; investigate failures before moving on.\n\nTrack every background job id you start. You are notified in-session when a job finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running job's work. Before giving a final answer, collect every still-relevant job with job_output (set wait: true only when you are genuinely blocked on it), and job_kill jobs that stopped mattering.\n\nUse the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls.\n\nUse the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.\n\nUse subagent in the background by default. Start independent delegations together in one assistant message and continue useful work while they run. Set `run_in_background: false` only when your next action depends on that subagent's result. When a background run settles, the runtime sends you a notice containing its outcome and any final assistant message.","tools":[{"name":"bash","description":"Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a job id immediately; read its output with `job_output` and stop it with `job_kill`.","parameters":{"type":"object","properties":{"command":{"type":"string","description":"The bash command to execute."},"description":{"type":"string","description":"Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."},"timeoutMs":{"type":"number","description":"Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."},"workdir":{"type":"string","description":"Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."},"run_in_background":{"type":"boolean","description":"Run in the background and return a job id immediately (collect with job_output, stop with job_kill). No timeout applies."}},"required":["command","description"]}},{"name":"edit","description":"Edit an existing UTF-8 text file by replacing literal text.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to edit, resolved by the filesystem backend."},"old_string":{"type":"string","description":"Literal text to replace. Must match exactly."},"new_string":{"type":"string","description":"Literal replacement text. Use an empty string to delete the match."},"replace_all":{"type":"boolean","description":"Replace all matches. Defaults to false; when false, old_string must appear exactly once."}},"required":["file_path","old_string","new_string"]}},{"name":"interrupt_agent","description":"Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op.","parameters":{"type":"object","properties":{"agent_id":{"type":"string","description":"The agent id of the running agent to interrupt."}},"required":["agent_id"]}},{"name":"job_kill","description":"Request cancellation of a running background job by job id. Returns immediately; the job settles as killed once its work actually stops.","parameters":{"type":"object","properties":{"job_id":{"type":"string","description":"Job id returned by the tool that started the background work."},"reason":{"type":"string","description":"Optional short reason, recorded in the log and forwarded to the job."}},"required":["job_id"]}},{"name":"job_list","description":"List your background jobs (running and finished) with their ids, kinds, and statuses.","parameters":{"type":"object","properties":{}}},{"name":"job_output","description":"Read a background job. Stream jobs return only output since the previous read; final-output jobs return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.","parameters":{"type":"object","properties":{"job_id":{"type":"string","description":"Job id returned by the tool that started the background work."},"wait":{"type":"boolean","description":"Block until the job reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the job alive."},"timeout_ms":{"type":"number","description":"Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."}},"required":["job_id"]}},{"name":"list_subagent_models","description":"Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.","parameters":{"type":"object","properties":{"provider":{"type":"string","description":"Registered LLM provider id. Omit to list providers."},"model":{"type":"string","description":"Exact model id to inspect. Requires provider; omit to list that provider's advertised models."}}}},{"name":"ralph","description":"Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.","parameters":{"type":"object","properties":{"objective":{"type":"string","description":"The immutable completion objective for every fresh Ralph round."},"maxRounds":{"type":"number","description":"Optional positive safe-integer round cap, bounded by the deployment ceiling."}},"required":["objective"]}},{"name":"read","description":"Read a UTF-8 text file and return line-numbered content.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to read, resolved by the filesystem backend."},"offset":{"type":"number","description":"1-based first line to return. Defaults to 1."},"limit":{"type":"number","description":"Maximum number of lines to return. Defaults to 2000."}},"required":["file_path"]}},{"name":"send_message","description":"Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered.","parameters":{"type":"object","properties":{"subagent_id":{"type":"string","description":"The subagent id returned when the background subagent was started."},"message":{"type":"string","description":"The message to deliver to the subagent."}},"required":["subagent_id","message"]}},{"name":"skill","description":"Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.","parameters":{"type":"object","properties":{"name":{"type":"string","description":"The exact skill name from the available skills list."}},"required":["name"]}},{"name":"subagent","description":"Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."},"provider":{"type":"string","description":"LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route."},"model":{"type":"string","description":"Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route."},"reasoning_effort":{"type":"string","description":"Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default."},"run_in_background":{"type":"boolean","description":"Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it."}},"required":["description","prompt"]}},{"name":"subagent_fork","description":"Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."}},"required":["description","prompt"]}},{"name":"todo_write","description":"Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).","parameters":{"type":"object","properties":{"todos":{"type":"array","description":"The COMPLETE task list, replacing any previous list.","items":{"type":"object","additionalProperties":false,"properties":{"content":{"type":"string","description":"What the task is — a short imperative line."},"status":{"type":"string","description":"pending (not started) | in_progress (now) | completed (done).","enum":["pending","in_progress","completed"]}},"required":["content","status"]}}},"required":["todos"]}},{"name":"workflow","description":"Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.\n\nThe workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result.\n\nScript-body hooks:\n- `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly.\n- `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages.\n- `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`.\n- `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim.\n\nMisused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`.\n\nConstraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.","parameters":{"type":"object","properties":{"script":{"type":"string","description":"The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `)."},"meta":{"type":"object","description":"The workflow identity block (plain JSON — never code).","additionalProperties":true,"properties":{"name":{"type":"string","description":"Short kebab-case workflow name."},"description":{"type":"string","description":"One-line description of what the workflow does."},"whenToUse":{"type":"string","description":"Optional guidance on when this workflow applies."},"phases":{"type":"array","description":"Optional phase declarations matched by phase() calls.","items":{"type":"object","additionalProperties":true,"properties":{"title":{"type":"string","description":"The phase title phase() calls match by exact string."},"detail":{"type":"string","description":"Optional one-line description of the phase."},"provider":{"type":"string","description":"Optional provider override this phase is expected to use."},"model":{"type":"string","description":"Optional model override this phase is expected to use."}},"required":["title"]}}},"required":["name","description"]},"args":{"type":"object","description":"Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}).","additionalProperties":true}},"required":["script","meta"]}},{"name":"write","description":"Create or fully replace a UTF-8 text file.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to write, resolved by the filesystem backend."},"content":{"type":"string","description":"Full UTF-8 text content to write."}},"required":["file_path","content"]}}]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":128000}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_compaction_marker","name":"bash","argumentsDelta":"{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_compaction_marker","name":"bash","arguments":"{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":24,"outputTokens":6}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_compaction_marker","name":"bash","arguments":"{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"a71b2cfd-c18f-4a1b-82f6-e89fb371a87e"},"usage":{"inputTokens":24,"outputTokens":6}},"sourceEventSeqs":[8,9,10,11,12],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_compaction_marker","name":"bash","arguments":"{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"f6d534ce-c24d-49ee-ac3c-d05257fa03fb"},"usage":{"inputTokens":24,"outputTokens":6}},"sourceEventSeqs":[8,9,10,11,12],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_compaction_marker","name":"bash","arguments":"{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"}} -{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_compaction_marker"},"content":[{"type":"tool-result","toolCallId":"call_compaction_marker","content":[{"type":"text","text":"alpha\n"}],"isError":false}],"role":"user","id":"c9e68608-2dff-44bc-a344-b01006272378"}},"sourceEventSeqs":[14],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_compaction_marker"},"content":[{"type":"tool-result","toolCallId":"call_compaction_marker","content":[{"type":"text","text":"alpha\n"}],"isError":false}],"role":"user","id":"86b7cf93-1014-4334-a4af-5862c82fd5fb"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"error","failure":{"message":"snapshot request exceeded the model context window","code":"CONTEXT_WINDOW_EXCEEDED"}}}}} {"type":"compaction/start","data":{"compactionId":"338e88fa-e78b-4d49-bd38-8f919e85f5e1","turn":1}} {"type":"compaction/summary","data":{"compactionId":"338e88fa-e78b-4d49-bd38-8f919e85f5e1","summary":[{"type":"text","text":"The request established a durable compaction premise."}],"rawOutput":[{"type":"text","text":"The request established a durable compaction premise."}],"llmStreamCall":true,"shadowedRange":{"start":4,"end":4},"shadowedSeqs":[4],"shadowedTokenCount":266,"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":32,"usage":{"inputTokens":20,"outputTokens":4}}} -{"type":"user/message","data":{"content":[{"type":"text","text":"This is an automatically generated checkpoint condensing an earlier span of the conversation to free up context. Treat the captured context as established background and build on it without restating it. Continue the task directly from the messages that follow, without acknowledging this checkpoint.\n\n"},{"type":"text","text":"The request established a durable compaction premise."},{"type":"text","text":""}],"source":{"kind":"plugin","plugin":"compact","compactionId":"338e88fa-e78b-4d49-bd38-8f919e85f5e1"},"role":"user","id":"3668b957-07a2-4cb7-96b1-98a23ac8cdb8"},"sourceEventSeqs":[19,20,4],"surfaceOp":{"op":"replace","start":4,"end":4}} +{"type":"user/message","data":{"content":[{"type":"text","text":"This is an automatically generated checkpoint condensing an earlier span of the conversation to free up context. Treat the captured context as established background and build on it without restating it. Continue the task directly from the messages that follow, without acknowledging this checkpoint.\n\n"},{"type":"text","text":"The request established a durable compaction premise."},{"type":"text","text":""}],"source":{"kind":"plugin","plugin":"compact","compactionId":"338e88fa-e78b-4d49-bd38-8f919e85f5e1"},"role":"user","id":"a4847ff0-6fa0-419b-8427-eade600c05ee"},"sourceEventSeqs":[19,20,4],"surfaceOp":{"op":"replace","start":4,"end":4}} {"type":"compaction/end","data":{"compactionId":"338e88fa-e78b-4d49-bd38-8f919e85f5e1","turn":1}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"COMPACTION RECOVERED"}}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"COMPACTION RECOVERED"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":20,"outputTokens":4}}}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"COMPACTION RECOVERED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"bcbfd4ff-60e5-4634-ae39-4de3708a8abc"},"usage":{"inputTokens":20,"outputTokens":4}},"sourceEventSeqs":[23,24,25,26,27],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"COMPACTION RECOVERED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"18a03939-b8df-491e-ac8a-a74938f2aa4c"},"usage":{"inputTokens":20,"outputTokens":4}},"sourceEventSeqs":[23,24,25,26,27],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/headless-agent/tests/snapshots/pty-tools/session.jsonl b/examples/headless-agent/tests/snapshots/pty-tools/session.jsonl index 6e5148b4de..a3b7e4dc17 100644 --- a/examples/headless-agent/tests/snapshots/pty-tools/session.jsonl +++ b/examples/headless-agent/tests/snapshots/pty-tools/session.jsonl @@ -1,21 +1,21 @@ {"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0} -{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Exercise the six PTY tools in order, including one missing-session signal error, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"d35cdacd-b5e6-4968-b7a3-5ec48f403ef7"}]}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Exercise the six PTY tools in order, including one missing-session signal error, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"41d289fe-fc8a-464a-b3e1-db2c0189cae9"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":1,"step":1}} -{"type":"user/message","data":{"content":[{"type":"text","text":"Exercise the six PTY tools in order, including one missing-session signal error, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"d35cdacd-b5e6-4968-b7a3-5ec48f403ef7"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."}]},"role":"user","id":"053af702-9950-4860-913a-3c7e45a54f9d"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Exercise the six PTY tools in order, including one missing-session signal error, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"41d289fe-fc8a-464a-b3e1-db2c0189cae9"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."}]},"role":"user","id":"21b5e1f2-ef74-44cf-820b-d2a0bf77a519"},"surfaceOp":"append"} {"type":"session/title","data":{"title":"Exercise the six PTY tools","messageSeqs":[4],"source":{"kind":"fallback"}}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"You are an AI agent powered by DeepSeek Harness.\n\nYou are headless-agent, a coding assistant powered by the deepseek-v4-flash model.\n\nVerify your work by running the code or tests. Keep answers brief and\nfactual.\n\n\nUse the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.\n\nUse the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-observation-policy requires it) and prefer edit for targeted changes.\n\nUse the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-observation-policy requires it), unless you just created or edited it in this session.\n\nCheck the [exit code: N] marker on every bash result; investigate failures before moving on.\n\nUse a terminal session only when work needs persistent terminal state or interactive stdin; prefer shell/read/write/edit for bounded one-shot operations. Track every terminal session id and close sessions that no longer matter. An inferred_idle or timeout result does not prove the foreground command exited.\n\nTrack every background job id you start. You are notified in-session when a job finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running job's work. Before giving a final answer, collect every still-relevant job with job_output (set wait: true only when you are genuinely blocked on it), and job_kill jobs that stopped mattering.\n\nUse the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls.\n\nUse the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.\n\nUse subagent in the background by default. Start independent delegations together in one assistant message and continue useful work while they run. Set `run_in_background: false` only when your next action depends on that subagent's result. When a background run settles, the runtime sends you a notice containing its outcome and any final assistant message.","tools":[{"name":"bash","description":"Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a job id immediately; read its output with `job_output` and stop it with `job_kill`.","parameters":{"type":"object","properties":{"command":{"type":"string","description":"The bash command to execute."},"description":{"type":"string","description":"Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."},"timeoutMs":{"type":"number","description":"Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."},"workdir":{"type":"string","description":"Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."},"run_in_background":{"type":"boolean","description":"Run in the background and return a job id immediately (collect with job_output, stop with job_kill). No timeout applies."}},"required":["command","description"]}},{"name":"edit","description":"Edit an existing UTF-8 text file by replacing literal text.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to edit, resolved by the filesystem backend."},"old_string":{"type":"string","description":"Literal text to replace. Must match exactly."},"new_string":{"type":"string","description":"Literal replacement text. Use an empty string to delete the match."},"replace_all":{"type":"boolean","description":"Replace all matches. Defaults to false; when false, old_string must appear exactly once."}},"required":["file_path","old_string","new_string"]}},{"name":"interrupt_agent","description":"Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op.","parameters":{"type":"object","properties":{"agent_id":{"type":"string","description":"The agent id of the running agent to interrupt."}},"required":["agent_id"]}},{"name":"job_kill","description":"Request cancellation of a running background job by job id. Returns immediately; the job settles as killed once its work actually stops.","parameters":{"type":"object","properties":{"job_id":{"type":"string","description":"Job id returned by the tool that started the background work."},"reason":{"type":"string","description":"Optional short reason, recorded in the log and forwarded to the job."}},"required":["job_id"]}},{"name":"job_list","description":"List your background jobs (running and finished) with their ids, kinds, and statuses.","parameters":{"type":"object","properties":{}}},{"name":"job_output","description":"Read a background job. Stream jobs return only output since the previous read; final-output jobs return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.","parameters":{"type":"object","properties":{"job_id":{"type":"string","description":"Job id returned by the tool that started the background work."},"wait":{"type":"boolean","description":"Block until the job reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the job alive."},"timeout_ms":{"type":"number","description":"Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."}},"required":["job_id"]}},{"name":"ralph","description":"Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.","parameters":{"type":"object","properties":{"objective":{"type":"string","description":"The immutable completion objective for every fresh Ralph round."},"maxRounds":{"type":"number","description":"Optional positive safe-integer round cap, bounded by the deployment ceiling."}},"required":["objective"]}},{"name":"read","description":"Read a UTF-8 text file and return line-numbered content.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to read, resolved by the filesystem backend."},"offset":{"type":"number","description":"1-based first line to return. Defaults to 1."},"limit":{"type":"number","description":"Maximum number of lines to return. Defaults to 2000."}},"required":["file_path"]}},{"name":"send_message","description":"Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered.","parameters":{"type":"object","properties":{"subagent_id":{"type":"string","description":"The subagent id returned when the background subagent was started."},"message":{"type":"string","description":"The message to deliver to the subagent."}},"required":["subagent_id","message"]}},{"name":"skill","description":"Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.","parameters":{"type":"object","properties":{"name":{"type":"string","description":"The exact skill name from the available skills list."}},"required":["name"]}},{"name":"subagent","description":"Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."},"run_in_background":{"type":"boolean","description":"Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it."}},"required":["description","prompt"]}},{"name":"subagent_fork","description":"Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."}},"required":["description","prompt"]}},{"name":"terminal_close","description":"Close one persistent terminal and wait until its captured owned process tree is gone.","parameters":{"type":"object","properties":{"sessionId":{"type":"string","description":"Terminal session id."}},"required":["sessionId"]}},{"name":"terminal_list","description":"List persistent terminal sessions owned by the current agent.","parameters":{"type":"object","properties":{}}},{"name":"terminal_open","description":"Create a persistent, owner-isolated terminal session from a registered backend type. Use this for shell or REPL state that must survive across tool calls.","parameters":{"type":"object","properties":{"type":{"type":"string","description":"Registered terminal backend type, usually \"shell\"."},"name":{"type":"string","description":"Optional owner-local display name such as \"main\" or \"gdb\"."},"cwd":{"type":"string","description":"Initial working directory. Defaults to the deployment workspace root."}},"required":["type"]}},{"name":"terminal_read","description":"Read a bounded page of retained output from a persistent terminal without sending input.","parameters":{"type":"object","properties":{"sessionId":{"type":"string","description":"Terminal session id."},"offset":{"type":"number","description":"Newest-relative line offset (default 0)."},"count":{"type":"number","description":"Requested line count (default 500; backend caps apply)."}},"required":["sessionId"]}},{"name":"terminal_send","description":"Send text to a persistent terminal. By default Enter is submitted and the call waits for a prompt, stdin wait, output silence, timeout, or session exit. Background mode returns a job id for job_output/job_kill.","parameters":{"type":"object","properties":{"sessionId":{"type":"string","description":"Terminal session id returned by terminal_open or terminal_list."},"text":{"type":"string","description":"UTF-8 text to write to the terminal."},"submit":{"type":"boolean","description":"Submit Enter after text (default true). Set false for control characters or incomplete REPL input."},"run_in_background":{"type":"boolean","description":"Return a job id immediately; collect with job_output or stop with job_kill."}},"required":["sessionId","text"]}},{"name":"terminal_signal","description":"Send an allowed signal to the current foreground process group of a persistent terminal.","parameters":{"type":"object","properties":{"sessionId":{"type":"string","description":"Terminal session id."},"signal":{"type":"string","description":"Signal to deliver. Shell-targeted SIGKILL is rejected; use terminal_close.","enum":["SIGINT","SIGTERM","SIGKILL","SIGTSTP","SIGHUP"]}},"required":["sessionId","signal"]}},{"name":"todo_write","description":"Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).","parameters":{"type":"object","properties":{"todos":{"type":"array","description":"The COMPLETE task list, replacing any previous list.","items":{"type":"object","additionalProperties":false,"properties":{"content":{"type":"string","description":"What the task is — a short imperative line."},"status":{"type":"string","description":"pending (not started) | in_progress (now) | completed (done).","enum":["pending","in_progress","completed"]}},"required":["content","status"]}}},"required":["todos"]}},{"name":"workflow","description":"Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.\n\nThe workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result.\n\nScript-body hooks:\n- `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly.\n- `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages.\n- `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`.\n- `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim.\n\nMisused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`.\n\nConstraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.","parameters":{"type":"object","properties":{"script":{"type":"string","description":"The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `)."},"meta":{"type":"object","description":"The workflow identity block (plain JSON — never code).","additionalProperties":true,"properties":{"name":{"type":"string","description":"Short kebab-case workflow name."},"description":{"type":"string","description":"One-line description of what the workflow does."},"whenToUse":{"type":"string","description":"Optional guidance on when this workflow applies."},"phases":{"type":"array","description":"Optional phase declarations matched by phase() calls.","items":{"type":"object","additionalProperties":true,"properties":{"title":{"type":"string","description":"The phase title phase() calls match by exact string."},"detail":{"type":"string","description":"Optional one-line description of the phase."},"provider":{"type":"string","description":"Optional provider override this phase is expected to use."},"model":{"type":"string","description":"Optional model override this phase is expected to use."}},"required":["title"]}}},"required":["name","description"]},"args":{"type":"object","description":"Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}).","additionalProperties":true}},"required":["script","meta"]}},{"name":"write","description":"Create or fully replace a UTF-8 text file.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to write, resolved by the filesystem backend."},"content":{"type":"string","description":"Full UTF-8 text content to write."}},"required":["file_path","content"]}}]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"You are an AI agent powered by DeepSeek Harness.\n\nYou are headless-agent, a coding assistant powered by the deepseek-v4-flash model.\n\nVerify your work by running the code or tests. Keep answers brief and\nfactual.\n\n\nUse the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.\n\nUse the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-observation-policy requires it) and prefer edit for targeted changes.\n\nUse the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-observation-policy requires it), unless you just created or edited it in this session.\n\nCheck the [exit code: N] marker on every bash result; investigate failures before moving on.\n\nUse a terminal session only when work needs persistent terminal state or interactive stdin; prefer shell/read/write/edit for bounded one-shot operations. Track every terminal session id and close sessions that no longer matter. An inferred_idle or timeout result does not prove the foreground command exited.\n\nTrack every background job id you start. You are notified in-session when a job finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running job's work. Before giving a final answer, collect every still-relevant job with job_output (set wait: true only when you are genuinely blocked on it), and job_kill jobs that stopped mattering.\n\nUse the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls.\n\nUse the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.\n\nUse subagent in the background by default. Start independent delegations together in one assistant message and continue useful work while they run. Set `run_in_background: false` only when your next action depends on that subagent's result. When a background run settles, the runtime sends you a notice containing its outcome and any final assistant message.","tools":[{"name":"bash","description":"Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a job id immediately; read its output with `job_output` and stop it with `job_kill`.","parameters":{"type":"object","properties":{"command":{"type":"string","description":"The bash command to execute."},"description":{"type":"string","description":"Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."},"timeoutMs":{"type":"number","description":"Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."},"workdir":{"type":"string","description":"Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."},"run_in_background":{"type":"boolean","description":"Run in the background and return a job id immediately (collect with job_output, stop with job_kill). No timeout applies."}},"required":["command","description"]}},{"name":"edit","description":"Edit an existing UTF-8 text file by replacing literal text.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to edit, resolved by the filesystem backend."},"old_string":{"type":"string","description":"Literal text to replace. Must match exactly."},"new_string":{"type":"string","description":"Literal replacement text. Use an empty string to delete the match."},"replace_all":{"type":"boolean","description":"Replace all matches. Defaults to false; when false, old_string must appear exactly once."}},"required":["file_path","old_string","new_string"]}},{"name":"interrupt_agent","description":"Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op.","parameters":{"type":"object","properties":{"agent_id":{"type":"string","description":"The agent id of the running agent to interrupt."}},"required":["agent_id"]}},{"name":"job_kill","description":"Request cancellation of a running background job by job id. Returns immediately; the job settles as killed once its work actually stops.","parameters":{"type":"object","properties":{"job_id":{"type":"string","description":"Job id returned by the tool that started the background work."},"reason":{"type":"string","description":"Optional short reason, recorded in the log and forwarded to the job."}},"required":["job_id"]}},{"name":"job_list","description":"List your background jobs (running and finished) with their ids, kinds, and statuses.","parameters":{"type":"object","properties":{}}},{"name":"job_output","description":"Read a background job. Stream jobs return only output since the previous read; final-output jobs return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.","parameters":{"type":"object","properties":{"job_id":{"type":"string","description":"Job id returned by the tool that started the background work."},"wait":{"type":"boolean","description":"Block until the job reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the job alive."},"timeout_ms":{"type":"number","description":"Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."}},"required":["job_id"]}},{"name":"list_subagent_models","description":"Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.","parameters":{"type":"object","properties":{"provider":{"type":"string","description":"Registered LLM provider id. Omit to list providers."},"model":{"type":"string","description":"Exact model id to inspect. Requires provider; omit to list that provider's advertised models."}}}},{"name":"ralph","description":"Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.","parameters":{"type":"object","properties":{"objective":{"type":"string","description":"The immutable completion objective for every fresh Ralph round."},"maxRounds":{"type":"number","description":"Optional positive safe-integer round cap, bounded by the deployment ceiling."}},"required":["objective"]}},{"name":"read","description":"Read a UTF-8 text file and return line-numbered content.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to read, resolved by the filesystem backend."},"offset":{"type":"number","description":"1-based first line to return. Defaults to 1."},"limit":{"type":"number","description":"Maximum number of lines to return. Defaults to 2000."}},"required":["file_path"]}},{"name":"send_message","description":"Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered.","parameters":{"type":"object","properties":{"subagent_id":{"type":"string","description":"The subagent id returned when the background subagent was started."},"message":{"type":"string","description":"The message to deliver to the subagent."}},"required":["subagent_id","message"]}},{"name":"skill","description":"Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.","parameters":{"type":"object","properties":{"name":{"type":"string","description":"The exact skill name from the available skills list."}},"required":["name"]}},{"name":"subagent","description":"Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."},"provider":{"type":"string","description":"LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route."},"model":{"type":"string","description":"Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route."},"reasoning_effort":{"type":"string","description":"Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default."},"run_in_background":{"type":"boolean","description":"Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it."}},"required":["description","prompt"]}},{"name":"subagent_fork","description":"Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."}},"required":["description","prompt"]}},{"name":"terminal_close","description":"Close one persistent terminal and wait until its captured owned process tree is gone.","parameters":{"type":"object","properties":{"sessionId":{"type":"string","description":"Terminal session id."}},"required":["sessionId"]}},{"name":"terminal_list","description":"List persistent terminal sessions owned by the current agent.","parameters":{"type":"object","properties":{}}},{"name":"terminal_open","description":"Create a persistent, owner-isolated terminal session from a registered backend type. Use this for shell or REPL state that must survive across tool calls.","parameters":{"type":"object","properties":{"type":{"type":"string","description":"Registered terminal backend type, usually \"shell\"."},"name":{"type":"string","description":"Optional owner-local display name such as \"main\" or \"gdb\"."},"cwd":{"type":"string","description":"Initial working directory. Defaults to the deployment workspace root."}},"required":["type"]}},{"name":"terminal_read","description":"Read a bounded page of retained output from a persistent terminal without sending input.","parameters":{"type":"object","properties":{"sessionId":{"type":"string","description":"Terminal session id."},"offset":{"type":"number","description":"Newest-relative line offset (default 0)."},"count":{"type":"number","description":"Requested line count (default 500; backend caps apply)."}},"required":["sessionId"]}},{"name":"terminal_send","description":"Send text to a persistent terminal. By default Enter is submitted and the call waits for a prompt, stdin wait, output silence, timeout, or session exit. Background mode returns a job id for job_output/job_kill.","parameters":{"type":"object","properties":{"sessionId":{"type":"string","description":"Terminal session id returned by terminal_open or terminal_list."},"text":{"type":"string","description":"UTF-8 text to write to the terminal."},"submit":{"type":"boolean","description":"Submit Enter after text (default true). Set false for control characters or incomplete REPL input."},"run_in_background":{"type":"boolean","description":"Return a job id immediately; collect with job_output or stop with job_kill."}},"required":["sessionId","text"]}},{"name":"terminal_signal","description":"Send an allowed signal to the current foreground process group of a persistent terminal.","parameters":{"type":"object","properties":{"sessionId":{"type":"string","description":"Terminal session id."},"signal":{"type":"string","description":"Signal to deliver. Shell-targeted SIGKILL is rejected; use terminal_close.","enum":["SIGINT","SIGTERM","SIGKILL","SIGTSTP","SIGHUP"]}},"required":["sessionId","signal"]}},{"name":"todo_write","description":"Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).","parameters":{"type":"object","properties":{"todos":{"type":"array","description":"The COMPLETE task list, replacing any previous list.","items":{"type":"object","additionalProperties":false,"properties":{"content":{"type":"string","description":"What the task is — a short imperative line."},"status":{"type":"string","description":"pending (not started) | in_progress (now) | completed (done).","enum":["pending","in_progress","completed"]}},"required":["content","status"]}}},"required":["todos"]}},{"name":"workflow","description":"Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.\n\nThe workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result.\n\nScript-body hooks:\n- `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly.\n- `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages.\n- `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`.\n- `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim.\n\nMisused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`.\n\nConstraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.","parameters":{"type":"object","properties":{"script":{"type":"string","description":"The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `)."},"meta":{"type":"object","description":"The workflow identity block (plain JSON — never code).","additionalProperties":true,"properties":{"name":{"type":"string","description":"Short kebab-case workflow name."},"description":{"type":"string","description":"One-line description of what the workflow does."},"whenToUse":{"type":"string","description":"Optional guidance on when this workflow applies."},"phases":{"type":"array","description":"Optional phase declarations matched by phase() calls.","items":{"type":"object","additionalProperties":true,"properties":{"title":{"type":"string","description":"The phase title phase() calls match by exact string."},"detail":{"type":"string","description":"Optional one-line description of the phase."},"provider":{"type":"string","description":"Optional provider override this phase is expected to use."},"model":{"type":"string","description":"Optional model override this phase is expected to use."}},"required":["title"]}}},"required":["name","description"]},"args":{"type":"object","description":"Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}).","additionalProperties":true}},"required":["script","meta"]}},{"name":"write","description":"Create or fully replace a UTF-8 text file.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to write, resolved by the filesystem backend."},"content":{"type":"string","description":"Full UTF-8 text content to write."}},"required":["file_path","content"]}}]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-spawn","name":"terminal_open","argumentsDelta":"{\"type\":\"shell\",\"name\":\"main\"}"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-spawn","name":"terminal_open","arguments":"{\"type\":\"shell\",\"name\":\"main\"}"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"pty-spawn","name":"terminal_open","arguments":"{\"type\":\"shell\",\"name\":\"main\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"911213f8-acce-47be-a4f2-9d72ef55d83a"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"pty-spawn","name":"terminal_open","arguments":"{\"type\":\"shell\",\"name\":\"main\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"36ae38c0-78e0-4302-9457-dd88fd8bde7c"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"pty-spawn","name":"terminal_open","arguments":"{\"type\":\"shell\",\"name\":\"main\"}"}} -{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"pty-spawn"},"content":[{"type":"tool-result","toolCallId":"pty-spawn","content":[{"type":"text","text":"started terminal session pty-1 (main) [type: shell]\ndsh> "}],"isError":false}],"role":"user","id":"2da21b47-7fb3-444c-99a6-2c21743731ee"}},"sourceEventSeqs":[15],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"pty-spawn"},"content":[{"type":"tool-result","toolCallId":"pty-spawn","content":[{"type":"text","text":"started terminal session pty-1 (main) [type: shell]\ndsh> "}],"isError":false}],"role":"user","id":"f6583921-7e08-49f6-b5bd-99f6a3ced4b2"}},"sourceEventSeqs":[15],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} @@ -23,9 +23,9 @@ {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-send","name":"terminal_send","arguments":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"pty-send","name":"terminal_send","arguments":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"cf12d7ee-322a-4057-97b0-98d828a96f1a"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"pty-send","name":"terminal_send","arguments":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"000f1cda-e29a-42e1-9c7c-6e3803aa9316"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"pty-send","name":"terminal_send","arguments":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}} -{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"pty-send"},"content":[{"type":"tool-result","toolCallId":"pty-send","content":[{"type":"text","text":"K\ndsh> \n[wait: stdin_read]\n[session: running]\n[output truncated]"}],"isError":false}],"role":"user","id":"d1ffb2dc-6a33-4c9e-aeb9-b87bf6da8617"},"meta":{"viewport":"printf 'PTY_OK\\n'\nPTY_OK\ndsh> ","waitReason":"stdin_read","sessionStatus":{"kind":"running"},"truncated":false}},"sourceEventSeqs":[25],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"pty-send"},"content":[{"type":"tool-result","toolCallId":"pty-send","content":[{"type":"text","text":"K\ndsh> \n[wait: stdin_read]\n[session: running]\n[output truncated]"}],"isError":false}],"role":"user","id":"68ac57a2-530b-4479-b278-78b8c10657d5"},"meta":{"viewport":"printf 'PTY_OK\\n'\nPTY_OK\ndsh> ","waitReason":"stdin_read","sessionStatus":{"kind":"running"},"truncated":false}},"sourceEventSeqs":[25],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} {"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} @@ -33,9 +33,9 @@ {"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-read","name":"terminal_read","arguments":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}} {"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"pty-read","name":"terminal_read","arguments":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"b9b99ae9-4685-4ee5-b951-fbe58f84c4e3"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[29,30,31,32,33],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"pty-read","name":"terminal_read","arguments":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"c67744f1-e874-4678-8ea4-45eb96d95692"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[29,30,31,32,33],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":3,"callId":"pty-read","name":"terminal_read","arguments":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}} -{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"pty-read"},"content":[{"type":"tool-result","toolCallId":"pty-read","content":[{"type":"text","text":"dsh> printf 'PTY_OK\\n'\nPTY_OK\ndsh> \n[lines: 0-3 of 3]"}],"isError":false}],"role":"user","id":"d0d7331d-0d54-457a-9f7b-beb22abd34e6"}},"sourceEventSeqs":[35],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"pty-read"},"content":[{"type":"tool-result","toolCallId":"pty-read","content":[{"type":"text","text":"dsh> printf 'PTY_OK\\n'\nPTY_OK\ndsh> \n[lines: 0-3 of 3]"}],"isError":false}],"role":"user","id":"e77a5157-0844-48d2-9a4d-4b242c5d8357"}},"sourceEventSeqs":[35],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} {"type":"step/start","data":{"turn":1,"step":4}} {"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} @@ -43,9 +43,9 @@ {"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-signal","name":"terminal_signal","arguments":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}} {"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"pty-signal","name":"terminal_signal","arguments":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"9bb8ec3d-6e6f-44a2-8957-8f2d855f4834"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[39,40,41,42,43],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"pty-signal","name":"terminal_signal","arguments":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"cc545337-43b3-4fc6-8a17-74da2a49086a"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[39,40,41,42,43],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":4,"callId":"pty-signal","name":"terminal_signal","arguments":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}} -{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"pty-signal"},"content":[{"type":"tool-result","toolCallId":"pty-signal","content":[{"type":"text","text":"Error: unknown PTY session pty-missing"}],"isError":true}],"role":"user","id":"16fc1ca2-0eca-42d7-85d0-31794424c260"}},"sourceEventSeqs":[45],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"pty-signal"},"content":[{"type":"tool-result","toolCallId":"pty-signal","content":[{"type":"text","text":"Error: unknown PTY session pty-missing"}],"isError":true}],"role":"user","id":"3cde95e4-fcc3-422d-ae60-cc041c2cb223"}},"sourceEventSeqs":[45],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":4}} {"type":"step/start","data":{"turn":1,"step":5}} {"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} @@ -53,9 +53,9 @@ {"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-kill","name":"terminal_close","arguments":"{\"sessionId\":\"pty-1\"}"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}} {"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"pty-kill","name":"terminal_close","arguments":"{\"sessionId\":\"pty-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"4ebdc957-0369-4bbb-a5a5-4d2ef8ac3493"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[49,50,51,52,53],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"pty-kill","name":"terminal_close","arguments":"{\"sessionId\":\"pty-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"830096ca-82a9-4f44-b798-7d7040c35172"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[49,50,51,52,53],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":5,"callId":"pty-kill","name":"terminal_close","arguments":"{\"sessionId\":\"pty-1\"}"}} -{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"pty-kill"},"content":[{"type":"tool-result","toolCallId":"pty-kill","content":[{"type":"text","text":"closed terminal session pty-1"}],"isError":false}],"role":"user","id":"55a3e2cc-dbc5-44bf-a824-e3bbc8568cd5"}},"sourceEventSeqs":[55],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"pty-kill"},"content":[{"type":"tool-result","toolCallId":"pty-kill","content":[{"type":"text","text":"closed terminal session pty-1"}],"isError":false}],"role":"user","id":"f43c7146-3cb9-4e91-96f8-aa77b26d09e2"}},"sourceEventSeqs":[55],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":5}} {"type":"step/start","data":{"turn":1,"step":6}} {"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} @@ -63,9 +63,9 @@ {"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-list","name":"terminal_list","arguments":"{}"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}} {"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"pty-list","name":"terminal_list","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"6f956d23-5437-4a75-93a9-3abacd378e07"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[59,60,61,62,63],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"pty-list","name":"terminal_list","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"3070bc05-8897-4f63-8c9d-257e6b518b67"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[59,60,61,62,63],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":6,"callId":"pty-list","name":"terminal_list","arguments":"{}"}} -{"type":"tool/result","data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"pty-list"},"content":[{"type":"tool-result","toolCallId":"pty-list","content":[{"type":"text","text":"(no terminal sessions)"}],"isError":false}],"role":"user","id":"be12d914-6fe1-4c1e-8262-65b2aa9c20e5"}},"sourceEventSeqs":[65],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"pty-list"},"content":[{"type":"tool-result","toolCallId":"pty-list","content":[{"type":"text","text":"(no terminal sessions)"}],"isError":false}],"role":"user","id":"bc65b492-2cef-4fe6-8476-910bdc21d4ed"}},"sourceEventSeqs":[65],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":6}} {"type":"step/start","data":{"turn":1,"step":7}} {"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} @@ -73,6 +73,6 @@ {"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"9cca9680-5795-47d8-8edc-f6d44bcaa1ef"},"usage":{"inputTokens":10,"outputTokens":3}},"sourceEventSeqs":[69,70,71,72,73],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"ce300b5f-bfbf-4d85-8801-dce12f260d36"},"usage":{"inputTokens":10,"outputTokens":3}},"sourceEventSeqs":[69,70,71,72,73],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":7}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/headless-agent/tests/snapshots/subagent-settlement/child.expected.jsonl b/examples/headless-agent/tests/snapshots/subagent-settlement/child.expected.jsonl index f07a82ecbc..d2ac2d31c6 100644 --- a/examples/headless-agent/tests/snapshots/subagent-settlement/child.expected.jsonl +++ b/examples/headless-agent/tests/snapshots/subagent-settlement/child.expected.jsonl @@ -1,5 +1,5 @@ {"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","parentSession":"{{sessionId}}","origin":"subagent","delegationDepth":1} -{"type":"subagent/descriptor","data":{"version":2,"mode":"continuable","provider":"spawn","label":"Return child result","agentProvider":"deepseek-official","agentModel":"deepseek-v4-flash"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"continuable","provider":"spawn","label":"Return child result","agentProvider":"deepseek-official","agentModel":"deepseek-v4-flash"}} {"type":"session/end-seed","data":{}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly CHILD_RESULT and nothing else. Do not call report."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}} {"type":"turn/start","data":{"turn":1}} diff --git a/examples/headless-agent/tests/subagent-inheritance-snapshots/parent-override/child.expected.jsonl b/examples/headless-agent/tests/subagent-inheritance-snapshots/parent-override/child.expected.jsonl index 013d23d9ee..286ae33dcc 100644 --- a/examples/headless-agent/tests/subagent-inheritance-snapshots/parent-override/child.expected.jsonl +++ b/examples/headless-agent/tests/subagent-inheritance-snapshots/parent-override/child.expected.jsonl @@ -3,12 +3,12 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} -{"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Delegated write probe"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn","label":"Delegated write probe"}} {"type":"step/start","data":{"turn":1,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"session/title","data":{"title":"Use the write tool exactly","messageSeqs":[6],"source":{"kind":"fallback"}}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","reasoningEffort":"low"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"child-write","name":"write","argumentsDelta":"{\"file_path\": \"inherited.txt\", \"content\": \"escaped\"}"}}} diff --git a/examples/headless-agent/tests/subagent-inheritance-snapshots/parent-override/parent.expected.jsonl b/examples/headless-agent/tests/subagent-inheritance-snapshots/parent-override/parent.expected.jsonl index 9efed81901..332cf1be07 100644 --- a/examples/headless-agent/tests/subagent-inheritance-snapshots/parent-override/parent.expected.jsonl +++ b/examples/headless-agent/tests/subagent-inheritance-snapshots/parent-override/parent.expected.jsonl @@ -2,6 +2,7 @@ {"type":"turn/start","data":{"turn":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Tighten this session to read-only."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"sandbox/mode","data":{"mode":"read-only"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","reasoningEffort":"low"}},"reason":"initial"}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} {"type":"session/end-seed","data":{}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Delegate the write probe to a subagent."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}} @@ -11,16 +12,16 @@ {"type":"user/message","data":{"content":[{"type":"text","text":"Delegate the write probe to a subagent."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"session/title","data":{"title":"Tighten this session to read-only.","messageSeqs":[1],"source":{"kind":"fallback"}}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","reasoningEffort":"low"},"system":"{{system}}","tools":"{{tools}}"},"reason":"resume"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"assistant/chunk","data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"delegate-write","name":"subagent","argumentsDelta":"{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}}} {"type":"assistant/chunk","data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"delegate-write","name":"subagent","arguments":"{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}}}} {"type":"assistant/chunk","data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}} {"type":"assistant/chunk","data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"delegate-write","name":"subagent","arguments":"{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[14,15,16,17,18],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"delegate-write","name":"subagent","arguments":"{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[15,16,17,18,19],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":2,"step":1,"callId":"delegate-write","name":"subagent","arguments":"{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}} -{"type":"tool/result","data":{"turn":2,"step":1,"message":{"source":{"kind":"tool","callId":"delegate-write"},"content":[{"type":"tool-result","toolCallId":"delegate-write","content":[{"type":"text","text":"CHILD_DENIED [sandbox: file access denied under read-only mode]"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[20],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":2,"step":1,"message":{"source":{"kind":"tool","callId":"delegate-write"},"content":[{"type":"tool-result","toolCallId":"delegate-write","content":[{"type":"text","text":"CHILD_DENIED [sandbox: file access denied under read-only mode]"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[21],"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"step/start","data":{"turn":2,"step":2}} {"type":"assistant/chunk","data":{"turn":2,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} @@ -28,6 +29,6 @@ {"type":"assistant/chunk","data":{"turn":2,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"The delegated child was denied by the sandbox. PARENT_DONE"}}}} {"type":"assistant/chunk","data":{"turn":2,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}} {"type":"assistant/chunk","data":{"turn":2,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"The delegated child was denied by the sandbox. PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[24,25,26,27,28],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"The delegated child was denied by the sandbox. PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[25,26,27,28,29],"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":2}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}} diff --git a/examples/headless-agent/tests/subagent-inheritance.snapshot.ts b/examples/headless-agent/tests/subagent-inheritance.snapshot.ts index 8ca9855ed4..7b68ab4481 100644 --- a/examples/headless-agent/tests/subagent-inheritance.snapshot.ts +++ b/examples/headless-agent/tests/subagent-inheritance.snapshot.ts @@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url' import { Context } from '@deepseek-ai/cordis' import { normalizeSessionSnapshot, type NormalizeContext } from '@deepseek-ai/dsh-acp-snapshot' import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke' -import { createUserMessage } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ReasoningEffortId } from '@deepseek-ai/dsh-llm' import SessionStore, { SESSION_FORMAT_VERSION, SessionId, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session' import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' import { describe, expect, it } from 'vitest' @@ -26,7 +26,7 @@ const sessionId = SessionId('subagent-inheritance-parent') const refreshing = process.env.DSH_SNAPSHOT === 'refresh' const task = 'Delegate the write probe to a subagent.' -/** Seed a completed parent turn with the only read-only fact in the app. */ +/** Seed a completed parent turn with its read-only policy and current LLM selection. */ async function seedReadOnlyParent(root: string, cwd: string): Promise { const ctx = new Context() await ctx.plugin(SessionStore) @@ -42,7 +42,22 @@ async function seedReadOnlyParent(root: string, cwd: string): Promise { { type: 'turn/start', seq: 0, time: 10, data: { turn: 1 } }, { type: 'user/message', seq: 1, time: 11, data: createUserMessage({ content: [{ type: 'text', text: 'Tighten this session to read-only.' }], source: { kind: 'user' } }), surfaceOp: 'append' }, { type: 'sandbox/mode', seq: 2, time: 12, data: { mode: 'read-only' } }, - { type: 'turn/end', seq: 3, time: 13, data: { turn: 1, reason: { kind: 'completed' } } }, + { + type: 'request/header', + seq: 3, + time: 13, + data: { + header: { + config: { + provider: 'deepseek-official', + model: 'deepseek-v4-flash', + reasoningEffort: ReasoningEffortId('low'), + }, + }, + reason: 'initial', + }, + }, + { type: 'turn/end', seq: 4, time: 14, data: { turn: 1, reason: { kind: 'completed' } } }, ] try { await ctx.sessionPersistence.create(meta) diff --git a/examples/python-sdk-agent/README.i18n.yaml b/examples/python-sdk-agent/README.i18n.yaml index d217628a89..e57c422380 100644 --- a/examples/python-sdk-agent/README.i18n.yaml +++ b/examples/python-sdk-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/python-sdk-agent/README.md -README.md: 279aa5bcf0e988168cc936fbd6d96b69e74a5873 -README.zh.md: d843d8be719349b24e0369a2177748f2b09e40d6 +README.md: 46a8dc2384d96db39841c4c1e4cdc88d82ff55eb +README.zh.md: 5e6e2f89f27398dd7404894f15391a6a693d689b diff --git a/examples/python-sdk-agent/README.md b/examples/python-sdk-agent/README.md index 279aa5bcf0..46a8dc2384 100644 --- a/examples/python-sdk-agent/README.md +++ b/examples/python-sdk-agent/README.md @@ -2,39 +2,43 @@ English | [中文](README.zh.md) -The unattended coding-agent composition for the Python SDK's bundled JSON-RPC runtime. It intentionally loads no terminal UI, console logger, approval UI, or user-questions tool because stdout belongs to the SDK protocol and turns are driven by the SDK. +Runnable Python SDK example over the sole application launcher, `dsh --profile sdk-minimal`. The Python client owns JSON-RPC stdio; the profile owns the agent composition, persistence, execution policy, and plugins. -The model-facing tools are: +## Run the minimal agent -- `bash`, foreground only -- `read`, `write`, and `edit` -- `subagent`, using one foreground in-process spawn provider -- `todo_write` +Install `deepseek-harness-sdk`, export a model credential, then supply an isolated Harness home and workspace: -The surrounding runtime also loads JSONL session persistence and automatic context compaction. `maxTokensAsSuccess` keeps a token-limited model turn as an accepted evaluation result while preserving its `max-tokens` reason. +```sh +export DEEPSEEK_API_KEY=sk-your-key-here +python examples/python-sdk-agent/minimal.py \ + --dsh-home /absolute/path/to/example-dsh-home \ + --workspace /absolute/path/to/disposable-workspace \ + --session-id example-001 \ + "Inspect the repository and fix the failing tests." +``` -## Runtime environment +Set `DEEPSEEK_BASE_URL` for a compatible proxy, `DSH_MODEL` for the script's default model, or `DSH_SYSTEM_PROMPT` for the deployment persona. `--model` is the single runtime model selection; no matching environment variable is required. `--profile` can select another SDK-serving profile. The selected home stores the generated `sdk-minimal` profile and uncompressed JSONL session logs under `sessions/`; the script never reads `~/.dsh` implicitly. -| Variable | Purpose | -|---|---| -| `DEEPSEEK_API_KEY` | Credential passed to the OpenAI-compatible host endpoint | -| `DEEPSEEK_BASE_URL` | Host endpoint used by `dsh-llm-deepseek` | -| `DSH_CWD` | Agent workspace for bash and filesystem tools | -| `DSH_CONTEXT_WINDOW` | Context capacity recorded for the `DSH_MODEL` catalog entry in the minimal variant | -| `DSH_MAX_TOKENS_AS_SUCCESS` | `true` (default) accepts token-limited results; `false` reports them as errors | -| `DSH_MODEL` | Default model used by `minimal.py`; `--model` takes precedence | -| `DSH_SESSION_ROOT` | JSONL session directory | -| `DSH_SYSTEM_PROMPT` | Deployment-provided coding persona | - -Pass the config path through the Python SDK's `cordis` option or `DSH_CORDIS_CONFIG`. The bundled executable already carries every plugin named by this file; the target machine does not need Node.js. - -## Minimal variant - -[`minimal.cordis.yml`](minimal.cordis.yml) is the complete standalone counterpart of the Web `minimal` preset. `DSH_SYSTEM_PROMPT` selects its system prompt, with `You are a helpful software engineer assistant.` as the fallback. It suppresses every system-prompt runtime-context contribution for fresh sessions and mounts no context-compaction plugin. Its model-facing tools are exactly: +The shipped [`@deepseek-ai/dsh-sdk-minimal` bundle](../../packages/bundle/sdk-minimal/README.md) is the complete explicit Cordis tree for this mode. It exposes exactly: - owner-scoped persistent `bash` - `str_replace_editor` with `view`, `create`, `str_replace`, and `insert` -It composes the local PTY, bare `fs-local` backend, danger-full-access policy for persistent Bash, and uncompressed JSONL persistence needed by the bundled runtime. Bash and absolute editor paths can modify any path available to the runtime process, so run this variant only against a disposable checkout or container. The persistent PTY requires a POSIX terminal environment and is not a Windows agent interface. +The bundle does not include `dsh-base`, so every additional row is an explicit profile change. Runtime context, local instruction discovery, compaction, settings, managed credentials, telemetry, Web tools, subagents, and the full default tool roster are absent. The tree retains SDK startup and JSON-RPC serving, one environment-configured DeepSeek adapter, local execution, and JSONL persistence. -[`minimal.py`](minimal.py) runs the composition through the Python SDK and uses `DSH_MODEL` as its default model. The [Python SDK tutorial](../../docs/user/guide/python-sdk.md) covers installation, execution, workspace selection, and session identity; the [SDK reference](../../python/sdk/README.md) owns runtime lifecycle and result semantics. +This variant is intentionally POSIX-only. Its persistent PTY and editor can modify any path available to the runtime process, so use a disposable checkout or container. + +## Add plugins + +Use the runtime wheel's `dsh` command against the same explicit home for persistent profile changes: + +```sh +export DSH_HOME=/absolute/path/to/example-dsh-home +dsh plugin --profile sdk-minimal add file:/absolute/path/to/my-plugin-bundle +``` + +Use `sdk-minimal` in that command to extend this example, or `sdk` to extend the full base-backed SDK profile. The Python call can also pass additional absolute patch paths in `patches=(...)`; later files win. A selected profile must retain `@deepseek-ai/dsh-sdk-app` or another JSON-RPC server row. The example accepts no complete Cordis file or arbitrary process argv. + +The same runtime wheel packages the `web` profile and its frontend assets for direct CLI use: `dsh web` starts that separate application. A Python SDK client cannot select `web` because it has no JSON-RPC server row. + +See the [Python SDK tutorial](../../docs/user/guide/python-sdk.md) and [SDK reference](../../python/sdk/README.md). diff --git a/examples/python-sdk-agent/README.zh.md b/examples/python-sdk-agent/README.zh.md index d843d8be71..5e6e2f89f2 100644 --- a/examples/python-sdk-agent/README.zh.md +++ b/examples/python-sdk-agent/README.zh.md @@ -2,39 +2,43 @@ [English](README.md) | 中文 -面向 Python SDK 内置 JSON-RPC 运行时的无人值守编码 agent(智能体)组合。它有意不加载终端 UI、控制台日志记录器、批准界面或用户交互工具,因为 stdout 属于 SDK 协议,轮次由 SDK 驱动。 +基于唯一应用启动器 `dsh --profile sdk-minimal` 的可运行 Python SDK 示例。Python 客户端负责 JSON-RPC stdio;profile 负责 agent 组合、持久化、执行策略与插件。 -面向模型的工具为: +## 运行极简 agent -- `bash`,仅前台 -- `read`、`write` 和 `edit` -- `subagent`,使用一个在进程内以前台方式运行的 spawn 提供方 -- `todo_write` +安装 `deepseek-harness-sdk`、导出模型凭据,然后提供隔离的 Harness home 与 workspace: -周边运行时还加载 JSONL 会话持久化和自动上下文压缩(context compaction)。`maxTokensAsSuccess` 将受 token 上限限制的模型轮次保留为已接受的评估结果,同时保留其 `max-tokens` 原因。 +```sh +export DEEPSEEK_API_KEY=sk-your-key-here +python examples/python-sdk-agent/minimal.py \ + --dsh-home /absolute/path/to/example-dsh-home \ + --workspace /absolute/path/to/disposable-workspace \ + --session-id example-001 \ + "Inspect the repository and fix the failing tests." +``` -## 运行时环境 +兼容代理使用 `DEEPSEEK_BASE_URL`,脚本默认模型使用 `DSH_MODEL`,deployment persona 使用 `DSH_SYSTEM_PROMPT`。`--model` 是唯一运行时模型选择,不要求匹配的环境变量;`--profile` 可以选择另一个提供 SDK 服务的 profile。所选 home 保存生成的 `sdk-minimal` profile,并在 `sessions/` 下保存未压缩 JSONL 会话日志;脚本绝不会隐式读取 `~/.dsh`。 -| 变量 | 用途 | -|---|---| -| `DEEPSEEK_API_KEY` | 传给 OpenAI 兼容宿主端点的凭据 | -| `DEEPSEEK_BASE_URL` | `dsh-llm-deepseek` 使用的宿主端点 | -| `DSH_CWD` | bash 和文件系统工具使用的 agent workspace | -| `DSH_CONTEXT_WINDOW` | 极简变体中为 `DSH_MODEL` 目录项记录的上下文容量 | -| `DSH_MAX_TOKENS_AS_SUCCESS` | `true`(默认)接受受 token 上限限制的结果;`false` 将其报告为错误 | -| `DSH_MODEL` | `minimal.py` 使用的默认模型;`--model` 优先 | -| `DSH_SESSION_ROOT` | JSONL 会话目录 | -| `DSH_SYSTEM_PROMPT` | 由部署提供的编码人格 | +随附的 [`@deepseek-ai/dsh-sdk-minimal` 组合包](../../packages/bundle/sdk-minimal/README.zh.md)是该模式完整且显式的 Cordis 配置树。它只暴露: -通过 Python SDK 的 `cordis` 选项或 `DSH_CORDIS_CONFIG` 传入配置路径。内置可执行文件已携带此文件中指定的每个插件;目标机器无需 Node.js。 +- agent 所有的持久 `bash` +- 支持 `view`、`create`、`str_replace` 与 `insert` 的 `str_replace_editor` -## 极简变体 +该组合包不包含 `dsh-base`,因此每一个新增配置项都是显式 profile 变更。运行时上下文、本地指令发现、compaction、settings、托管凭据、遥测、Web 工具、subagent 与完整默认工具清单均不存在。配置树保留 SDK 启动与 JSON-RPC 服务、一个由环境配置的 DeepSeek 适配器、本地执行和 JSONL 持久化。 -[`minimal.cordis.yml`](minimal.cordis.yml) 是 Web `minimal` preset 的完整独立版本。`DSH_SYSTEM_PROMPT` 选择它的系统提示词,未设置时使用 `You are a helpful software engineer assistant.`。它为新建会话抑制每个 system-prompt runtime-context 贡献,且不挂载上下文压缩插件。面向模型的工具严格只有: +此变体刻意只支持 POSIX。其持久 PTY 与 editor 可以修改运行时进程可访问的任何路径,因此只应在一次性 checkout 或容器中使用。 -- 所有者作用域内持久化的 `bash` -- 提供 `view`、`create`、`str_replace` 与 `insert` 的 `str_replace_editor` +## 添加插件 -它组合了内置运行时所需的本地 PTY、裸 `fs-local` 后端、供持久 Bash 使用的 danger-full-access 策略,以及未压缩的 JSONL 持久化。Bash 和编辑器绝对路径可以修改运行时进程有权访问的任何路径,因此只能针对可丢弃的 checkout 或容器运行该变体。持久 PTY 需要 POSIX 终端环境,因此不适用于 Windows agent 接口。 +对同一个显式 home 使用运行时 wheel 提供的 `dsh` 命令,以进行持久 profile 变更: -[`minimal.py`](minimal.py)通过 Python SDK 运行该组合,并把 `DSH_MODEL` 作为默认模型。[Python SDK 教程](../../docs/user/guide/python-sdk.zh.md)介绍安装、运行、workspace 选择与 session 标识;[SDK 参考](../../python/sdk/README.zh.md)归属运行时生命周期与结果语义。 +```sh +export DSH_HOME=/absolute/path/to/example-dsh-home +dsh plugin --profile sdk-minimal add file:/absolute/path/to/my-plugin-bundle +``` + +在该命令中使用 `sdk-minimal` 可扩展本示例,使用 `sdk` 则扩展基于完整 base 的 SDK profile。Python 调用也可以在 `patches=(...)` 中传入更多绝对 patch 路径;后面的文件优先。所选 profile 必须保留 `@deepseek-ai/dsh-sdk-app` 或另一个 JSON-RPC server 配置项。该示例不接受完整 Cordis 文件或任意进程 argv。 + +同一个运行时 wheel 还为直接 CLI 使用打包 `web` profile 及其前端产物:`dsh web` 会启动这个独立应用。Python SDK client 不能选择 `web`,因为其中没有 JSON-RPC server 配置项。 + +另见 [Python SDK 教程](../../docs/user/guide/python-sdk.zh.md)与 [SDK 参考](../../python/sdk/README.zh.md)。 diff --git a/examples/python-sdk-agent/cordis.snapshot.yml b/examples/python-sdk-agent/cordis.snapshot.yml index 23bc8c5402..a054ddbe4e 100644 --- a/examples/python-sdk-agent/cordis.snapshot.yml +++ b/examples/python-sdk-agent/cordis.snapshot.yml @@ -3,10 +3,9 @@ # key or network; every other entry remains shared. The replay provider # catalog claims the `deepseek-official` provider so the SDK server's `initialize` # finds it owned and never mounts the real-adapter fallback. The SDK snapshot -# suite passes this path explicitly through `DSH_CORDIS_CONFIG` (the -# jsonrpc-demo bin performs no DSH_SNAPSHOT config swap of its own), and -# `llm-replay` reads `DSH_SNAPSHOT_FILE` / `DSH_SNAPSHOT_CHILD_FILES` from the -# harness. Stdout remains reserved for JSON-RPC frames. +# suite selects this complete-config fixture explicitly, and `llm-replay` +# reads `DSH_SNAPSHOT_FILE` / `DSH_SNAPSHOT_CHILD_FILES` from the harness. +# It is not a Python launch interface. Stdout remains reserved for JSON-RPC. - id: base name: '@deepseek-ai/cordis-plugin-include' config: diff --git a/examples/python-sdk-agent/cordis.yml b/examples/python-sdk-agent/cordis.yml index 40878c58b2..eeececff10 100644 --- a/examples/python-sdk-agent/cordis.yml +++ b/examples/python-sdk-agent/cordis.yml @@ -1,5 +1,6 @@ -# Unattended coding-agent deployment for the bundled dsh-jsonrpc-agent runtime. -# stdout is reserved for JSON-RPC; do not add a console logger or terminal UI. +# Complete JSON-RPC composition fixture for lower-level Loader and SDK tests. +# Python users launch `dsh --profile sdk` and apply patches instead. +# Stdout is reserved for JSON-RPC; do not add a console logger or terminal UI. - id: sdk-jsonrpc-server name: '@deepseek-ai/dsh-sdk-jsonrpc-server' @@ -68,6 +69,7 @@ config: provider: spawn toolName: subagent + enableModelSelection: true enableRunInBackground: false - id: tool-todo diff --git a/examples/python-sdk-agent/minimal.cordis.yml b/examples/python-sdk-agent/minimal.cordis.yml deleted file mode 100644 index fdf3a18e7a..0000000000 --- a/examples/python-sdk-agent/minimal.cordis.yml +++ /dev/null @@ -1,91 +0,0 @@ -# Complete unattended minimal-agent composition for the Python SDK. The model -# sees one deployment-selected system prompt and only the owner-scoped -# persistent Bash and string-replace editor tools. Runtime-context injection and -# context compaction are absent. - -- id: sdk-jsonrpc-server - name: '@deepseek-ai/dsh-sdk-jsonrpc-server' - config: - maxTokensAsSuccess: false - -- id: deepseek-llm-api-extensions - name: '@deepseek-ai/dsh-deepseek-llm-api-extensions' - -- id: session-log-deepseek - name: '@deepseek-ai/dsh-session-log-deepseek' - -- id: plugin-package-inventory-deepseek - name: '@deepseek-ai/dsh-plugin-package-inventory-deepseek' - -- id: llm-deepseek - name: '@deepseek-ai/dsh-llm-deepseek' - config: - apiKeyEnv: DEEPSEEK_API_KEY - streamIdleTimeoutMs: 172800000 - models: - - id: !!js process.env.DSH_MODEL ?? 'deepseek-v4-flash' - contextWindow: !!js Number(process.env.DSH_CONTEXT_WINDOW ?? 1000000) - -- id: sandbox - name: '@deepseek-ai/dsh-sandbox-local' - -- id: sandbox-policy - name: '@deepseek-ai/dsh-sandbox-policy' - config: - mode: danger-full-access - workspaceRoot: !!js process.env.DSH_CWD ?? process.cwd() - -- id: subprocess - name: '@deepseek-ai/dsh-subprocess-local' - -- id: pty - name: '@deepseek-ai/dsh-terminal' - -- id: terminal-bash - name: '@deepseek-ai/dsh-terminal-bash' - config: - timeoutMs: 300000 - -# The editor uses the bare local filesystem; persistent Bash still consumes the -# shared danger-full-access sandbox policy above. -- id: fs-local - name: '@deepseek-ai/dsh-fs-local' - config: - cwd: !!js process.env.DSH_CWD ?? process.cwd() - -- id: agent-spine - name: '@deepseek-ai/dsh-agent-spine-demo' - config: - includeHarnessIdentity: false - includeRuntimeContext: false - persona: !!js process.env.DSH_SYSTEM_PROMPT ?? 'You are a helpful software engineer assistant.' - workspaceContext: false - skills: - enabled: false - toolBash: false - toolJobs: false - -- id: persistent-bash - name: '@deepseek-ai/dsh-tool-bash-persistent' - config: - timeoutMs: 300000 - description: |- - Run commands in a bash shell - * When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped. - * You don't have access to the internet via this tool. - * You do have access to a mirror of common linux and python packages via apt and pip. - * State is persistent across command calls and discussions with the user. - * To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'. - * Please avoid commands that may produce a very large amount of output. - * Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background. - -- id: str-replace-editor - name: '@deepseek-ai/dsh-tool-str-replace-editor' - config: - maxOutputChars: 16000 - -- id: sessions - name: '@deepseek-ai/dsh-session-persistence-jsonl' - config: - root: !!js process.env.DSH_SESSION_ROOT ?? './.sessions' - compression: none diff --git a/examples/python-sdk-agent/minimal.py b/examples/python-sdk-agent/minimal.py index e94b02b7d8..28d2fd8c54 100644 --- a/examples/python-sdk-agent/minimal.py +++ b/examples/python-sdk-agent/minimal.py @@ -10,30 +10,35 @@ from pathlib import Path from deepseek_harness import DeepSeekHarness -CONFIG = Path(__file__).with_name("minimal.cordis.yml") - - def main() -> None: """Parse one task and print the agent's final response.""" parser = argparse.ArgumentParser() + configured_home = os.environ.get("DSH_HOME", "") parser.add_argument("prompt", help="Task for the minimal agent") parser.add_argument("--workspace", type=Path, default=Path.cwd()) - parser.add_argument("--session-root", type=Path, default=Path(".dsh-sessions")) + parser.add_argument( + "--dsh-home", + type=Path, + default=Path(configured_home) if configured_home.strip() else None, + ) + parser.add_argument("--profile", default="sdk-minimal") parser.add_argument("--session-id") parser.add_argument("--provider", default="deepseek-official") parser.add_argument("--model", default=os.environ.get("DSH_MODEL", "deepseek-v4-flash")) parser.add_argument("--max-tokens", type=int) args = parser.parse_args() + if args.dsh_home is None: + parser.error("--dsh-home or a non-empty DSH_HOME is required") workspace = args.workspace.resolve() - session_root = args.session_root.resolve() + dsh_home = args.dsh_home.resolve() with DeepSeekHarness( provider=args.provider, model=args.model, max_tokens=args.max_tokens, cwd=str(workspace), - session_root=str(session_root), - cordis=str(CONFIG.resolve()), + dsh_home=str(dsh_home), + profile=args.profile, ) as harness: result = harness.run(args.prompt, session_id=args.session_id) print(result.final_response) diff --git a/examples/python-sdk-agent/minimal.snapshot.cordis.yml b/examples/python-sdk-agent/minimal.snapshot.cordis.yml deleted file mode 100644 index 0f26fa6716..0000000000 --- a/examples/python-sdk-agent/minimal.snapshot.cordis.yml +++ /dev/null @@ -1,20 +0,0 @@ -# Keyless replay keeps the complete minimal composition intact and replaces -# only its live DeepSeek adapter with the fixture-backed provider. The replay -# catalog claims the same route initialized by the SDK. -- id: base - name: '@deepseek-ai/cordis-plugin-include' - config: - path: ./minimal.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-official - name: DeepSeek - models: - - id: deepseek-v4-flash diff --git a/examples/python-sdk-agent/tests/keyless-smoke.e2e.ts b/examples/python-sdk-agent/tests/keyless-smoke.e2e.ts index 99897079ef..230ada0336 100644 --- a/examples/python-sdk-agent/tests/keyless-smoke.e2e.ts +++ b/examples/python-sdk-agent/tests/keyless-smoke.e2e.ts @@ -8,8 +8,7 @@ import { zstdDecompress } from 'node:zlib' import { execa } from 'execa' import { describe, expect, it } from 'vitest' -const binScript = fileURLToPath(new URL('../../../packages/sdk/python-runtime/src/packaged-bin.ts', import.meta.url)) -const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url)) +const binScript = fileURLToPath(new URL('../../../apps/cli/src/bin.ts', import.meta.url)) const repoRoot = fileURLToPath(new URL('../../..', import.meta.url)) const decompress = promisify(zstdDecompress) @@ -45,7 +44,7 @@ function waitForLine( }) } -describe('Python SDK runtime carrier keyless smoke', () => { +describe('Python SDK dsh profile keyless smoke', () => { it.each([ { label: 'reports max-token turns with the default mapping config', envValue: undefined }, { label: 'reports max-token turns with mapping enabled through env', envValue: 'true' }, @@ -73,16 +72,18 @@ describe('Python SDK runtime carrier keyless smoke', () => { // execa owns spawn, the deadline, and exit settlement around it. const child = execa(process.execPath, [ '--import', - 'tsx', + 'tsx/esm', binScript, - configPath, + '--profile', + 'sdk', ], { cwd: repoRoot, env: { + DSH_HOME: join(root, '.dsh'), + DSH_PERMISSION_MODE: 'danger-full-access', + DSH_TELEMETRY_DISABLED: '1', DEEPSEEK_API_KEY: 'keyless-smoke-no-call', DEEPSEEK_BASE_URL: `http://127.0.0.1:${address.port}`, - DSH_CWD: root, - DSH_SESSION_ROOT: join(root, '.sessions'), ...(envValue === undefined ? {} : { DSH_MAX_TOKENS_AS_SUCCESS: envValue }), }, timeout: 35_000, @@ -145,21 +146,14 @@ describe('Python SDK runtime carrier keyless smoke', () => { }) const tools = modelRequests[0]?.tools as { function?: { name?: string } }[] expect(modelRequests[0]?.max_tokens).toBe(1234) - expect(tools.map(tool => tool.function?.name).sort()).toEqual([ - 'bash', - 'edit', - 'read', - 'subagent', - 'todo_write', - 'write', - ]) + expect(tools.map(tool => tool.function?.name)).toContain('list_subagent_models') child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'shutdown' })}\n`) const shutdown = await waitForLine(lines, value => value.id === 3, () => stderr) expect(shutdown).toMatchObject({ jsonrpc: '2.0', id: 3, result: {} }) const exit = await child expect(exit.exitCode, `signal=${String(exit.signal)}; stderr=${stderr}`).toBe(0) - const sessionsRoot = join(root, '.sessions') + const sessionsRoot = join(root, '.dsh', 'sessions') const files = await readdir(sessionsRoot, { recursive: true }) const log = files.find(file => file.endsWith('.jsonl.zstd')) expect(log).toBeDefined() @@ -175,28 +169,129 @@ describe('Python SDK runtime carrier keyless smoke', () => { } }, 40_000) - it('rejects an invalid max-token success env value', async () => { - const { exitCode, stdout, stderr } = await execa(process.execPath, [ + it('boots the standalone minimal profile with its exact model-facing roster', async () => { + const root = await mkdtemp(join(tmpdir(), 'dsh-python-sdk-minimal-')) + const modelRequests: Record[] = [] + const modelServer = createServer((request, response) => { + let body = '' + request.setEncoding('utf8') + request.on('data', (chunk: string) => { body += chunk }) + request.on('end', () => { + modelRequests.push(JSON.parse(body) as Record) + response.writeHead(200, { 'content-type': 'text/event-stream' }) + response.write('data: {"choices":[{"delta":{"role":"assistant","content":null}}]}\n\n') + response.write('data: {"choices":[{"delta":{"content":"done"}}]}\n\n') + response.write('data: {"choices":[{"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":1}}\n\n') + response.end('data: [DONE]\n\n') + }) + }) + 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 a TCP port') + const child = execa(process.execPath, [ '--import', - 'tsx', + 'tsx/esm', binScript, - configPath, + '--profile', + 'sdk-minimal', ], { cwd: repoRoot, env: { + DSH_HOME: join(root, '.dsh'), + DSH_SYSTEM_PROMPT: 'Minimal allowlist prompt.', DEEPSEEK_API_KEY: 'keyless-smoke-no-call', - DSH_MAX_TOKENS_AS_SUCCESS: 'sometimes', + DEEPSEEK_BASE_URL: `http://127.0.0.1:${address.port}`, }, - stdin: 'ignore', - timeout: 25_000, + timeout: 35_000, killSignal: 'SIGKILL', reject: false, }) + const lines: string[] = [] + let stdoutBuffer = '' + let stderr = '' + child.stdout.on('data', (chunk: Buffer) => { + stdoutBuffer += chunk.toString('utf8') + const parts = stdoutBuffer.split('\n') + stdoutBuffer = parts.pop() ?? '' + lines.push(...parts) + }) + child.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString('utf8') }) - expect(exitCode, stderr).toBe(1) - expect(stdout).toBe('') - expect(stderr).toContain('plugin tree failed to load') - expect(stderr).toContain('failed to apply loader entry sdk-jsonrpc-server (@deepseek-ai/dsh-sdk-jsonrpc-server)') - expect(stderr).toContain('sometimes') + try { + child.stdin.write(`${JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { cwd: root, provider: 'deepseek-official', model: 'deepseek-v4-pro' }, + })}\n`) + await waitForLine(lines, value => value.id === 1, () => stderr) + child.stdin.write(`${JSON.stringify({ + jsonrpc: '2.0', + id: 2, + method: 'session/prompt', + params: { sessionId: 'minimal', contentBlocks: [{ type: 'text', text: 'inspect tools' }] }, + })}\n`) + await waitForLine(lines, (value) => { + const params = value.params as Record | undefined + const event = params?.event as Record | undefined + return params?.sessionId === 'minimal' && event?.type === 'turn/end' + }, () => stderr) + + const request = modelRequests[0] as { + messages?: Array<{ role?: string; content?: unknown }> + tools?: Array<{ function?: { name?: string } }> + } + expect(request.messages?.[0]).toMatchObject({ role: 'system', content: 'Minimal allowlist prompt.' }) + expect(request.tools?.map(tool => tool.function?.name).sort()).toEqual(['bash', 'str_replace_editor']) + const profile = JSON.parse( + await readFile(join(root, '.dsh', 'profiles', 'sdk-minimal', 'package.json'), 'utf8'), + ) as { dsh?: { profile?: { bundles?: string[]; patchReload?: string } } } + expect(profile.dsh?.profile).toEqual({ + bundles: ['@deepseek-ai/dsh-sdk-minimal'], + patchReload: 'startup', + }) + + child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'shutdown' })}\n`) + await waitForLine(lines, value => value.id === 3, () => stderr) + const exit = await child + expect(exit.exitCode, `signal=${String(exit.signal)}; stderr=${stderr}`).toBe(0) + } finally { + child.kill('SIGKILL') + await child + await new Promise(resolve => modelServer.close(() => { resolve() })) + await rm(root, { recursive: true, force: true }) + } + }, 40_000) + + it('rejects an invalid max-token success env value', async () => { + const root = await mkdtemp(join(tmpdir(), 'dsh-python-sdk-runtime-invalid-')) + try { + const { exitCode, stdout, stderr } = await execa(process.execPath, [ + '--import', + 'tsx/esm', + binScript, + '--profile', + 'sdk', + ], { + cwd: repoRoot, + env: { + DSH_HOME: join(root, '.dsh'), + DEEPSEEK_API_KEY: 'keyless-smoke-no-call', + DSH_MAX_TOKENS_AS_SUCCESS: 'sometimes', + }, + stdin: 'ignore', + timeout: 25_000, + killSignal: 'SIGKILL', + reject: false, + }) + + expect(exitCode, stderr).toBe(1) + expect(stdout).toBe('') + expect(stderr).toContain('plugin tree failed to load') + expect(stderr).toContain('failed to apply loader entry sdk-jsonrpc-server (@deepseek-ai/dsh-sdk-jsonrpc-server)') + expect(stderr).toContain('sometimes') + } finally { + await rm(root, { recursive: true, force: true }) + } }, 30_000) }) diff --git a/examples/python-sdk-agent/tests/snapshots/subagent-spawn-in-process/notifications.expected.jsonl b/examples/python-sdk-agent/tests/snapshots/subagent-spawn-in-process/notifications.expected.jsonl index f00b85e15f..48d98ad6de 100644 --- a/examples/python-sdk-agent/tests/snapshots/subagent-spawn-in-process/notifications.expected.jsonl +++ b/examples/python-sdk-agent/tests/snapshots/subagent-spawn-in-process/notifications.expected.jsonl @@ -104,7 +104,7 @@ {"method":"session.status","params":{"sessionId":"{{sessionId}}","status":"running"}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/start","seq":3,"time":0,"data":{"turn":1}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":4,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"subagent/descriptor","seq":5,"time":0,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"echo probe"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"subagent/descriptor","seq":5,"time":0,"data":{"version":3,"mode":"one-shot","provider":"spawn","label":"echo probe"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":7,"time":0,"data":{"content":[{"type":"text","text":"Reply with exactly: child answer 42."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} diff --git a/examples/python-sdk-agent/tests/snapshots/subagent-spawn-in-process/session.1.jsonl b/examples/python-sdk-agent/tests/snapshots/subagent-spawn-in-process/session.1.jsonl index 52aa539f93..059bded5dc 100644 --- a/examples/python-sdk-agent/tests/snapshots/subagent-spawn-in-process/session.1.jsonl +++ b/examples/python-sdk-agent/tests/snapshots/subagent-spawn-in-process/session.1.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly: child answer 42."}],"source":{"kind":"user"},"role":"user","id":"7ae1698c-db1d-4fca-8404-3a9dece9c1d0"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} -{"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"spawn","label":"echo probe"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn","label":"echo probe"}} {"type":"step/start","data":{"turn":1,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly: child answer 42."}],"source":{"kind":"user"},"role":"user","id":"7ae1698c-db1d-4fca-8404-3a9dece9c1d0"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"dc291267-28a7-40f4-adac-cd856dbe0bba"},"surfaceOp":"append"} diff --git a/knip.json b/knip.json index b6c4e33254..e8dc6139ee 100644 --- a/knip.json +++ b/knip.json @@ -550,11 +550,6 @@ "zod" ] }, - "packages/sdk/python-runtime": { - "project": [ - "src/**/*.ts" - ] - }, "packages/subagent/subagent-spawn-in-process": { "entry": [ "tests/**/*.spec.ts", @@ -704,6 +699,11 @@ "@deepseek-ai/dsh-sdk-jsonrpc-server" ] }, + "packages/bundle/sdk-minimal": { + "ignoreDependencies": [ + "@deepseek-ai/.+" + ] + }, "packages/bundle/web-app": { "ignoreDependencies": [ "@deepseek-ai/.+" diff --git a/packages/README.i18n.yaml b/packages/README.i18n.yaml index 9478bfcb2a..4dc1bb405a 100644 --- a/packages/README.i18n.yaml +++ b/packages/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/README.md -README.md: 5e44b3821d1272923f1545e697d91a434374d24a -README.zh.md: 1bf06b993c0379ecebf8246a50e1fc5fe5f4fce1 +README.md: 6d1add5baa7c73033dadc4247a03b546e6ff9dfd +README.zh.md: da2606b93cae1a6a642a6ee15ac5f0cfc8ce6457 diff --git a/packages/README.md b/packages/README.md index 5e44b3821d..6d1add5baa 100644 --- a/packages/README.md +++ b/packages/README.md @@ -50,7 +50,7 @@ Groups hold `packages///`; names stay `@deepseek-ai/dsh-`. **Gr | [`credentials/`](credentials/README.md) | Credential reference/record seam + env-over-`.env` provider + authorization flows | Product — stable API | | [`storage/`](storage/README.md) | Non-session storage hub + backends + domain form | Product — stable API | | [`workspace/`](workspace/README.md) | Workspace entity | Product — stable API | -| [`sdk/`](sdk/README.md) | Out-of-process SDK: JSON-RPC protocol, TypeScript client/server, and private Python carrier | Product — stable API | +| [`sdk/`](sdk/README.md) | Out-of-process SDK: JSON-RPC protocol and TypeScript client/server | Product — stable API | | [`acp/`](acp/README.md) | Automation-only Agent Client Protocol server | Product — stable API | | [`interaction/`](interaction/README.md) | Human-collaboration plane: approval/interaction seams, permission preset, commands, ask-user tool | Product — stable API | | [`boot/`](boot/README.md) | Shared app-bin boot glue | Product — stable API | diff --git a/packages/README.zh.md b/packages/README.zh.md index 1bf06b993c..da2606b93c 100644 --- a/packages/README.zh.md +++ b/packages/README.zh.md @@ -50,7 +50,7 @@ npm scope 为 `@deepseek-ai/dsh-*`;Cordis `Service` 子类和函数插件通 | [`credentials/`](credentials/README.zh.md) | 凭据引用/记录 seam + 环境变量优先于 `.env` 的提供方 + 授权 flow | 产品:稳定 API | | [`storage/`](storage/README.zh.md) | 非会话存储中枢 + 后端 + 领域形式 | 产品:稳定 API | | [`workspace/`](workspace/README.zh.md) | Workspace 实体 | 产品:稳定 API | -| [`sdk/`](sdk/README.zh.md) | 进程外 SDK:JSON-RPC 协议、TypeScript 客户端/服务器和私有 Python 载体 | 产品:稳定 API | +| [`sdk/`](sdk/README.zh.md) | 进程外 SDK:JSON-RPC 协议与 TypeScript 客户端/服务器 | 产品:稳定 API | | [`acp/`](acp/README.zh.md) | 仅面向自动化的 ACP(Agent Client Protocol)服务器 | 产品:稳定 API | | [`interaction/`](interaction/README.zh.md) | 人机协作平面:批准/交互 seam、权限预设、命令、询问用户的工具 | 产品:稳定 API | | [`boot/`](boot/README.zh.md) | 共享的 app bin 启动粘合层 | 产品:稳定 API | diff --git a/packages/api/session-controller/tests/session-cold.host.spec.ts b/packages/api/session-controller/tests/session-cold.host.spec.ts index da2d77ece5..aa51d159f6 100644 --- a/packages/api/session-controller/tests/session-cold.host.spec.ts +++ b/packages/api/session-controller/tests/session-cold.host.spec.ts @@ -15,6 +15,7 @@ import { SessionHistoryController } from '@deepseek-ai/dsh-api-session-controlle import { TypertLookupFailure } from '@deepseek-ai/dsh-typert-protocol' import TypertRegistry from '@deepseek-ai/dsh-typert-registry' import { createUserMessage, MessageId } from '@deepseek-ai/dsh-llm' +import { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent' import type { Agent } from '@deepseek-ai/dsh-agent' import type { SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session' import type { SessionPromptRequest, SessionRequestId } from '../src/types.ts' @@ -448,7 +449,11 @@ describe('subagent ownership fence', () => { type: 'subagent/descriptor', seq: 2, time: 3, - data: { version: 2, mode: 'continuable', provider: 'spawn', label: 'child' }, + data: snapshotSubagentDescriptor({ + mode: 'continuable', + provider: 'spawn', + label: 'child', + }), }, { type: 'turn/end', seq: 3, time: 4, data: { turn: 1, reason: { kind: 'completed' } } }, ] as SessionEvent[] diff --git a/packages/attachment/attachment-local/README.i18n.yaml b/packages/attachment/attachment-local/README.i18n.yaml index 4916c8ad2e..353d5d1d7b 100644 --- a/packages/attachment/attachment-local/README.i18n.yaml +++ b/packages/attachment/attachment-local/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/attachment/attachment-local/README.md -README.md: d9b6839c9b02fdfa4a0661ed44ae77e32e9a255e -README.zh.md: 11b4a6239e8b14ecfcc905e40a4cdc5042de5fcf +README.md: 7bd0283e7f922ecfa2be4b67e296f5c0016f4302 +README.zh.md: 6e8ad367cf7e81f373c112e27cace07491638e0f diff --git a/packages/attachment/attachment-local/README.md b/packages/attachment/attachment-local/README.md index d9b6839c9b..7bd0283e7f 100644 --- a/packages/attachment/attachment-local/README.md +++ b/packages/attachment/attachment-local/README.md @@ -4,9 +4,9 @@ English | [中文](README.zh.md) The private local implementation of [`@deepseek-ai/dsh-attachment`](../attachment). Objects land at `/attachments/v1/objects//` and are addressed by an opaque `sha256:` id. Each process proves a home durable once by syncing every ancestor entry to the filesystem root. Writes use a private staging directory, a synced temporary file, an atomic exclusive hard-link publish, owner-read-only object permissions, and directory syncs on the publication path (POSIX; Windows relies on filesystem metadata journaling) so the reported reference survives a crash. -Admission accepts at most 20 images and 200MiB of encoded source bytes per message. Each source may use up to 20MiB, 64,000,000 pixels, and 8192px per side. It then prepares a provider-independent normalized attachment. EXIF orientation is applied, metadata and color profiles are removed, pixels become 8-bit sRGB/sRGBA, and the long edge is reduced proportionally to `normalizedImageMaxDimension` (2048px by default). The normalized attachment has its own `normalizedImageMaxBytes` safety cap (4MiB by default). Transparent pixels are retained; Sharp/libvips may omit an alpha plane whose samples are all opaque. A nearest-neighbour bounded sample classifies color complexity without averaging high-frequency pixels. Confirmed low-color images try PNG, using a palette only when the input has no alpha channel, then WebP at qualities 85, 80, and 75. Other alpha images try WebP at those qualities; other opaque images try JPEG. Each candidate runs only after the preceding candidate exceeds the cap. Dimensions shrink only after every candidate at one size exceeds the cap. A clean, single-frame 8-bit sRGB/sRGBA PNG, JPEG, or WebP already within both normalization limits passes through byte-identically; 16-bit PNG, GIF, animated input, metadata, orientation, and incompatible color spaces force conversion. The source and converted attachment are each fully decoded once. `saveImages` prepares and verifies every normalized attachment once before publishing the batch, so validation failure leaves no partial references and commit does not repeat full image encoding. +Admission accepts at most 20 images and 200MiB of encoded source bytes per message. Each source may use up to 20MiB, 64,000,000 pixels, and 8192px per side. It then prepares a provider-independent normalized attachment. EXIF orientation is applied, metadata and color profiles are removed, pixels become 8-bit sRGB/sRGBA, and the raster is reduced proportionally to the `normalizedImageMaxPixels` total-pixel budget (2048x2048 by default) with a `normalizedImageMaxDimension` long-edge cap (8192px by default), so extreme aspect ratios keep their short-edge resolution instead of collapsing under a long-edge rule. The normalized attachment has its own `normalizedImageMaxBytes` encoded-byte target (4MiB by default). Transparent pixels are retained; Sharp/libvips may omit an alpha plane whose samples are all opaque. Sources with an alpha channel encode as WebP (effort 0) and opaque sources as JPEG, both on the quality ladder 85, 75, 60. Each ladder step runs only after the preceding step exceeds the target, and when every step exceeds it the smallest output is kept; provider byte caps stay enforced by the route that transmits the bytes. A clean, single-frame 8-bit sRGB/sRGBA PNG, JPEG, or WebP already within both normalization limits passes through byte-identically; 16-bit PNG, GIF, animated input, metadata, orientation, and incompatible color spaces force conversion. The source and converted attachment are each fully decoded once. `saveImages` prepares and verifies every normalized attachment once before publishing the batch, so validation failure leaves no partial references and commit does not repeat full image encoding. -Request versions live below `/attachments/v1/request-images/`. `readImageRequest` scales the stored normalized attachment under a total-pixel budget without enlargement, then enforces a separate encoded-byte cap. The request encoder uses the same color branches, with PNG (palette only without alpha) before WebP 85 and 80 for low-color images, WebP 85 then 80 for other alpha images, and JPEG 85 then 80 for other opaque images. It executes candidates lazily and reduces dimensions only after both quality attempts exceed the request cap. Its cache identity includes the attachment id, transform version, pixel and byte budgets, and fixed encoder settings. Cached bytes are fully decoded and checked as 8-bit sRGB/sRGBA before use. Concurrent calls for one identity share one transform and cache write; cancelling one waiter does not cancel the shared work. Callers compose ordered batches from singular reads, while the service's FIFO limiter applies `imageCompressionConcurrency` to simultaneous normalization and request transforms. The setting ranges from 1 through 8 and defaults to 2; file publication remains ordered after preparation. +Request versions live below `/attachments/v1/request-images/`. `readImageRequest` scales the stored normalized attachment under a total-pixel budget without enlargement, then applies a separate encoded-byte target. The request encoder uses the same alpha routing and quality ladder as normalization, WebP (effort 0) at 85, 75, 60 for alpha sources and JPEG at those qualities for opaque sources, executed lazily and keeping the smallest output when every quality exceeds the target. Its cache identity includes the attachment id, transform version, pixel and byte budgets, and fixed encoder settings. Cached bytes are header-probed for format, 8-bit sRGB/sRGBA, dimension, and alpha facts before use; a mismatch regenerates the entry. Concurrent calls for one identity share one transform and cache write; cancelling one waiter does not cancel the shared work. Callers compose ordered batches from singular reads, while the service's FIFO limiter applies `imageCompressionConcurrency` to simultaneous normalization and request transforms. The setting ranges from 1 through 8 and defaults to 2; file publication remains ordered after preparation. `DSH_HOME` resolves through the shared path policy: explicit config, `$DSH_HOME`, then `~/.dsh`. Session logs contain only the reference and verified metadata. `imageHostPath` derives the normalized object's absolute host path and does not inspect the tool execution world. At request assembly, an LLM consumer asks the mounted filesystem to map that host object into its execution world. A host-backed filesystem returns a process path; a remote filesystem without a shared mount returns no path. The mapped path is absent from durable history and from `RequestImageAttachment`. `readImage` forwards optional cancellation into the filesystem read, observes it around verification, and preserves it instead of wrapping it as `ATTACHMENT_READ_FAILED`. diff --git a/packages/attachment/attachment-local/README.zh.md b/packages/attachment/attachment-local/README.zh.md index 11b4a6239e..6e8ad367cf 100644 --- a/packages/attachment/attachment-local/README.zh.md +++ b/packages/attachment/attachment-local/README.zh.md @@ -4,9 +4,9 @@ 这是 [`@deepseek-ai/dsh-attachment`](../attachment) 的私有本地实现。对象存放在 `/attachments/v1/objects//`,并通过不透明的 `sha256:` 标识符寻址。每个进程都会把每级祖先目录项同步到文件系统根目录,以此一次性证明 home 已持久化。写入使用私有暂存目录、经过同步的临时文件、原子且排他的硬链接发布、仅所有者可读的对象权限,并对发布路径执行目录同步(适用于 POSIX;Windows 依赖文件系统元数据日志),确保已报告的引用能够在崩溃后继续存在。 -每条消息最多准入 20 张图片,源图编码字节总量不超过 200MiB。每张源图不得超过 20MiB、64,000,000 像素和单边 8192px。随后生成提供方无关的规范化附件:应用 EXIF 方向,删除元数据和色彩配置文件,转换为 8-bit sRGB/sRGBA,并保持宽高比把长边限制到 `normalizedImageMaxDimension`(默认 2048px)。规范化附件有独立的 `normalizedImageMaxBytes` 安全上限(默认 4MiB)。透明像素会保留;当所有 alpha 样本均为不透明时,Sharp/libvips 可能省略没有实际作用的 alpha 平面。系统用 nearest-neighbour 对有界样本分类,不会通过像素平均把高频图片误判为低色数。确认的低色数图片先尝试 PNG,只有不带 alpha 通道时才使用 palette,随后依次尝试质量 85、80、75 的 WebP;其他透明图片依次尝试这些质量的 WebP;其他非透明图片依次尝试这些质量的 JPEG。只有前一个候选超限时才会执行下一个候选;同一尺寸的候选全部超限后才缩小尺寸。已经处于两个规范化上限内的干净、单帧、8-bit sRGB/sRGBA PNG、JPEG 或 WebP 按字节原样直通;16-bit PNG、GIF、动图、元数据、方向和不兼容色彩空间都会触发转换。源图和转换后的附件各完整解码一次。`saveImages` 在发布任何批次成员前为每张图片各准备并验证一次规范化附件,因此校验失败不会留下部分引用,提交阶段也不会重复执行完整图片编码。 +每条消息最多准入 20 张图片,源图编码字节总量不超过 200MiB。每张源图不得超过 20MiB、64,000,000 像素和单边 8192px。随后生成提供方无关的规范化附件:应用 EXIF 方向,删除元数据和色彩配置文件,转换为 8-bit sRGB/sRGBA,并保持宽高比把像素总量缩到 `normalizedImageMaxPixels` 总像素预算内(默认 2048×2048),再受 `normalizedImageMaxDimension` 长边上限约束(默认 8192px),因此极端长宽比的图保留短边分辨率,而不会在长边规则下坍缩。规范化附件有独立的 `normalizedImageMaxBytes` 编码字节目标(默认 4MiB)。透明像素会保留;当所有 alpha 样本均为不透明时,Sharp/libvips 可能省略没有实际作用的 alpha 平面。带 alpha 通道的源图编码为 WebP(effort 0),不透明源图编码为 JPEG,共用质量阶梯 85、75、60。只有前一档超过目标时才会执行下一档;全部档位都超过目标时保留最小的产物,提供方字节硬上限仍由传输该字节的路由执行。已经处于两个规范化上限内的干净、单帧、8-bit sRGB/sRGBA PNG、JPEG 或 WebP 按字节原样直通;16-bit PNG、GIF、动图、元数据、方向和不兼容色彩空间都会触发转换。源图和转换后的附件各完整解码一次。`saveImages` 在发布任何批次成员前为每张图片各准备并验证一次规范化附件,因此校验失败不会留下部分引用,提交阶段也不会重复执行完整图片编码。 -请求版本保存在 `/attachments/v1/request-images/`。`readImageRequest` 在不放大小图的前提下,把存储的规范化附件缩放到总像素预算内,再执行独立的编码字节上限。请求编码器使用同一分类分支:低色数图片先尝试 PNG,只有不带 alpha 通道时才使用 palette,再尝试质量 85 和 80 的 WebP;其他透明图片依次尝试质量 85 和 80 的 WebP;其他非透明图片依次尝试质量 85 和 80 的 JPEG。候选按需执行,两个质量档均超限后才缩小尺寸。缓存身份包含附件 ID、变换策略版本、像素和字节预算及固定编码参数。缓存字节在使用前会完整解码并校验为 8-bit sRGB/sRGBA。同一身份的并发调用共享一次变换和缓存写入;取消一个等待方不会取消共享任务。调用方组合单数读取得到有序批次,服务的 FIFO 限流器通过 `imageCompressionConcurrency` 限制同时执行的规范化和请求变换。该配置范围为 1 至 8,默认值为 2;文件发布仍在准备结束后按顺序执行。 +请求版本保存在 `/attachments/v1/request-images/`。`readImageRequest` 在不放大小图的前提下,把存储的规范化附件缩放到总像素预算内,再应用独立的编码字节目标。请求编码器与规范化共用同一套 alpha 路由和质量阶梯:带 alpha 的源图依次尝试质量 85、75、60 的 WebP(effort 0),不透明源图依次尝试这些质量的 JPEG;候选按需执行,全部档位都超过目标时保留最小的产物。缓存身份包含附件 ID、变换策略版本、像素和字节预算及固定编码参数。缓存字节在使用前经头部探测校验格式、8-bit sRGB/sRGBA、尺寸和 alpha 事实;不匹配则重新生成该条目。同一身份的并发调用共享一次变换和缓存写入;取消一个等待方不会取消共享任务。调用方组合单数读取得到有序批次,服务的 FIFO 限流器通过 `imageCompressionConcurrency` 限制同时执行的规范化和请求变换。该配置范围为 1 至 8,默认值为 2;文件发布仍在准备结束后按顺序执行。 `DSH_HOME` 按共享路径策略解析:显式配置、`$DSH_HOME`,最后是 `~/.dsh`。会话日志只包含引用和经过校验的元数据。`imageHostPath` 派生规范化对象的绝对宿主路径,不检查工具执行环境。组装请求时,LLM 消费方要求当前文件系统把该宿主对象映射到其执行环境。宿主文件系统返回进程路径;没有共享挂载的远程文件系统不返回路径。映射后的路径不进入持久历史,也不进入 `RequestImageAttachment`。`readImage` 会把可选取消信号传入文件系统读取、在校验前后观察该信号,并保留取消语义,而不会将其包装成 `ATTACHMENT_READ_FAILED`。 diff --git a/packages/attachment/attachment-local/src/encoding.ts b/packages/attachment/attachment-local/src/encoding.ts index bf83d48cf9..1b16aafb30 100644 --- a/packages/attachment/attachment-local/src/encoding.ts +++ b/packages/attachment/attachment-local/src/encoding.ts @@ -1,4 +1,41 @@ -/** Shared lazy candidate execution for normalization and request-image encoders. */ +/** Shared quality ladder and lazy candidate execution for normalization and request-image encoders. */ + +import type { Sharp } from 'sharp' + +/** Shared ladder for both encoders: spaced so each step buys a real size reduction. */ +export const IMAGE_ENCODING_QUALITIES = [85, 75, 60] as const +/** Fixed lossy-WebP effort; deeper search costs 3-4x encode time for about 5% size. */ +export const WEBP_ENCODING_EFFORT = 0 + +/** One ladder output carrying its complete bytes and exact facts. */ +export interface EncodedImage { + data: Uint8Array + mediaType: 'image/jpeg' | 'image/webp' + width: number + height: number +} + +async function encode(pipeline: Sharp, mediaType: EncodedImage['mediaType'], quality: number): Promise { + const encoded = mediaType === 'image/webp' + ? pipeline.webp({ quality, effort: WEBP_ENCODING_EFFORT }) + : pipeline.jpeg({ quality }) + const { data, info } = await encoded.toBuffer({ resolveWithObject: true }) + return { data: new Uint8Array(data), mediaType, width: info.width, height: info.height } +} + +/** + * Build the lazy quality ladder for one prepared pipeline: WebP keeps a source + * alpha channel, everything else is JPEG. + * @param prepared - sized sRGB pipeline; cloned per candidate. + * @param hasAlpha - decoded source alpha fact selecting the codec. + * @returns encoders ordered from highest to lowest ladder quality. + */ +export function encodingLadder(prepared: Sharp, hasAlpha: boolean): Array<() => Promise> { + const mediaType = hasAlpha ? 'image/webp' : 'image/jpeg' + return IMAGE_ENCODING_QUALITIES.map(quality => ( + () => encode(prepared.clone(), mediaType, quality) + )) +} /** One encoded candidate carrying its complete bytes. */ export interface EncodedCandidate { @@ -13,7 +50,7 @@ export interface ExhaustedEncoding { /** * Execute encoding candidates in preference order and stop after the first fitting output. * @param attempts - lazy encoders ordered from preferred to fallback representation. - * @param maxBytes - positive encoded-byte cap. + * @param maxBytes - positive encoded-byte target. * @returns the first fitting candidate, otherwise the smallest completed fallback. */ export async function encodeFirstWithinLimit( @@ -37,7 +74,7 @@ export async function encodeFirstWithinLimit( /** * Whether a lazy encoding result exhausted every candidate at one size. * @param result - first fitting candidate or exhausted result. - * @returns whether every candidate exceeded the byte cap. + * @returns whether every candidate exceeded the byte target. */ export function isExhaustedEncoding( result: T | ExhaustedEncoding, diff --git a/packages/attachment/attachment-local/src/index.ts b/packages/attachment/attachment-local/src/index.ts index 053fa69fc6..46919200eb 100644 --- a/packages/attachment/attachment-local/src/index.ts +++ b/packages/attachment/attachment-local/src/index.ts @@ -35,12 +35,16 @@ export const DEFAULT_MAX_IMAGE_PIXELS = 64_000_000 /** Default per-side pixel cap for one submitted image. */ export const DEFAULT_MAX_IMAGE_DIMENSION = 8192 /** - * Default long-edge target of the stored normalized image. A larger source - * is admitted and downscaled to this edge, so admission bounds what rides - * every later model request without refusing ordinary large sources. + * Default total-pixel budget of the stored normalized image. A larger source + * is admitted and downscaled proportionally, so admission bounds what rides + * every later model request without refusing ordinary large sources; extreme + * aspect ratios keep their short-edge resolution instead of collapsing under + * a long-edge rule. */ -export const DEFAULT_NORMALIZED_IMAGE_MAX_DIMENSION = 2048 -/** Default independent safety cap for one stored normalized image. */ +export const DEFAULT_NORMALIZED_IMAGE_MAX_PIXELS = 2048 * 2048 +/** Default long-edge cap of the stored normalized image, applied after the total-pixel budget. */ +export const DEFAULT_NORMALIZED_IMAGE_MAX_DIMENSION = 8192 +/** Default encoded-byte target for one stored normalized image. */ export const DEFAULT_NORMALIZED_IMAGE_MAX_BYTES = 4 * 1024 * 1024 /** Conservative default number of simultaneous native image transformations per store. */ export const DEFAULT_IMAGE_COMPRESSION_CONCURRENCY = 2 @@ -61,9 +65,14 @@ export interface Config { maxImagePixels?: number /** Maximum intrinsic width and maximum intrinsic height accepted for one submitted image. Default: 8192px. */ maxImageDimension?: number - /** Long-edge pixel cap of the stored provider-independent normalized image. */ + /** Total-pixel budget of the stored provider-independent normalized image. */ + normalizedImageMaxPixels?: number + /** Long-edge pixel cap of the stored provider-independent normalized image, applied after the total-pixel budget. */ normalizedImageMaxDimension?: number - /** Encoded-byte safety cap of the stored provider-independent normalized image. */ + /** + * Encoded-byte target of the stored provider-independent normalized image; + * the smallest quality-ladder output is kept when no quality fits. + */ normalizedImageMaxBytes?: number /** Maximum simultaneous normalization or request-image transformations in this service instance. */ imageCompressionConcurrency?: number @@ -139,6 +148,7 @@ export class LocalAttachmentStore extends AttachmentStore { maxMessageImageBytes: z.number().step(1).min(1).default(DEFAULT_MAX_MESSAGE_IMAGE_BYTES), maxImagePixels: z.number().step(1).min(1).default(DEFAULT_MAX_IMAGE_PIXELS), maxImageDimension: z.number().step(1).min(1).default(DEFAULT_MAX_IMAGE_DIMENSION), + normalizedImageMaxPixels: z.number().step(1).min(1).default(DEFAULT_NORMALIZED_IMAGE_MAX_PIXELS), normalizedImageMaxDimension: z.number().step(1).min(1).default(DEFAULT_NORMALIZED_IMAGE_MAX_DIMENSION), normalizedImageMaxBytes: z.number().step(1).min(1).default(DEFAULT_NORMALIZED_IMAGE_MAX_BYTES), imageCompressionConcurrency: z.number().step(1).min(1).max(MAX_IMAGE_COMPRESSION_CONCURRENCY) @@ -167,6 +177,7 @@ export class LocalAttachmentStore extends AttachmentStore { mediaTypes: Object.freeze(['image/png', 'image/jpeg', 'image/webp', 'image/gif'] as const), }) this.normalizationPolicy = Object.freeze({ + maxPixels: config.normalizedImageMaxPixels ?? DEFAULT_NORMALIZED_IMAGE_MAX_PIXELS, maxDimension: config.normalizedImageMaxDimension ?? DEFAULT_NORMALIZED_IMAGE_MAX_DIMENSION, maxBytes: config.normalizedImageMaxBytes ?? DEFAULT_NORMALIZED_IMAGE_MAX_BYTES, }) diff --git a/packages/attachment/attachment-local/src/normalization.ts b/packages/attachment/attachment-local/src/normalization.ts index e9ecd8d3e7..22db15b740 100644 --- a/packages/attachment/attachment-local/src/normalization.ts +++ b/packages/attachment/attachment-local/src/normalization.ts @@ -3,15 +3,18 @@ import sharp, { type Sharp } from 'sharp' import { AttachmentError } from '@deepseek-ai/dsh-attachment' import type { ImageMediaType } from '@deepseek-ai/dsh-attachment' -import { encodeFirstWithinLimit, isExhaustedEncoding } from './encoding.ts' +import { encodeFirstWithinLimit, encodingLadder, isExhaustedEncoding } from './encoding.ts' +import { requestImageDimensions } from './request-image.ts' import { detectImage, encodedAlphaIsCompatible } from './image.ts' import type { DetectedImage } from './image.ts' /** Deployment-resolved policy for the persisted normalized attachment. */ export interface NormalizationPolicy { - /** Long-edge cap in pixels; larger sources are downscaled proportionally. */ + /** Total-pixel budget; larger sources are downscaled proportionally. */ + maxPixels: number + /** Long-edge cap in pixels applied after the total-pixel budget, bounding extreme aspect ratios. */ maxDimension: number - /** Independent safety cap for encoded normalized image bytes. */ + /** Encoded-byte target for the quality ladder; the smallest ladder output is kept when no quality fits. */ maxBytes: number } @@ -23,27 +26,6 @@ export interface NormalizedImage { height: number } -const NORMALIZATION_QUALITIES = [85, 80, 75] as const -const LOW_COLOUR_SAMPLE_EDGE = 128 -const LOW_COLOUR_LIMIT = 256 -const MIN_SCALE_STEP = 0.9 - -/** Encode one prepared pipeline and report exact output facts. */ -async function encode( - pipeline: Sharp, - mediaType: 'image/png' | 'image/jpeg' | 'image/webp', - quality?: number, - palette = true, -): Promise { - const encoded = mediaType === 'image/png' - ? pipeline.png({ compressionLevel: 9, palette }) - : mediaType === 'image/webp' - ? pipeline.webp({ quality }) - : pipeline.jpeg({ quality }) - const { data, info } = await encoded.toBuffer({ resolveWithObject: true }) - return { data: new Uint8Array(data), mediaType, width: info.width, height: info.height } -} - /** * Whether bytes already satisfy the normalization requirements. * @param detected - fully decoded source facts. @@ -62,35 +44,10 @@ export function canPassThroughNormalization( && detected.depth === 'uchar' && detected.space === 'srgb' && bytes <= policy.maxBytes + && detected.width * detected.height <= policy.maxPixels && Math.max(detected.width, detected.height) <= policy.maxDimension } -/** - * Classify a bounded pixel sample without assuming that a PNG source is a screenshot. - * @param pipeline - oriented sRGB source pipeline before output resizing. - * @returns whether the nearest-neighbour sample stays within the low-color threshold. - */ -export async function hasLowColourCount(pipeline: Sharp): Promise { - const { data, info } = await pipeline.clone().resize({ - width: LOW_COLOUR_SAMPLE_EDGE, - height: LOW_COLOUR_SAMPLE_EDGE, - fit: 'inside', - withoutEnlargement: true, - kernel: sharp.kernel.nearest, - fastShrinkOnLoad: false, - }).raw().toBuffer({ resolveWithObject: true }) - const colours = new Set() - for (let offset = 0; offset < data.length; offset += info.channels) { - const red = data.readUInt8(offset) - const green = data.readUInt8(offset + 1) - const blue = data.readUInt8(offset + 2) - const alpha = info.channels === 4 ? data.readUInt8(offset + 3) : 255 - colours.add(((red >> 3) << 15) | ((green >> 3) << 10) | ((blue >> 3) << 5) | (alpha >> 3)) - if (colours.size > LOW_COLOUR_LIMIT) return false - } - return true -} - /** Assert that a normalized output is an 8-bit sRGB/sRGBA single-frame image with matching facts. */ async function verifyNormalizedImage( image: NormalizedImage, @@ -121,41 +78,24 @@ function preparedPipeline(data: Uint8Array, width: number, height: number): Shar .resize({ width, height, fit: 'inside', withoutEnlargement: true }) } -/** Dimensions after the long edge is capped without changing aspect ratio. */ -function initialDimensions(detected: DetectedImage, maxDimension: number): { width: number; height: number } { - const scale = Math.min(1, maxDimension / Math.max(detected.width, detected.height)) +/** Dimensions under the total-pixel budget, then the long-edge cap, without changing aspect ratio. */ +function initialDimensions(detected: DetectedImage, policy: NormalizationPolicy): { width: number; height: number } { + const budgeted = requestImageDimensions(detected.width, detected.height, policy.maxPixels) + const longEdge = Math.max(budgeted.width, budgeted.height) + if (longEdge <= policy.maxDimension) return budgeted + const scale = policy.maxDimension / longEdge return { - width: Math.max(1, Math.round(detected.width * scale)), - height: Math.max(1, Math.round(detected.height * scale)), + width: Math.max(1, Math.floor(budgeted.width * scale)), + height: Math.max(1, Math.floor(budgeted.height * scale)), } } -/** Lazy encoding order for one size, separated by sampled colour complexity and alpha. */ -function encodingAttemptsAtSize( - data: Uint8Array, - width: number, - height: number, - hasAlpha: boolean, - lowColour: boolean, -): Array<() => Promise> { - const prepared = preparedPipeline(data, width, height) - const webp = NORMALIZATION_QUALITIES.map(quality => ( - () => encode(prepared.clone(), 'image/webp', quality) - )) - if (lowColour) { - return [() => encode(prepared.clone(), 'image/png', undefined, !hasAlpha), ...webp] - } - if (hasAlpha) return webp - return NORMALIZATION_QUALITIES.map(quality => ( - () => encode(prepared.clone(), 'image/jpeg', quality) - )) -} - /** * Produce the persisted provider-independent normalized version of one fully decoded source. * The source is passed through only when it is already clean, single-frame, 8-bit sRGB/sRGBA, - * and inside both normalization limits. Re-encoding never removes transparency. After the fixed - * quality floor is reached, dimensions continue shrinking until the independent byte cap holds. + * and inside every normalization limit. Re-encoding never removes transparency. When every + * ladder quality exceeds the byte target, the smallest ladder output is kept; provider byte + * caps stay enforced at the route that transmits the bytes. * @param data - complete admitted source bytes. * @param detected - fully decoded source facts. * @param policy - resolved independent normalization limits. @@ -170,27 +110,13 @@ export async function normalizeImage( return { data, mediaType: detected.mediaType, width: detected.width, height: detected.height } } try { - let { width, height } = initialDimensions(detected, policy.maxDimension) - const classificationPipeline = sharp(data, { failOn: 'error', limitInputPixels: false }) - .rotate() - .toColourspace('srgb') - const lowColour = await hasLowColourCount(classificationPipeline) - for (;;) { - const encoded = await encodeFirstWithinLimit( - encodingAttemptsAtSize(data, width, height, detected.hasAlpha, lowColour), - policy.maxBytes, - ) - if (!isExhaustedEncoding(encoded)) { - return await verifyNormalizedImage(encoded, detected.mediaType === 'image/gif' ? undefined : detected.hasAlpha) - } - if (width === 1 && height === 1) break - const sizeScale = Math.sqrt(policy.maxBytes / encoded.smallest.data.byteLength) * 0.95 - const scale = Math.min(MIN_SCALE_STEP, sizeScale) - const nextWidth = Math.max(1, Math.floor(width * scale)) - const nextHeight = Math.max(1, Math.floor(height * scale)) - width = nextWidth - height = nextHeight - } + const { width, height } = initialDimensions(detected, policy) + const encoded = await encodeFirstWithinLimit( + encodingLadder(preparedPipeline(data, width, height), detected.hasAlpha), + policy.maxBytes, + ) + const chosen = isExhaustedEncoding(encoded) ? encoded.smallest : encoded + return await verifyNormalizedImage(chosen, detected.mediaType === 'image/gif' ? undefined : detected.hasAlpha) } catch (error) { if (error instanceof AttachmentError) throw error const source = detected.mediaType === 'image/png' && detected.depth !== 'uchar' @@ -202,5 +128,4 @@ export async function normalizeImage( { cause: error }, ) } - throw new AttachmentError('Image cannot be encoded within the configured normalized-image byte cap.', 'IMAGE_TOO_LARGE') } diff --git a/packages/attachment/attachment-local/src/request-image.ts b/packages/attachment/attachment-local/src/request-image.ts index 66c427480b..598e79a893 100644 --- a/packages/attachment/attachment-local/src/request-image.ts +++ b/packages/attachment/attachment-local/src/request-image.ts @@ -12,14 +12,17 @@ import type { RequestImageAttachment, StoredImageAttachment, } from '@deepseek-ai/dsh-attachment' -import { hasLowColourCount } from './normalization.ts' -import { encodeFirstWithinLimit, isExhaustedEncoding } from './encoding.ts' +import { + IMAGE_ENCODING_QUALITIES, + WEBP_ENCODING_EFFORT, + encodeFirstWithinLimit, + encodingLadder, + isExhaustedEncoding, +} from './encoding.ts' import { detectImage, encodedAlphaIsCompatible, probeImage } from './image.ts' /** Transform version included in every cache and upload-index identity. */ -export const REQUEST_IMAGE_TRANSFORM_VERSION = 'request-image-v4' -/** DeepSeek request versions normally fit at these two preferred qualities. */ -export const REQUEST_IMAGE_QUALITIES = [85, 80] as const +export const REQUEST_IMAGE_TRANSFORM_VERSION = 'request-image-v5' interface EncodedRequestImage { data: Uint8Array @@ -87,10 +90,10 @@ function descriptor(attachment: ImageAttachmentRef, policy: ImageRequestPolicy): routePixelBudget: policy.maxPixels, encodedByteBudget: policy.maxBytes, encoding: { - png: { compressionLevel: 9, palette: 'opaque-only' }, - webpQualities: REQUEST_IMAGE_QUALITIES, - jpegQualities: REQUEST_IMAGE_QUALITIES, - order: ['low-colour:png-webp', 'alpha:webp', 'opaque:jpeg'], + webpQualities: IMAGE_ENCODING_QUALITIES, + webpEffort: WEBP_ENCODING_EFFORT, + jpegQualities: IMAGE_ENCODING_QUALITIES, + order: ['alpha:webp', 'opaque:jpeg'], colourspace: 'srgb', }, }) @@ -118,45 +121,12 @@ function sourcePipeline(attachment: StoredImageAttachment): Sharp { return sharp(attachment.data, { failOn: 'error', limitInputPixels: false }).toColourspace('srgb') } -async function encoded( - image: Sharp, - mediaType: 'image/png' | 'image/jpeg' | 'image/webp', - quality?: number, - palette = true, -): Promise { - const output = mediaType === 'image/png' - ? image.png({ compressionLevel: 9, palette }) - : mediaType === 'image/webp' - ? image.webp({ quality }) - : image.jpeg({ quality }) - const { data, info } = await output.toBuffer({ resolveWithObject: true }) - return { data: new Uint8Array(data), mediaType, width: info.width, height: info.height } -} - -function encodingAttempts( - attachment: StoredImageAttachment, - width: number, - height: number, - hasAlpha: boolean, - lowColour: boolean, -): Array<() => Promise> { - const prepared = pipeline(attachment, width, height) - const webp = REQUEST_IMAGE_QUALITIES.map(quality => ( - () => encoded(prepared.clone(), 'image/webp', quality) - )) - if (lowColour) return [() => encoded(prepared.clone(), 'image/png', undefined, !hasAlpha), ...webp] - if (hasAlpha) return webp - return REQUEST_IMAGE_QUALITIES.map(quality => ( - () => encoded(prepared.clone(), 'image/jpeg', quality) - )) -} - async function createRequestImage( attachment: StoredImageAttachment, policy: ImageRequestPolicy, hasAlpha: boolean, ): Promise { - let dimensions = requestImageDimensions(attachment.ref.width, attachment.ref.height, policy.maxPixels) + const dimensions = requestImageDimensions(attachment.ref.width, attachment.ref.height, policy.maxPixels) if (dimensions.width === attachment.ref.width && dimensions.height === attachment.ref.height && attachment.data.byteLength <= policy.maxBytes) { @@ -167,21 +137,11 @@ async function createRequestImage( height: attachment.ref.height, } } - const lowColour = await hasLowColourCount(sourcePipeline(attachment)) - for (;;) { - const encodedVersion = await encodeFirstWithinLimit( - encodingAttempts(attachment, dimensions.width, dimensions.height, hasAlpha, lowColour), - policy.maxBytes, - ) - if (!isExhaustedEncoding(encodedVersion)) return encodedVersion - if (dimensions.width === 1 && dimensions.height === 1) break - const scale = Math.min(0.9, Math.sqrt(policy.maxBytes / encodedVersion.smallest.data.byteLength) * 0.95) - dimensions = { - width: Math.max(1, Math.floor(dimensions.width * scale)), - height: Math.max(1, Math.floor(dimensions.height * scale)), - } - } - throw new AttachmentError('Image cannot be encoded within the model-request byte budget.', 'IMAGE_TOO_LARGE') + const encodedVersion = await encodeFirstWithinLimit( + encodingLadder(pipeline(attachment, dimensions.width, dimensions.height), hasAlpha), + policy.maxBytes, + ) + return isExhaustedEncoding(encodedVersion) ? encodedVersion.smallest : encodedVersion } function cachePath(root: string, hash: string): string { @@ -199,7 +159,7 @@ async function readCached( const data = new Uint8Array(await readFile(path, { signal })) const detected = await probeImage(data) const maximum = requestImageDimensions(attachment.ref.width, attachment.ref.height, policy.maxPixels) - if (data.byteLength > policy.maxBytes || detected.depth !== 'uchar' || detected.space !== 'srgb' + if (detected.depth !== 'uchar' || detected.space !== 'srgb' || detected.width > maximum.width || detected.height > maximum.height || !encodedAlphaIsCompatible(expectedAlpha, detected)) return undefined return { data, mediaType: detected.mediaType, width: detected.width, height: detected.height, hasAlpha: detected.hasAlpha } diff --git a/packages/attachment/attachment-local/tests/index.spec.ts b/packages/attachment/attachment-local/tests/index.spec.ts index ef06768d55..4d9d9f350b 100644 --- a/packages/attachment/attachment-local/tests/index.spec.ts +++ b/packages/attachment/attachment-local/tests/index.spec.ts @@ -9,6 +9,7 @@ import sharp from 'sharp' import LocalAttachmentStore, { DEFAULT_NORMALIZED_IMAGE_MAX_BYTES, DEFAULT_NORMALIZED_IMAGE_MAX_DIMENSION, + DEFAULT_NORMALIZED_IMAGE_MAX_PIXELS, DEFAULT_IMAGE_COMPRESSION_CONCURRENCY, DEFAULT_MAX_IMAGE_BYTES, DEFAULT_MAX_IMAGE_DIMENSION, @@ -34,6 +35,7 @@ describe('local attachment service', () => { mediaTypes: ['image/png', 'image/jpeg', 'image/webp', 'image/gif'], }) expect(service.normalizationPolicy).toEqual({ + maxPixels: DEFAULT_NORMALIZED_IMAGE_MAX_PIXELS, maxDimension: DEFAULT_NORMALIZED_IMAGE_MAX_DIMENSION, maxBytes: DEFAULT_NORMALIZED_IMAGE_MAX_BYTES, }) @@ -136,15 +138,15 @@ describe('local attachment service', () => { it('prepares every batch member before any write', async () => { const dshHome = await mkdtemp(join(tmpdir(), 'dsh-attachment-batch-')) try { - const service = new LocalAttachmentStore(new Context(), { dshHome, normalizedImageMaxBytes: 1 }) + const service = new LocalAttachmentStore(new Context(), { dshHome }) const valid = Uint8Array.from(Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAADElEQVQImWNgZGIGAAAOAAeCcsnOAAAAAElFTkSuQmCC', 'base64', )) await expect(service.saveImages([ { data: valid, mediaType: 'image/png' }, - { data: valid, mediaType: 'image/png' }, - ])).rejects.toMatchObject({ code: 'IMAGE_TOO_LARGE' }) + { data: Uint8Array.of(1, 2, 3), mediaType: 'image/png' }, + ])).rejects.toThrow(/Unsupported or malformed image data/) expect(existsSync(service.root)).toBe(false) } finally { await rm(dshHome, { recursive: true, force: true }) diff --git a/packages/attachment/attachment-local/tests/normalization-verification.spec.ts b/packages/attachment/attachment-local/tests/normalization-verification.spec.ts new file mode 100644 index 0000000000..4a9faf6d7d --- /dev/null +++ b/packages/attachment/attachment-local/tests/normalization-verification.spec.ts @@ -0,0 +1,38 @@ +import sharp from 'sharp' +import { afterEach, describe, expect, it, vi } from 'vitest' + +const control = vi.hoisted(() => ({ mismatch: false })) + +vi.mock('../src/image.ts', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + async detectImage(data: Uint8Array): Promise>> { + const detected = await actual.detectImage(data) + return control.mismatch ? { ...detected, width: detected.width + 1 } : detected + }, + } +}) + +import { normalizeImage } from '../src/normalization.ts' +import { detectImage } from '../src/image.ts' + +afterEach(() => { + control.mismatch = false +}) + +describe('normalization verification', () => { + it('rejects a normalized output whose decoded facts disagree with the encoder result', async () => { + const data = new Uint8Array(await sharp({ + create: { width: 10, height: 6, channels: 3, background: { r: 12, g: 200, b: 64 } }, + }).png().toBuffer()) + const detected = await detectImage(data) + control.mismatch = true + + await expect(normalizeImage(data, detected, { maxPixels: 2048 * 2048, maxDimension: 5, maxBytes: 4 * 1024 * 1024 })) + .rejects.toMatchObject({ + code: 'ATTACHMENT_WRITE_FAILED', + message: 'Image normalization did not produce a single-frame 8-bit sRGB image with matching metadata.', + }) + }) +}) diff --git a/packages/attachment/attachment-local/tests/normalization.spec.ts b/packages/attachment/attachment-local/tests/normalization.spec.ts index d43ae45bcd..d60989075d 100644 --- a/packages/attachment/attachment-local/tests/normalization.spec.ts +++ b/packages/attachment/attachment-local/tests/normalization.spec.ts @@ -1,10 +1,10 @@ import { describe, expect, it } from 'vitest' import sharp from 'sharp' -import { hasLowColourCount, canPassThroughNormalization, normalizeImage } from '../src/normalization.ts' +import { canPassThroughNormalization, normalizeImage } from '../src/normalization.ts' import type { NormalizationPolicy } from '../src/normalization.ts' import { detectImage } from '../src/image.ts' -const POLICY: NormalizationPolicy = { maxDimension: 2048, maxBytes: 4 * 1024 * 1024 } +const POLICY: NormalizationPolicy = { maxPixels: 2048 * 2048, maxDimension: 8192, maxBytes: 4 * 1024 * 1024 } /** Deterministic pseudo-random RGB noise; PNG cannot compress it below raw size. */ function noisePixels(width: number, height: number): Uint8Array { @@ -34,13 +34,14 @@ async function flatImage(width: number, height: number, format: 'png' | 'jpeg' | describe('canPassThroughNormalization', () => { it('accepts an in-budget clean PNG/JPEG/WebP and refuses GIF, animation, metadata, oversized edges, and oversized bytes', () => { const clean = { animated: false, carriesMetadata: false, depth: 'uchar', space: 'srgb', hasAlpha: false } - expect(canPassThroughNormalization({ mediaType: 'image/png', width: 2048, height: 4, ...clean }, 100, POLICY)).toBe(true) + expect(canPassThroughNormalization({ mediaType: 'image/png', width: 8192, height: 4, ...clean }, 100, POLICY)).toBe(true) expect(canPassThroughNormalization({ mediaType: 'image/gif', width: 4, height: 4, ...clean }, 100, POLICY)).toBe(false) expect(canPassThroughNormalization({ mediaType: 'image/webp', width: 4, height: 4, animated: true, carriesMetadata: false, depth: 'uchar', space: 'srgb', hasAlpha: false }, 100, POLICY)).toBe(false) expect(canPassThroughNormalization({ mediaType: 'image/jpeg', width: 4, height: 4, animated: false, carriesMetadata: true, depth: 'uchar', space: 'srgb', hasAlpha: false }, 100, POLICY)).toBe(false) expect(canPassThroughNormalization({ mediaType: 'image/png', width: 4, height: 4, ...clean, depth: 'ushort' }, 100, POLICY)).toBe(false) expect(canPassThroughNormalization({ mediaType: 'image/png', width: 4, height: 4, ...clean, space: 'rgb16' }, 100, POLICY)).toBe(false) - expect(canPassThroughNormalization({ mediaType: 'image/jpeg', width: 2049, height: 4, ...clean }, 100, POLICY)).toBe(false) + expect(canPassThroughNormalization({ mediaType: 'image/jpeg', width: 2049, height: 2048, ...clean }, 100, POLICY)).toBe(false) + expect(canPassThroughNormalization({ mediaType: 'image/jpeg', width: 8193, height: 4, ...clean }, 100, POLICY)).toBe(false) expect(canPassThroughNormalization({ mediaType: 'image/webp', width: 4, height: 4, ...clean }, POLICY.maxBytes + 1, POLICY)).toBe(false) }) }) @@ -72,44 +73,48 @@ describe('normalizeImage', () => { }) }) - it('downscales an oversized PNG to the long-edge target and stays PNG', async () => { + it('downscales an oversized opaque PNG to the long-edge target as JPEG', async () => { const data = await flatImage(10, 6, 'png') const detected = await detectImage(data) - const normalized = await normalizeImage(data, detected, { maxDimension: 5, maxBytes: POLICY.maxBytes }) + const normalized = await normalizeImage(data, detected, { maxPixels: POLICY.maxPixels, maxDimension: 5, maxBytes: POLICY.maxBytes }) - expect(normalized).toMatchObject({ mediaType: 'image/png', width: 5, height: 3 }) - await expect(detectImage(normalized.data)).resolves.toMatchObject({ mediaType: 'image/png', width: 5, height: 3, animated: false, carriesMetadata: false, depth: 'uchar', space: 'srgb' }) - const again = await normalizeImage(data, detected, { maxDimension: 5, maxBytes: POLICY.maxBytes }) + expect(normalized).toMatchObject({ mediaType: 'image/jpeg', width: 5, height: 3 }) + await expect(detectImage(normalized.data)).resolves.toMatchObject({ mediaType: 'image/jpeg', width: 5, height: 3, animated: false, carriesMetadata: false, depth: 'uchar', space: 'srgb' }) + const again = await normalizeImage(data, detected, { maxPixels: POLICY.maxPixels, maxDimension: 5, maxBytes: POLICY.maxBytes }) expect(again.data).toEqual(normalized.data) }) it('re-encodes the normalized output of a resize into itself (idempotence)', async () => { const data = await flatImage(10, 6, 'png') - const first = await normalizeImage(data, await detectImage(data), { maxDimension: 5, maxBytes: POLICY.maxBytes }) + const budget = { maxPixels: POLICY.maxPixels, maxDimension: 5, maxBytes: POLICY.maxBytes } + const first = await normalizeImage(data, await detectImage(data), budget) - const second = await normalizeImage(first.data, await detectImage(first.data), { maxDimension: 5, maxBytes: POLICY.maxBytes }) + const second = await normalizeImage(first.data, await detectImage(first.data), budget) expect(second.data).toBe(first.data) }) - it('always re-encodes GIF to the PNG of its first frame', async () => { + it('always re-encodes GIF as a single still frame', async () => { const data = await flatImage(6, 4, 'gif') const detected = await detectImage(data) const normalized = await normalizeImage(data, detected, POLICY) - expect(normalized.mediaType).toBe('image/png') - await expect(detectImage(normalized.data)).resolves.toMatchObject({ mediaType: 'image/png', width: 6, height: 4, animated: false, carriesMetadata: false, depth: 'uchar', space: 'srgb' }) + // gifload always decodes to RGBA, so a GIF re-encodes on the WebP ladder. + expect(detected.hasAlpha).toBe(true) + expect(normalized.mediaType).toBe('image/webp') + await expect(detectImage(normalized.data)).resolves.toMatchObject({ width: 6, height: 4, animated: false, carriesMetadata: false, depth: 'uchar', space: 'srgb' }) }) - it('keeps a low-colour alpha source on PNG when the budget holds', async () => { + it('keeps a transparent source on the WebP ladder', async () => { const data = await flatImage(9, 5, 'webp', true) const detected = await detectImage(data) - const normalized = await normalizeImage(data, detected, { maxDimension: 4, maxBytes: POLICY.maxBytes }) + const normalized = await normalizeImage(data, detected, { maxPixels: POLICY.maxPixels, maxDimension: 4, maxBytes: POLICY.maxBytes }) - expect(normalized).toMatchObject({ mediaType: 'image/png', width: 4, height: 2 }) + expect(normalized).toMatchObject({ mediaType: 'image/webp', width: 4, height: 2 }) + await expect(detectImage(normalized.data)).resolves.toMatchObject({ hasAlpha: true }) }) it('accepts WebP output that omits an all-opaque source alpha plane', async () => { @@ -129,6 +134,7 @@ describe('normalizeImage', () => { await expect(detectImage(data)).resolves.toMatchObject({ hasAlpha: true }) const normalized = await normalizeImage(data, await detectImage(data), { + maxPixels: POLICY.maxPixels, maxDimension: 32, maxBytes: POLICY.maxBytes, }) @@ -137,7 +143,7 @@ describe('normalizeImage', () => { await expect(detectImage(normalized.data)).resolves.toMatchObject({ hasAlpha: false }) }) - it('keeps transparency when the byte cap requires another encoding and smaller dimensions', async () => { + it('keeps the smallest transparent ladder output above an unreachable byte target without shrinking', async () => { const side = 128 const pixels = new Uint8Array(side * side * 4) const noise = noisePixels(side, side) @@ -151,10 +157,12 @@ describe('normalizeImage', () => { } const data = new Uint8Array(await sharp(pixels, { raw: { width: side, height: side, channels: 4 } }).png().toBuffer()) - const normalized = await normalizeImage(data, await detectImage(data), { maxDimension: side, maxBytes: 1_024 }) + const normalized = await normalizeImage(data, await detectImage(data), { + maxPixels: POLICY.maxPixels, maxDimension: side, maxBytes: 1_024, + }) - expect(normalized.data.byteLength).toBeLessThanOrEqual(1_024) - expect(normalized.width).toBeLessThan(side) + expect(normalized.data.byteLength).toBeGreaterThan(1_024) + expect(normalized).toMatchObject({ mediaType: 'image/webp', width: side, height: side }) await expect(detectImage(normalized.data)).resolves.toMatchObject({ hasAlpha: true, depth: 'uchar', space: 'srgb' }) }) @@ -162,15 +170,12 @@ describe('normalizeImage', () => { const data = await noiseImage(64, 32, 'jpeg') const detected = await detectImage(data) - const normalized = await normalizeImage(data, detected, { maxDimension: 32, maxBytes: POLICY.maxBytes }) + const normalized = await normalizeImage(data, detected, { maxPixels: POLICY.maxPixels, maxDimension: 32, maxBytes: POLICY.maxBytes }) expect(normalized).toMatchObject({ mediaType: 'image/jpeg', width: 32, height: 16 }) }) - it('classifies a photographic PNG by pixels and uses an opaque photographic encoding', async () => { - // A smooth gradient: palette quantization dithers it into a sizable PNG - // while JPEG at quality 85 stays far smaller, so the budget between the - // two forces exactly one ladder hop. + it('re-encodes an opaque gradient PNG as JPEG within the byte target', async () => { const side = 256 const pixels = new Uint8Array(side * side * 3) for (let y = 0; y < side; y += 1) { @@ -183,7 +188,7 @@ describe('normalizeImage', () => { } const data = new Uint8Array(await sharp(pixels, { raw: { width: side, height: side, channels: 3 } }).png().toBuffer()) const detected = await detectImage(data) - const budget = { maxDimension: 128, maxBytes: POLICY.maxBytes } + const budget = { maxPixels: POLICY.maxPixels, maxDimension: 128, maxBytes: POLICY.maxBytes } const normalized = await normalizeImage(data, detected, budget) @@ -192,14 +197,15 @@ describe('normalizeImage', () => { expect(normalized.data.byteLength).toBeLessThanOrEqual(budget.maxBytes) }) - it('shrinks dimensions after the quality floor instead of refusing an oversized encoding', async () => { + it('keeps the smallest opaque ladder output above an unreachable byte target', async () => { const data = await noiseImage(64, 64, 'png') - const normalized = await normalizeImage(data, await detectImage(data), { maxDimension: 2048, maxBytes: 512 }) + const normalized = await normalizeImage(data, await detectImage(data), { + maxPixels: POLICY.maxPixels, maxDimension: 2048, maxBytes: 512, + }) - expect(normalized.data.byteLength).toBeLessThanOrEqual(512) - expect(normalized.width).toBeLessThan(64) - expect(normalized.height).toBeLessThan(64) + expect(normalized.data.byteLength).toBeGreaterThan(512) + expect(normalized).toMatchObject({ mediaType: 'image/jpeg', width: 64, height: 64 }) }) it('re-encodes an in-budget oriented JPEG, baking rotation and stripping metadata', async () => { @@ -263,86 +269,27 @@ describe('normalizeImage', () => { }) }) - it('rejects a converted normalized image whose verified alpha metadata disagrees with the source facts', async () => { - const data = await flatImage(8, 8, 'png', true) - const detected = await detectImage(data) + it('downscales by total pixels so an extreme aspect ratio keeps its short edge', async () => { + const data = await flatImage(10, 40, 'png') - await expect(normalizeImage(data, { ...detected, hasAlpha: false }, { - maxDimension: 4, - maxBytes: POLICY.maxBytes, - })).rejects.toMatchObject({ - code: 'ATTACHMENT_WRITE_FAILED', - message: 'Image normalization did not produce a single-frame 8-bit sRGB image with matching metadata.', - }) - }) -}) - -describe('hasLowColourCount', () => { - it('distinguishes photographic rasters from low-colour graphics without averaged sampling', async () => { - const side = 512 - const highFrequency = sharp(noisePixels(side, side), { raw: { width: side, height: side, channels: 3 } }) - const gradientPixels = new Uint8Array(side * side * 3) - for (let y = 0; y < side; y += 1) { - for (let x = 0; x < side; x += 1) { - const offset = (y * side + x) * 3 - gradientPixels[offset] = x & 0xff - gradientPixels[offset + 1] = y & 0xff - gradientPixels[offset + 2] = (x * 3 + y * 5) & 0xff - } - } - const ordinaryPhoto = sharp(gradientPixels, { raw: { width: side, height: side, channels: 3 } }) - const solid = sharp({ - create: { width: side, height: side, channels: 3, background: { r: 12, g: 34, b: 56 } }, - }) - const text = sharp(Buffer.from(` - - - DeepSeek 16-bit - - `)) - const transparentData = await sharp({ - create: { width: side, height: side, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } }, - }).composite([{ input: Buffer.from(` - - - - `) }]).png().toBuffer() - const transparent = sharp(transparentData) - - await expect(hasLowColourCount(highFrequency)).resolves.toBe(false) - await expect(hasLowColourCount(ordinaryPhoto)).resolves.toBe(false) - await expect(hasLowColourCount(solid)).resolves.toBe(true) - await expect(hasLowColourCount(text)).resolves.toBe(true) - await expect(hasLowColourCount(transparent)).resolves.toBe(true) - }) - - it('reads grayscale-alpha samples without treating alpha or the next pixel as RGB', async () => { - const symbols: number[] = [] - for (let first = 0; first < 32; first += 1) { - for (let second = 0; second < 32; second += 1) symbols.push(first, second) - } - const pixels = new Uint8Array(symbols.length * 2) - for (const [index, symbol] of symbols.entries()) { - pixels[index * 2] = symbol * 8 - pixels[index * 2 + 1] = symbol * 8 - } - const grayscaleAlpha = sharp(pixels, { - raw: { width: 128, height: 16, channels: 2 }, + const normalized = await normalizeImage(data, await detectImage(data), { + maxPixels: 100, maxDimension: 8192, maxBytes: POLICY.maxBytes, }) - await expect(hasLowColourCount(grayscaleAlpha)).resolves.toBe(true) + expect(normalized).toMatchObject({ mediaType: 'image/jpeg', width: 5, height: 20 }) }) - it('reads one-channel grayscale samples as equal RGB values', async () => { - const pixels = new Uint8Array(128 * 16) - for (let index = 0; index < pixels.length; index += 1) pixels[index] = index & 0xff + it('caps the long edge after the total-pixel budget', async () => { + const data = await flatImage(4, 64, 'png') - await expect(hasLowColourCount(sharp(pixels, { - raw: { width: 128, height: 16, channels: 1 }, - }))).resolves.toBe(true) + const normalized = await normalizeImage(data, await detectImage(data), { + maxPixels: 10_000, maxDimension: 16, maxBytes: POLICY.maxBytes, + }) + + expect(normalized).toMatchObject({ mediaType: 'image/jpeg', width: 1, height: 16 }) }) - it('keeps an antialiased text screenshot readable on the low-colour PNG path', async () => { + it('keeps an antialiased text screenshot readable on the JPEG ladder', async () => { const source = new Uint8Array(await sharp(Buffer.from(` @@ -351,12 +298,13 @@ describe('hasLowColourCount', () => { `)).removeAlpha().png().toBuffer()) const normalized = await normalizeImage(source, await detectImage(source), { + maxPixels: POLICY.maxPixels, maxDimension: 512, maxBytes: POLICY.maxBytes, }) const stats = await sharp(normalized.data).greyscale().stats() - expect(normalized).toMatchObject({ mediaType: 'image/png', width: 512, height: 256 }) + expect(normalized).toMatchObject({ mediaType: 'image/jpeg', width: 512, height: 256 }) expect(stats.channels[0]?.min).toBeLessThan(80) expect(stats.channels[0]?.max).toBeGreaterThan(240) }) diff --git a/packages/attachment/attachment-local/tests/request-image.spec.ts b/packages/attachment/attachment-local/tests/request-image.spec.ts index 66932b5e52..e470b4c657 100644 --- a/packages/attachment/attachment-local/tests/request-image.spec.ts +++ b/packages/attachment/attachment-local/tests/request-image.spec.ts @@ -97,12 +97,15 @@ describe('local request-image cache', () => { .rejects.toThrow('Image request maxBytes must be a positive integer') }) - it('refuses a one-pixel request that cannot meet the encoded-byte budget', async () => { + it('keeps the smallest ladder output when the encoded-byte target is unreachable', async () => { const attachments = await store() const attachment = await attachments.saveImage({ data: await image(1, 1), mediaType: 'image/png' }) - await expect(attachments.readImageRequest(attachment, { maxPixels: 1, maxBytes: 1 })) - .rejects.toMatchObject({ code: 'IMAGE_TOO_LARGE' }) + const request = await attachments.readImageRequest(attachment, { maxPixels: 1, maxBytes: 1 }) + + expect(request.mediaType).toBe('image/jpeg') + expect(request.bytes).toBeGreaterThan(1) + expect(request).toMatchObject({ width: 1, height: 1 }) }) it('regenerates invalid, oversized, incompatible, or mismatched cached variants', async () => { @@ -171,7 +174,7 @@ describe('local request-image cache', () => { expect(low.width * low.height).toBeLessThanOrEqual(512 * 512 + low.width) }) - it('classifies opaque PNG pixels and preserves alpha while enforcing the request budget', async () => { + it('routes opaque pixels to JPEG and preserves alpha on the WebP ladder', async () => { const attachments = await store() const side = 256 const photoPixels = new Uint8Array(side * side * 3) @@ -204,8 +207,9 @@ describe('local request-image cache', () => { const alphaRequest = await attachments.readImageRequest(alpha, { maxPixels: 128 * 128, maxBytes: 4_096 }) expect(photoRequest.mediaType).toBe('image/jpeg') - expect(alphaRequest.bytes).toBeLessThanOrEqual(4_096) - expect(alphaRequest.width).toBeLessThan(128) + expect(alphaRequest.mediaType).toBe('image/webp') + expect(alphaRequest.bytes).toBeGreaterThan(4_096) + expect(alphaRequest).toMatchObject({ width: 128, height: 128 }) await expect(sharp(alphaRequest.data).metadata()).resolves.toMatchObject({ hasAlpha: true, depth: 'uchar', space: 'srgb' }) }) diff --git a/packages/attachment/attachment-local/tests/store.spec.ts b/packages/attachment/attachment-local/tests/store.spec.ts index 478c3f6d50..3ff58eb2da 100644 --- a/packages/attachment/attachment-local/tests/store.spec.ts +++ b/packages/attachment/attachment-local/tests/store.spec.ts @@ -39,7 +39,7 @@ const PNG = Uint8Array.from(Buffer.from( 'base64', )) -const POLICY: NormalizationPolicy = { maxDimension: 2048, maxBytes: 1024 * 1024 } +const POLICY: NormalizationPolicy = { maxPixels: 2048 * 2048, maxDimension: 8192, maxBytes: 1024 * 1024 } const LIMITS: ImageAttachmentLimits = { maxImageBytes: 1024, @@ -157,10 +157,10 @@ describe('local attachment store', () => { const saved = await saveImageFile(storageRoot, { data: oversized, mediaType: 'image/png', name: 'big.png', - }, { ...LIMITS, maxImagePixels: 64 }, { maxDimension: 2, maxBytes: 1024 * 1024 }) + }, { ...LIMITS, maxImagePixels: 64 }, { maxPixels: POLICY.maxPixels, maxDimension: 2, maxBytes: 1024 * 1024 }) expect(saved).toMatchObject({ - mediaType: 'image/png', + mediaType: 'image/jpeg', width: 2, height: 2, name: 'big.png', diff --git a/packages/attachment/attachment/src/index.ts b/packages/attachment/attachment/src/index.ts index ccfc3a3f98..3e8add393f 100644 --- a/packages/attachment/attachment/src/index.ts +++ b/packages/attachment/attachment/src/index.ts @@ -121,7 +121,7 @@ export abstract class AttachmentStore extends Service { /** * Generate or read one deterministic model-request version from the stored normalized image. * @param ref - durable provider-independent normalized attachment reference. - * @param policy - exact route pixel and encoded-byte budget. + * @param policy - exact route pixel budget and encoded-byte target; a target no ladder quality meets yields the smallest ladder output. * @param signal - optional cancellation. * @returns request bytes and the cache/upload identity covering every transform input. */ diff --git a/packages/attachment/attachment/src/types.ts b/packages/attachment/attachment/src/types.ts index e23a7a7d4c..046444cd76 100644 --- a/packages/attachment/attachment/src/types.ts +++ b/packages/attachment/attachment/src/types.ts @@ -71,7 +71,7 @@ export interface StoredImageAttachment { export interface ImageRequestPolicy { /** Maximum width multiplied by height after aspect-preserving projection. */ maxPixels: number - /** Encoded-byte cap before base64 expansion or Files API upload. */ + /** Encoded-byte target before base64 expansion or Files API upload; the smallest quality-ladder output is kept when no quality fits. */ maxBytes: number } diff --git a/packages/boot/app-boot/README.i18n.yaml b/packages/boot/app-boot/README.i18n.yaml index bcdd303a0d..7f7c3badcf 100644 --- a/packages/boot/app-boot/README.i18n.yaml +++ b/packages/boot/app-boot/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/boot/app-boot/README.md -README.md: 9965d6d57f4ec6cd9a93650bbd0096b059fb010b -README.zh.md: 436b19e4b2a05f4462f7636fccd3911b8ed41548 +README.md: a7c6272fd96fdf24b4f21bb8b60c087af4a4dc96 +README.zh.md: 09a63765414ba76d78a7c88f3777f63eecd835f7 diff --git a/packages/boot/app-boot/README.md b/packages/boot/app-boot/README.md index 9965d6d57f..a7c6272fd9 100644 --- a/packages/boot/app-boot/README.md +++ b/packages/boot/app-boot/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Shared Loader boot glue for [`dsh`](../../../apps/cli/README.md) profiles and the [temporarily packaged Python SDK runtime](../../../python/README.md). The product launcher owns profile composition and process lifecycle; the direct-config helpers remain only for that held-back runtime until its later migration. +Shared Loader boot glue for [`dsh`](../../../apps/cli/README.md) profiles, including the CLI packaged by the [Python runtime wheel](../../../python/README.md). The product launcher owns profile composition and process lifecycle. Direct-config helpers serve lower-level embedders and tests; they do not define another supported application entrypoint. | Export | Role | |---|---| @@ -35,7 +35,7 @@ This package carries no loader hooks and no dev-mode surface. The [`dsh` app](.. ## Profiles -A profile is a directory under `$DSH_HOME/profiles/` (the Harness home resolves through [`resolveDshHome`](../../util/home-paths/README.md): `$DSH_HOME`, else `~/.dsh`) holding a `package.json` — out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list and `patchReload: live | startup` — and the user's own `cordis.patch.yml`. `live` watches the profile and home-level patch files after boot; `startup` applies every layer once. A missing value keeps the historical `live` default for custom profiles. A bundle is an npm package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; `loadProfile` resolves each `dsh.profile.bundles` name two-anchored (the dsh installation first, then the profile directory) and fails loud on a listed package without a bundle declaration. `composeEntries` applies patch layers over an empty entry list through the include's own `applyEntryPatches`, so composition, flag derivation, and config dumps cannot drift from what boots. `healProfilesModuleFallback` maintains the flat `$DSH_HOME/profiles/node_modules` directory — one symlink per package the installation's app and bundles depend on — so bare plugin names in any profile resolve through Node's ordinary parent-walk without pnpm managing in-box packages. `PROFILE_TEMPLATES` auto-initializes `web` with live reload and `headless`/`sdk`/`acp` with startup-only patches; other names fail loud until `initProfile` creates them through `dsh plugin`. `loadProfile` normalizes an exact installation-owned bundle tuple and a missing reload choice to its shipped template while preserving every explicit reload choice and every other manifest field; any extra, missing, or reordered bundle makes the list user-owned and leaves it unchanged. +A profile is a directory under `$DSH_HOME/profiles/` (the Harness home resolves through [`resolveDshHome`](../../util/home-paths/README.md): `$DSH_HOME`, else `~/.dsh`) holding a `package.json` — out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list and `patchReload: live | startup` — and the user's own `cordis.patch.yml`. `live` watches the profile and home-level patch files after boot; `startup` applies every layer once. A missing value keeps the historical `live` default for custom profiles. A bundle is an npm package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; `loadProfile` resolves each `dsh.profile.bundles` name two-anchored (the dsh installation first, then the profile directory) and fails loud on a listed package without a bundle declaration. `composeEntries` applies patch layers over an empty entry list through the include's own `applyEntryPatches`, so composition, flag derivation, and config dumps cannot drift from what boots. `healProfilesModuleFallback` maintains the flat `$DSH_HOME/profiles/node_modules` directory. Plain Node writes one symlink per package in the installation dependency closure; a pkg executable resolves available explicit exports directly from each installed manifest with Node ESM import conditions and writes real proxy packages that re-export virtual module URLs, because an operating-system symlink cannot enter pkg's `/snapshot` tree. Export targets absent from an installed package remain unavailable without blocking its other exports; malformed export maps fail startup. An executable-only or declaration-only package with no module entry produces no proxy. A complete matching generation returns without acquiring the writer lock. A missing or stale entry acquires the cross-process lock, rechecks the full generation, and repairs it without exposing partial proxies; either carrier replaces the other carrier's managed entry. Both forms let profile plugins resolve installation packages through Node's ordinary parent walk and preserve one module instance for external plugin peers. `PROFILE_TEMPLATES` auto-initializes `web` with live reload and `headless`/`sdk`/`sdk-minimal`/`acp` with startup-only patches; `sdk-minimal` lists only its standalone bundle, while the other templates retain their base-plus-mode stacks. Other names fail loud until `initProfile` creates them through `dsh plugin`. `loadProfile` normalizes an exact installation-owned bundle tuple and a missing reload choice to its shipped template while preserving every explicit reload choice and every other manifest field; any extra, missing, or reordered bundle makes the list user-owned and leaves it unchanged. User-level machine-local preferences also live in the Harness home: diff --git a/packages/boot/app-boot/README.zh.md b/packages/boot/app-boot/README.zh.md index 436b19e4b2..09a6376541 100644 --- a/packages/boot/app-boot/README.zh.md +++ b/packages/boot/app-boot/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -供 [`dsh`](../../../apps/cli/README.zh.md) profile 与[暂时打包的 Python SDK runtime](../../../python/README.zh.md) 共用的 Loader 启动粘合层。产品启动器负责 profile 组合与进程生命周期;直接配置 helper 只为暂缓迁移的 runtime 保留,直至后续迁移。 +供 [`dsh`](../../../apps/cli/README.zh.md) profile 共用的 Loader 启动粘合层,也用于 [Python 运行时 wheel](../../../python/README.zh.md)打包的 CLI。产品启动器负责 profile 组合与进程生命周期。直接配置 helper 服务于底层 embedder 与测试,不会定义另一个受支持的应用入口。 | 导出 | 职责 | |---|---| @@ -35,7 +35,7 @@ Loader 并发挂载各个条目,因此当其他环节失败时,某个界面 ## Profiles -profile 是位于 `$DSH_HOME/profiles/` 下的目录(harness home 由 [`resolveDshHome`](../../util/home-paths/README.zh.md) 解析:先取 `$DSH_HOME`,否则取 `~/.dsh`),其中包含一个 `package.json`(树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表和 `patchReload: live | startup`)和用户自己的 `cordis.patch.yml`。`live` 会在启动后监视 profile 与 home 级 patch 文件;`startup` 只应用每层一次。缺失值为自定义 profile 保留历史 `live` 默认值。组合包是在 manifest 中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;`loadProfile` 以双锚点解析每个 `dsh.profile.bundles` 名称(先从 dsh 安装目录,再从 profile 目录),列出的包若没有组合包声明则明确报错。`composeEntries` 通过 include 自己的 `applyEntryPatches` 在空条目列表之上应用各 patch 层,因此组合、标志推导和配置 dump 绝不会与实际启动内容发生偏离。`healProfilesModuleFallback` 维护扁平的 `$DSH_HOME/profiles/node_modules` 目录(安装目录的应用与各组合包依赖的每个包对应一个符号链接),使任意 profile 中的裸插件名都能经 Node 常规的逐级向上查找解析,而无需由 pnpm 管理随安装内置的包。`PROFILE_TEMPLATES` 首次使用时以实时重载初始化 `web`,以仅启动时 patch 初始化 `headless`/`sdk`/`acp`;其他名称在通过 `dsh plugin` 由 `initProfile` 创建前都会明确报错。`loadProfile` 会把安装自有的精确组合包元组和缺失的重载选择规范化为随附模板,同时保留每个显式重载选择和 manifest 中其他所有字段;组合包一旦有任何额外、缺失或重排,列表就归用户所有并保持不变。 +profile 是位于 `$DSH_HOME/profiles/` 下的目录(harness home 由 [`resolveDshHome`](../../util/home-paths/README.zh.md) 解析:先取 `$DSH_HOME`,否则取 `~/.dsh`),其中包含一个 `package.json`(树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表和 `patchReload: live | startup`)和用户自己的 `cordis.patch.yml`。`live` 会在启动后监视 profile 与 home 级 patch 文件;`startup` 只应用每层一次。缺失值为自定义 profile 保留历史 `live` 默认值。组合包是在 manifest 中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;`loadProfile` 以双锚点解析每个 `dsh.profile.bundles` 名称(先从 dsh 安装目录,再从 profile 目录),列出的包若没有组合包声明则明确报错。`composeEntries` 通过 include 自己的 `applyEntryPatches` 在空条目列表之上应用各 patch 层,因此组合、标志推导和配置 dump 绝不会与实际启动内容发生偏离。`healProfilesModuleFallback` 维护扁平的 `$DSH_HOME/profiles/node_modules` 目录。普通 Node 为安装依赖闭包中的每个包写入一个符号链接;pkg 可执行程序则直接从每个已安装 manifest 中按 Node ESM import 条件解析实际存在的显式 exports,并写入重新导出虚拟模块 URL 的真实代理包,因为操作系统符号链接无法进入 pkg 的 `/snapshot` 树。安装包中不存在的 export 目标保持不可用,但不阻塞其他 exports;格式错误的 exports map 会导致启动失败。只有可执行入口或类型声明入口而没有模块入口的包不会生成代理。完整且匹配的 generation 不会获取写入锁。缺失或过期的配置项会获取跨进程锁、重新检查完整 generation,并在不暴露半成品代理的前提下修复;两种载体都会替换另一种载体留下的受管条目。两种形式都使 profile 插件可以通过 Node 常规的逐级向上查找解析安装包,并让外部插件 peer 共用一个模块实例。`PROFILE_TEMPLATES` 首次使用时以实时重载初始化 `web`,以仅启动时 patch 初始化 `headless`/`sdk`/`sdk-minimal`/`acp`;`sdk-minimal` 只列出自己的独立组合包,其他模板保留 base 加模式层的组合。其他名称在通过 `dsh plugin` 由 `initProfile` 创建前都会明确报错。`loadProfile` 会把安装自有的精确组合包元组和缺失的重载选择规范化为随附模板,同时保留每个显式重载选择和 manifest 中其他所有字段;组合包一旦有任何额外、缺失或重排,列表就归用户所有并保持不变。 用户级的机器本地偏好同样位于 harness home 中: diff --git a/packages/boot/app-boot/package.json b/packages/boot/app-boot/package.json index 0b4b1d7a74..d33cae8870 100644 --- a/packages/boot/app-boot/package.json +++ b/packages/boot/app-boot/package.json @@ -32,7 +32,9 @@ ], "license": "MIT", "dependencies": { - "js-yaml": "^4.2.0" + "@deepseek-ai/dsh-atomic-write": "workspace:^", + "js-yaml": "^4.2.0", + "resolve.exports": "^2.0.3" }, "peerDependencies": { "@deepseek-ai/cordis-plugin-group": "workspace:^", diff --git a/packages/boot/app-boot/src/index.ts b/packages/boot/app-boot/src/index.ts index ecfe695f34..9a1d05ffbf 100644 --- a/packages/boot/app-boot/src/index.ts +++ b/packages/boot/app-boot/src/index.ts @@ -1,5 +1,5 @@ /** - * Shared boot glue for `dsh` profiles and the temporarily packaged Python SDK runtime: load the gitignored + * Shared boot glue for `dsh` profiles, including the CLI packaged by the Python runtime wheel: load the gitignored * `.env`, install the fail-loud Loader guards, resolve the config path (snapshot-aware), load the * optional user patch layers from the Harness home (`~/.dsh`), expose its path resolver to * config expressions, and drive the Cordis Loader against a leaf `cordis.yml` until the tree settles. @@ -811,7 +811,9 @@ export async function boot( // original activation error instead of only the wrap chain. let deepest: unknown = cause while (deepest instanceof Error && deepest.cause !== undefined) deepest = deepest.cause - const stack = deepest instanceof Error && deepest !== cause ? `\n${deepest.stack ?? deepest.message}` : '' + const stack = deepest instanceof AggregateError + ? `\n${deepest.stack ?? deepest.message}\n${deepest.errors.map(formatActivationError).join('\n')}` + : deepest instanceof Error && deepest !== cause ? `\n${deepest.stack ?? deepest.message}` : '' throw new Error(`${binName}: ${stage}: ${detail}${stack}`, { cause }) } } diff --git a/packages/boot/app-boot/src/profile.ts b/packages/boot/app-boot/src/profile.ts index cbd6074ff9..b99dca29c7 100644 --- a/packages/boot/app-boot/src/profile.ts +++ b/packages/boot/app-boot/src/profile.ts @@ -24,12 +24,15 @@ import { createRequire } from 'node:module' import { - existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, symlinkSync, unlinkSync, writeFileSync, + existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync, } from 'node:fs' -import { basename, dirname, join } from 'node:path' +import { basename, dirname, join, relative, resolve } from 'node:path' +import { pathToFileURL } from 'node:url' +import { withFileLock } from '@deepseek-ai/dsh-atomic-write' import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader' import { applyEntryPatches, type PatchOptions } from '@deepseek-ai/cordis-plugin-include' import { resolveDshHome } from '@deepseek-ai/dsh-home-paths' +import { resolve as resolvePackage, type Package as ResolvePackageManifest } from 'resolve.exports' import { loadOverlayPatches } from './index.ts' /** Directory under the Harness home holding every profile. */ @@ -143,6 +146,10 @@ export const PROFILE_TEMPLATES: Record = { bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-sdk-app'], patchReload: 'startup', }, + 'sdk-minimal': { + bundles: ['@deepseek-ai/dsh-sdk-minimal'], + patchReload: 'startup', + }, } /** Installation-owned bundle tuples normalized to the shipped template. */ @@ -204,7 +211,16 @@ export function initProfile( if (!existsSync(workspacePath)) writeFileSync(workspacePath, PROFILE_PNPM_WORKSPACE) } -/** Ensure `link` is a symlink to `target`, replacing a wrong or dangling link; a real directory throws. */ +function readModuleProxyRecord(link: string): ModuleProxyRecord | undefined { + try { + return JSON.parse(readFileSync(join(link, 'package.json'), 'utf8')) as ModuleProxyRecord + } catch { + // Missing or invalid metadata is not managed state; callers reject it. + return undefined + } +} + +/** Ensure `link` is a symlink to `target`, replacing a wrong link or a dsh-managed packaged proxy. */ function ensureSymlink(link: string, target: string): void { let stat try { @@ -216,12 +232,19 @@ function ensureSymlink(link: string, target: string): void { } if (stat !== undefined) { if (!stat.isSymbolicLink()) { - throw new Error(`dsh: ${link} exists and is not a symlink; remove it so dsh can manage the installation fallback`) + const existing = stat.isDirectory() ? readModuleProxyRecord(link) : undefined + if (existing?.dsh?.moduleFallback?.targets === undefined) { + throw new Error(`dsh: ${link} exists and is not a symlink or dsh-managed module proxy; remove it so dsh can manage the installation fallback`) + } + rmSync(link, { recursive: true }) + stat = undefined + } + if (stat !== undefined) { + if (readlinkSync(link) === target) return + // unlink deletes the reparse point itself on Windows too; rmSync treats a + // junction as a directory and throws EISDIR unless recursive. + unlinkSync(link) } - if (readlinkSync(link) === target) return - // unlink deletes the reparse point itself on Windows too; rmSync treats a - // junction as a directory and throws EISDIR unless recursive. - unlinkSync(link) } try { symlinkSync(target, link, 'junction') @@ -238,29 +261,162 @@ function ensureSymlink(link: string, target: string): void { } } +interface ModuleProxyManifest { + name: string + version: string + private: true + type: 'module' + exports: Record + dsh: { moduleFallback: { targets: Record } } +} + +interface ModuleProxyRecord { + version?: unknown + dsh?: { moduleFallback?: { targets?: unknown } } +} + +/** Return whether the process reads application modules from pkg's virtual filesystem. */ +function isPackagedExecutable(): boolean { + return (process as NodeJS.Process & { pkg?: unknown }).pkg !== undefined +} + +/** Resolve one available explicit package export under Node ESM import conditions. */ +function packageEntryFromPackage( + packageName: string, + packageDir: string, + declared: ResolvePackageManifest['exports'], + subpath: string, +): string | undefined { + let candidates: string[] | void + try { + candidates = resolvePackage({ name: packageName, exports: declared }, subpath) + } catch (error) { + if ((error as Error).message.startsWith('No known conditions for ')) return undefined + const specifier = subpath === '.' ? packageName : packageName + subpath.slice(1) + throw new Error(`dsh: cannot resolve ESM export ${specifier} from installed package ${packageName}`, { cause: error }) + } + for (const candidate of candidates ?? []) { + const target = candidate + const entry = resolve(packageDir, target) + const relativeEntry = relative(packageDir, entry) + if (!target.startsWith('./') || /^\.\.(?:[\\/]|$)/u.test(relativeEntry)) { + throw new Error(`dsh: installed package ${packageName} export ${subpath} resolves outside its package: ${target}`) + } + if (existsSync(entry) && statSync(entry).isFile()) return pathToFileURL(entry).href + } + return undefined +} + +/** Resolve every explicit ESM runtime export that an out-of-tree plugin can import. */ +function packageProxySource( + packageName: string, + packageDir: string, +): { version: string; targets: Record } { + const manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8')) as { + bin?: unknown + exports?: unknown + main?: unknown + types?: unknown + typings?: unknown + version?: unknown + } + if (typeof manifest.version !== 'string' || manifest.version.length === 0) { + throw new Error(`dsh: installed package ${packageName} must declare a non-empty version`) + } + const declared = manifest.exports + if (declared === undefined) { + const main = typeof manifest.main === 'string' && manifest.main.length > 0 ? manifest.main : undefined + const entry = join(packageDir, main ?? 'index') + try { + const resolved = createRequire(join(packageDir, 'package.json')).resolve(entry) + return { version: manifest.version, targets: { '.': pathToFileURL(resolved).href } } + } catch (error) { + if (main === undefined + && (manifest.bin !== undefined || manifest.types !== undefined || manifest.typings !== undefined)) { + return { version: manifest.version, targets: {} } + } + throw new Error(`dsh: installed package ${packageName} main entry is missing at ${entry}`, { cause: error }) + } + } + const subpaths = declared !== null && typeof declared === 'object' && !Array.isArray(declared) + && Object.keys(declared).some(key => key.startsWith('.')) + ? Object.keys(declared).filter(key => key === '.' || ( + key.startsWith('./') && !key.includes('*') && !key.endsWith('/') && key !== './package.json' + )) + : ['.'] + const targets: Record = {} + for (const subpath of subpaths) { + const target = packageEntryFromPackage( + packageName, + packageDir, + declared as ResolvePackageManifest['exports'], + subpath, + ) + if (target !== undefined) targets[subpath] = target + } + return { version: manifest.version, targets } +} + /** - * Maintain the flat module fallback `$DSH_HOME/profiles/node_modules`: one - * symlink per package in the dsh app's resolvable dependency CLOSURE (BFS - * over `dependencies` from the app manifest), each resolved from its own - * real location. Node's parent-directory walk from any profile finds this - * directory after the profile's own `node_modules`, so every in-box plugin - * resolves without pnpm ever managing it — the exact "bundles come from the - * installation" contract. The closure (not just direct dependencies) is - * required for out-of-tree plugins: their peer dependencies name Service - * Definition packages (`dsh-compaction`, `dsh-invariants`, ...) that the app - * reaches only through its Service Provider packages. Symlinked packages - * resolve their own dependencies from their real directories (Node's default - * symlink-following), so each package needs only its one flat link. - * Idempotent: correct links are kept and moved installations are - * re-pointed; a stale link to a vanished package stays until its name is - * reused (dangling links are invisible to resolution). - * @param installAnchor - absolute path of the dsh app's package.json. - * @param home - the Harness home; defaults to {@link resolveDshHome}. + * Materialize a real package proxy whose exports retain pkg's virtual module + * URL. Files outside the executable cannot traverse a symlink into + * `/snapshot`, while an ESM re-export can import that URL and preserves the + * executable's single module instance for out-of-tree plugin peers. */ -export function healProfilesModuleFallback(installAnchor: string, home: string = resolveDshHome()): void { - const profilesDir = join(home, PROFILES_DIR) - const modulesDir = join(profilesDir, 'node_modules') - mkdirSync(modulesDir, { recursive: true }) +function ensureModuleProxy( + link: string, + packageName: string, + version: string, + targets: Record, +): void { + const proxyExports = Object.fromEntries( + Object.keys(targets).map((subpath, index) => [subpath, `./entry-${index}.js`]), + ) + const manifest: ModuleProxyManifest = { + name: packageName, + version, + private: true, + type: 'module', + exports: proxyExports, + dsh: { moduleFallback: { targets } }, + } + let stat + try { + stat = lstatSync(link) + } catch { + stat = undefined + } + if (stat?.isSymbolicLink()) { + unlinkSync(link) + stat = undefined + } + if (stat !== undefined) { + const existing = readModuleProxyRecord(link) + if (existing?.dsh?.moduleFallback?.targets === undefined) { + throw new Error(`dsh: ${link} exists and is not a dsh-managed module proxy; remove it so dsh can manage the installation fallback`) + } + if (existing.version === version + && JSON.stringify(existing.dsh.moduleFallback.targets) === JSON.stringify(targets) + && Object.keys(targets).every((_, index) => existsSync(join(link, `entry-${index}.js`)))) return + rmSync(link, { recursive: true }) + } + mkdirSync(link, { recursive: true }) + writeFileSync(join(link, 'package.json'), JSON.stringify(manifest, undefined, 2) + '\n') + for (const [index, target] of Object.values(targets).entries()) { + const specifier = JSON.stringify(target) + writeFileSync( + join(link, `entry-${index}.js`), + `export * from ${specifier}\nimport * as target from ${specifier}\nexport default target.default\n`, + ) + } +} + +type ModuleFallbackEntry = + | { kind: 'symlink'; packageName: string; packageDir: string } + | { kind: 'proxy'; packageName: string; version: string; targets: Record } + +/** Resolve the installation generation that every profile must find through the fallback directory. */ +function resolveModuleFallbackEntries(installAnchor: string): ModuleFallbackEntry[] { const appManifest = JSON.parse(readFileSync(installAnchor, 'utf8')) as ProfileManifest const links = new Map() /* v8 ignore next -- a real app manifest always declares its name */ @@ -284,10 +440,89 @@ export function healProfilesModuleFallback(installAnchor: string, home: string = queue.push({ anchor: manifestPath, manifest: JSON.parse(readFileSync(manifestPath, 'utf8')) as ProfileManifest }) } } - for (const [packageName, target] of links) { - const link = join(modulesDir, packageName) + if (!isPackagedExecutable()) { + return [...links].map(([packageName, packageDir]) => ({ kind: 'symlink', packageName, packageDir })) + } + return [...links].flatMap(([packageName, packageDir]) => { + const source = packageProxySource(packageName, packageDir) + return Object.keys(source.targets).length === 0 + ? [] + : [{ kind: 'proxy' as const, packageName, version: source.version, targets: source.targets }] + }) +} + +/** Return whether one existing fallback entry already matches its resolved installation generation. */ +function moduleFallbackEntryCurrent(modulesDir: string, entry: ModuleFallbackEntry): boolean { + const link = join(modulesDir, entry.packageName) + try { + const stat = lstatSync(link) + if (entry.kind === 'symlink') { + return stat.isSymbolicLink() && readlinkSync(link) === entry.packageDir + } + if (!stat.isDirectory()) return false + const existing = readModuleProxyRecord(link) + return existing?.version === entry.version + && JSON.stringify(existing.dsh?.moduleFallback?.targets) === JSON.stringify(entry.targets) + && Object.keys(entry.targets).every((_, index) => existsSync(join(link, `entry-${index}.js`))) + } catch { + return false + } +} + +/** Return whether every required fallback entry is already ready for this installation. */ +function moduleFallbackCurrent(modulesDir: string, entries: readonly ModuleFallbackEntry[]): boolean { + return entries.every(entry => moduleFallbackEntryCurrent(modulesDir, entry)) +} + +/** + * Maintain the flat module fallback `$DSH_HOME/profiles/node_modules`: one + * entry per package in the dsh app's resolvable dependency CLOSURE (BFS + * over `dependencies` from the app manifest), each resolved from its own + * installation location. Plain Node uses symlinks. A pkg executable resolves + * exports under ESM import conditions and writes small proxy packages because + * the host filesystem cannot follow a symlink into pkg's virtual `/snapshot` + * tree; the proxy re-exports the virtual URL, preserving the executable's + * single module instance. A complete matching generation returns without a + * writer lock; actual repairs acquire and recheck one cross-process lock so + * partial proxies and carrier transitions remain serialized. Node's + * parent-directory walk from any profile finds this + * directory after the profile's own `node_modules`, so every in-box plugin + * resolves without pnpm ever managing it — the exact "bundles come from the + * installation" contract. The closure (not just direct dependencies) is + * required for out-of-tree plugins: their peer dependencies name Service + * Definition packages (`dsh-compaction`, `dsh-invariants`, ...) that the app + * reaches only through its Service Provider packages. Both a symlink target + * and a proxy's virtual target resolve transitive imports from the original + * package directory, so each package needs one flat fallback entry. + * Idempotent: correct entries are kept and changed installation targets are + * rewritten; under plain Node, a stale dangling link stays until its name is + * reused because resolution cannot discover it. + * @param installAnchor - absolute path of the dsh app's package.json. + * @param home - the Harness home; defaults to {@link resolveDshHome}. + * @returns settlement after current-state validation or a locked repair. + */ +export async function healProfilesModuleFallback(installAnchor: string, home: string = resolveDshHome()): Promise { + const profilesDir = join(home, PROFILES_DIR) + const modulesDir = join(profilesDir, 'node_modules') + mkdirSync(modulesDir, { recursive: true }) + const entries = resolveModuleFallbackEntries(installAnchor) + if (moduleFallbackCurrent(modulesDir, entries)) return + await withFileLock(modulesDir, () => { + if (!moduleFallbackCurrent(modulesDir, entries)) healProfilesModuleFallbackLocked(entries, modulesDir) + return Promise.resolve() + }) +} + +/** Heal one module-fallback generation while the cross-process writer lock is held. */ +function healProfilesModuleFallbackLocked(entries: readonly ModuleFallbackEntry[], modulesDir: string): void { + for (const entry of entries) { + const link = join(modulesDir, entry.packageName) mkdirSync(dirname(link), { recursive: true }) - ensureSymlink(link, target) + if (entry.kind === 'proxy') { + ensureModuleProxy(link, entry.packageName, entry.version, entry.targets) + } else { + ensureSymlink(link, entry.packageDir) + } } } diff --git a/packages/boot/app-boot/tests/app-boot.spec.ts b/packages/boot/app-boot/tests/app-boot.spec.ts index 8726895c1b..8ae1a21cd2 100644 --- a/packages/boot/app-boot/tests/app-boot.spec.ts +++ b/packages/boot/app-boot/tests/app-boot.spec.ts @@ -771,6 +771,28 @@ describe('boot', () => { ) }) + it('expands a stackless aggregate at the deepest activation cause', async () => { + const dir = tmp() + const aggregate = new AggregateError([ + new Error('first aggregate member'), + 'second aggregate member', + ], 'aggregate activation failure') + delete (aggregate as { stack?: string }).stack + try { + await boot(NAME, join(dir, 'cordis.yml'), undefined, () => { + throw new Error('wrapped aggregate failure', { cause: aggregate }) + }) + expect.fail('boot should reject the aggregate activation failure') + } catch (error) { + expect(error).toBeInstanceOf(Error) + const message = (error as Error).message + expect(message).toContain(`${NAME}: host preparation failed: wrapped aggregate failure`) + expect(message).toContain('aggregate activation failure') + expect(message).toContain('first aggregate member') + expect(message).toContain('second aggregate member') + } + }) + it('reports a pending real Loader fiber and the service unresolved in its own context', async () => { const dir = tmp() writeFileSync(join(dir, 'waiting.mjs'), 'export const inject = ["neverProvided"]\nexport function apply() {}\n') diff --git a/packages/boot/app-boot/tests/profile.spec.ts b/packages/boot/app-boot/tests/profile.spec.ts index 92265d2fdb..4e7a2c5ddf 100644 --- a/packages/boot/app-boot/tests/profile.spec.ts +++ b/packages/boot/app-boot/tests/profile.spec.ts @@ -4,9 +4,10 @@ * empty-root composition, and the installation module-fallback healing. */ -import { lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' +import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' +import { withFileLock } from '@deepseek-ai/dsh-atomic-write' import { describe, expect, it } from 'vitest' import { composeEntries, @@ -36,12 +37,18 @@ function stageInstallation(bundles: Record { bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-sdk-app'], patchReload: 'startup', }) + expect(PROFILE_TEMPLATES['sdk-minimal']).toEqual({ + bundles: ['@deepseek-ai/dsh-sdk-minimal'], + patchReload: 'startup', + }) try { loadProfile('t', 'web', anchor, home) } catch { @@ -265,7 +276,7 @@ describe('composeEntries', () => { }) describe('healProfilesModuleFallback', () => { - it('links the app and bundle dependency surface flat under profiles/node_modules', () => { + it('links the app and bundle dependency surface flat under profiles/node_modules', async () => { const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n', deps: { 'dep-of-a': '0.0.0', 'ghost-dep': '0.0.0' } }, 'plain-lib': {}, @@ -279,7 +290,7 @@ describe('healProfilesModuleFallback', () => { mkdirSync(join(modules, 'dep-of-a'), { recursive: true }) writeFileSync(join(modules, 'dep-of-a', 'package.json'), JSON.stringify({ name: 'dep-of-a', version: '0.0.0' })) const home = tmp() - healProfilesModuleFallback(anchor, home) + await healProfilesModuleFallback(anchor, home) const fallback = join(home, 'profiles', 'node_modules') // App deps, the bundle's own deps, and the bundle itself are linked; the // plain library is linked as an app dep (harmless), the app itself too. @@ -287,39 +298,403 @@ describe('healProfilesModuleFallback', () => { expect(lstatSync(join(fallback, name)).isSymbolicLink(), name).toBe(true) } // Idempotent, and a moved target is re-pointed. - healProfilesModuleFallback(anchor, home) + await healProfilesModuleFallback(anchor, home) const before = readlinkSync(join(fallback, 'dep-of-a')) expect(before).toContain('dep-of-a') }) - it('throws when a fallback entry is a real directory', () => { + it('throws when a fallback entry is a foreign file or directory', async () => { const anchor = stageInstallation({}) - const home = tmp() - mkdirSync(join(home, 'profiles', 'node_modules', 'dsh-app'), { recursive: true }) - expect(() => { healProfilesModuleFallback(anchor, home) }).toThrow('is not a symlink') + for (const kind of ['file', 'directory']) { + const home = tmp() + const entry = join(home, 'profiles', 'node_modules', 'dsh-app') + mkdirSync(join(entry, '..'), { recursive: true }) + if (kind === 'directory') mkdirSync(entry) + else writeFileSync(entry, '') + await expect(healProfilesModuleFallback(anchor, home)).rejects.toThrow('is not a symlink') + } }) - it('replaces a wrong symlink', () => { + it('replaces a wrong symlink', async () => { const anchor = stageInstallation({}) const home = tmp() const fallback = join(home, 'profiles', 'node_modules') mkdirSync(fallback, { recursive: true }) symlinkSync(tmp(), join(fallback, 'dsh-app'), 'junction') - healProfilesModuleFallback(anchor, home) + await healProfilesModuleFallback(anchor, home) expect(readlinkSync(join(fallback, 'dsh-app'))).toContain('app') }) - it('tolerates losing the concurrent-heal race to an identical link and rejects a different one', () => { - // The EEXIST arm: a second process wrote the link between our lstat miss - // and symlinkSync. Simulated by pre-creating the correct link and calling - // the internal path through a stale-lstat shim is not possible from - // outside, so probe the observable contract: healing twice concurrently - // is a no-op, and a foreign REAL directory still fails loud. + it('retains current links while repairing a missing sibling', async () => { + const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) + const home = tmp() + const fallback = join(home, 'profiles', 'node_modules') + await healProfilesModuleFallback(anchor, home) + const appTarget = readlinkSync(join(fallback, 'dsh-app')) + unlinkSync(join(fallback, 'bundle-a')) + + await healProfilesModuleFallback(anchor, home) + + expect(readlinkSync(join(fallback, 'dsh-app'))).toBe(appTarget) + expect(lstatSync(join(fallback, 'bundle-a')).isSymbolicLink()).toBe(true) + }) + + it('serializes concurrent healers and retains the identical link', async () => { const anchor = stageInstallation({}) const home = tmp() - healProfilesModuleFallback(anchor, home) - healProfilesModuleFallback(anchor, home) // second healer sees the correct link + await Promise.all([ + healProfilesModuleFallback(anchor, home), + healProfilesModuleFallback(anchor, home), + ]) const fallback = join(home, 'profiles', 'node_modules') expect(lstatSync(join(fallback, 'dsh-app')).isSymbolicLink()).toBe(true) }) + + it('does not acquire the writer lock for a complete generation', async () => { + const anchor = stageInstallation({}) + const home = tmp() + const modules = join(home, 'profiles', 'node_modules') + await healProfilesModuleFallback(anchor, home) + let releaseLock: (() => void) | undefined + let reportLock: (() => void) | undefined + const lockHeld = new Promise((resolve) => { reportLock = resolve }) + const release = new Promise((resolve) => { releaseLock = resolve }) + const holder = withFileLock(modules, async () => { + reportLock?.() + await release + }) + await lockHeld + + const healer = healProfilesModuleFallback(anchor, home) + const outcome = await Promise.race([ + healer.then(() => 'complete' as const), + new Promise<'blocked'>(resolve => setTimeout(() => { resolve('blocked') }, 100)), + ]) + releaseLock?.() + await Promise.all([holder, healer]) + expect(outcome).toBe('complete') + }) + + it('waits for the module-fallback writer lock before publishing entries', async () => { + const anchor = stageInstallation({}) + const home = tmp() + const modules = join(home, 'profiles', 'node_modules') + mkdirSync(modules, { recursive: true }) + let releaseLock: (() => void) | undefined + let reportLock: (() => void) | undefined + const lockHeld = new Promise((resolve) => { reportLock = resolve }) + const release = new Promise((resolve) => { releaseLock = resolve }) + const holder = withFileLock(modules, async () => { + reportLock?.() + await release + }) + await lockHeld + + const healer = healProfilesModuleFallback(anchor, home) + await new Promise(resolve => setTimeout(resolve, 20)) + expect(existsSync(join(modules, 'dsh-app'))).toBe(false) + releaseLock?.() + await Promise.all([holder, healer]) + expect(lstatSync(join(modules, 'dsh-app')).isSymbolicLink()).toBe(true) + }) + + it('writes real ESM proxies for a packaged executable', async () => { + const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) + const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a') + const bundleManifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record + bundleManifest.exports = { + '.': './index.js', + './feature': './feature.js', + './legacy/': './legacy/', + './types': { types: './feature.d.ts' }, + } + writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(bundleManifest)) + writeFileSync(join(bundleDir, 'feature.js'), 'export const feature = "proxied"\n') + const home = tmp() + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + try { + await healProfilesModuleFallback(anchor, home) + const fallback = join(home, 'profiles', 'node_modules') + const proxy = join(fallback, 'bundle-a') + expect(lstatSync(proxy).isDirectory()).toBe(true) + const proxyManifest = JSON.parse(readFileSync(join(proxy, 'package.json'), 'utf8')) as { + version: unknown + exports: unknown + dsh: { moduleFallback: { targets: Record } } + } + expect(proxyManifest).toMatchObject({ + version: '0.0.0', + exports: { '.': './entry-0.js', './feature': './entry-1.js' }, + }) + expect(proxyManifest.dsh.moduleFallback.targets['.']).toEqual(expect.stringContaining('/bundle-a/index.js')) + await expect(import(join(proxy, 'entry-0.js'))).resolves.toMatchObject({ packageName: 'bundle-a' }) + await expect(import(join(proxy, 'entry-1.js'))).resolves.toMatchObject({ feature: 'proxied' }) + await healProfilesModuleFallback(anchor, home) + } finally { + delete (process as NodeJS.Process & { pkg?: unknown }).pkg + } + }) + + it('resolves import-only exports from each package installation', async () => { + const anchor = stageInstallation({ + 'bundle-a': { patch: '[]\n', deps: { 'nested-esm': '0.0.0' } }, + }) + const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a') + const bundleManifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record + bundleManifest.exports = { '.': { import: './index.js' } } + writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(bundleManifest)) + const nestedDir = join(bundleDir, 'node_modules', 'nested-esm') + mkdirSync(nestedDir, { recursive: true }) + writeFileSync(join(nestedDir, 'package.json'), JSON.stringify({ + name: 'nested-esm', + version: '0.0.0', + type: 'module', + exports: { import: './index.js' }, + })) + writeFileSync(join(nestedDir, 'index.js'), 'export const nested = "proxied"\n') + const home = tmp() + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + try { + await healProfilesModuleFallback(anchor, home) + const fallback = join(home, 'profiles', 'node_modules') + await expect(import(join(fallback, 'bundle-a', 'entry-0.js'))).resolves.toMatchObject({ packageName: 'bundle-a' }) + await expect(import(join(fallback, 'nested-esm', 'entry-0.js'))).resolves.toMatchObject({ nested: 'proxied' }) + } finally { + delete (process as NodeJS.Process & { pkg?: unknown }).pkg + } + }) + + it('resolves explicit condition targets without filesystem package lookup', async () => { + const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) + const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a') + const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record + manifest.exports = { + '.': { import: './index.js', require: './index.cjs' }, + './mini': { types: './mini/index.d.ts', import: './mini/index.js', require: './mini/index.cjs' }, + './web': { types: './dist/web/web.d.ts', import: './dist/web/index.mjs', default: './dist/web/index.mjs' }, + } + writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest)) + mkdirSync(join(bundleDir, 'mini')) + writeFileSync(join(bundleDir, 'mini', 'index.js'), 'export const mini = true\n') + mkdirSync(join(bundleDir, 'dist', 'web'), { recursive: true }) + writeFileSync(join(bundleDir, 'dist', 'web', 'index.mjs'), 'export const web = true\n') + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + try { + const home = tmp() + await healProfilesModuleFallback(anchor, home) + const proxy = join(home, 'profiles', 'node_modules', 'bundle-a') + await expect(import(join(proxy, 'entry-1.js'))).resolves.toMatchObject({ mini: true }) + await expect(import(join(proxy, 'entry-2.js'))).resolves.toMatchObject({ web: true }) + } finally { + delete (process as NodeJS.Process & { pkg?: unknown }).pkg + } + }) + + it('preserves the installation path while resolving packaged exports', async () => { + const anchor = stageInstallation({}) + const appDir = join(anchor, '..') + const physical = tmp() + writeFileSync(join(physical, 'package.json'), JSON.stringify({ + name: 'linked-esm', + version: '0.0.0', + type: 'module', + exports: { import: './index.js' }, + })) + writeFileSync(join(physical, 'index.js'), 'export const linked = true\n') + symlinkSync(physical, join(appDir, 'node_modules', 'linked-esm'), 'junction') + const appManifest = JSON.parse(readFileSync(anchor, 'utf8')) as { dependencies: Record } + appManifest.dependencies['linked-esm'] = '0.0.0' + writeFileSync(anchor, JSON.stringify(appManifest)) + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + try { + const home = tmp() + await healProfilesModuleFallback(anchor, home) + const proxyManifest = JSON.parse(readFileSync( + join(home, 'profiles', 'node_modules', 'linked-esm', 'package.json'), + 'utf8', + )) as { dsh: { moduleFallback: { targets: Record } } } + expect(proxyManifest.dsh.moduleFallback.targets['.']).toContain('/app/node_modules/linked-esm/index.js') + } finally { + delete (process as NodeJS.Process & { pkg?: unknown }).pkg + } + }) + + it('uses the legacy index fallback when a package has no exports or main', async () => { + const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) + const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a') + const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record + delete manifest.main + writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest)) + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + try { + const home = tmp() + await healProfilesModuleFallback(anchor, home) + await expect(import(join(home, 'profiles', 'node_modules', 'bundle-a', 'entry-0.js'))) + .resolves.toMatchObject({ packageName: 'bundle-a' }) + } finally { + delete (process as NodeJS.Process & { pkg?: unknown }).pkg + } + }) + + it('uses Node legacy resolution for an extensionless main entry', async () => { + const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) + const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a') + const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record + manifest.main = './index' + writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest)) + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + try { + const home = tmp() + await healProfilesModuleFallback(anchor, home) + await expect(import(join(home, 'profiles', 'node_modules', 'bundle-a', 'entry-0.js'))) + .resolves.toMatchObject({ packageName: 'bundle-a' }) + } finally { + delete (process as NodeJS.Process & { pkg?: unknown }).pkg + } + }) + + it('skips executable-only and declaration-only packages without import entries', async () => { + for (const marker of ['bin', 'types', 'typings']) { + const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) + const manifest = JSON.parse(readFileSync(anchor, 'utf8')) as Record + delete manifest.main + manifest[marker] = marker === 'bin' ? { dsh: './lib/bin.js' } : './index.d.ts' + if (marker === 'types') manifest.main = '' + writeFileSync(anchor, JSON.stringify(manifest)) + rmSync(join(anchor, '..', 'index.js')) + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + try { + const home = tmp() + await healProfilesModuleFallback(anchor, home) + const fallback = join(home, 'profiles', 'node_modules') + expect(existsSync(join(fallback, 'dsh-app'))).toBe(false) + expect(existsSync(join(fallback, 'bundle-a', 'entry-0.js'))).toBe(true) + } finally { + delete (process as NodeJS.Process & { pkg?: unknown }).pkg + } + } + }) + + it('fails loud on a missing legacy main entry', async () => { + const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) + const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a') + const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record + delete manifest.main + writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest)) + rmSync(join(bundleDir, 'index.js')) + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + try { + await expect(healProfilesModuleFallback(anchor, tmp())).rejects.toThrow('main entry is missing') + } finally { + delete (process as NodeJS.Process & { pkg?: unknown }).pkg + } + }) + + it('omits unavailable ESM exports and rejects malformed export targets', async () => { + for (const mode of ['missing', 'directory', 'absent-map', 'invalid', 'escape', 'null', 'null-subpath']) { + const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) + const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a') + const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record + const target = mode === 'missing' ? './missing.js' + : mode === 'directory' ? './mini' + : mode === 'escape' ? './../outside.js' + : '../outside.js' + manifest.exports = mode === 'absent-map' ? null + : mode === 'null-subpath' ? { './bad': null } + : { '.': mode === 'null' ? null : { import: target } } + writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest)) + if (mode === 'directory') mkdirSync(join(bundleDir, 'mini')) + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + try { + const home = tmp() + if (mode === 'missing' || mode === 'directory' || mode === 'absent-map') { + await healProfilesModuleFallback(anchor, home) + expect(existsSync(join(home, 'profiles', 'node_modules', 'bundle-a'))).toBe(false) + } else { + await expect(healProfilesModuleFallback(anchor, home)).rejects.toThrow( + mode === 'null' || mode === 'null-subpath' + ? 'cannot resolve ESM export bundle-a' + : 'resolves outside its package', + ) + } + } finally { + delete (process as NodeJS.Process & { pkg?: unknown }).pkg + } + } + }) + + it('requires a package version before writing a packaged proxy', async () => { + const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) + const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a') + const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record + manifest.version = '' + writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest)) + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + try { + await expect(healProfilesModuleFallback(anchor, tmp())).rejects.toThrow( + 'installed package bundle-a must declare a non-empty version', + ) + } finally { + delete (process as NodeJS.Process & { pkg?: unknown }).pkg + } + }) + + it('replaces plain-node links and stale managed proxies in packaged mode', async () => { + const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) + const home = tmp() + await healProfilesModuleFallback(anchor, home) + const proxy = join(home, 'profiles', 'node_modules', 'bundle-a') + expect(lstatSync(proxy).isSymbolicLink()).toBe(true) + + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + try { + await healProfilesModuleFallback(anchor, home) + expect(lstatSync(proxy).isDirectory()).toBe(true) + const stale = JSON.parse(readFileSync(join(proxy, 'package.json'), 'utf8')) as { + version: string + } + stale.version = 'stale' + writeFileSync(join(proxy, 'package.json'), JSON.stringify(stale)) + await healProfilesModuleFallback(anchor, home) + expect(JSON.parse(readFileSync(join(proxy, 'package.json'), 'utf8'))).toMatchObject({ + version: '0.0.0', + }) + } finally { + delete (process as NodeJS.Process & { pkg?: unknown }).pkg + } + }) + + it('replaces a managed packaged proxy with a plain-node symlink', async () => { + const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) + const home = tmp() + const fallback = join(home, 'profiles', 'node_modules', 'bundle-a') + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + try { + await healProfilesModuleFallback(anchor, home) + expect(lstatSync(fallback).isDirectory()).toBe(true) + } finally { + delete (process as NodeJS.Process & { pkg?: unknown }).pkg + } + + await healProfilesModuleFallback(anchor, home) + expect(lstatSync(fallback).isSymbolicLink()).toBe(true) + }) + + it('rejects foreign packaged fallback directories with valid or invalid metadata', async () => { + const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + try { + for (const metadata of ['{}', '{']) { + const home = tmp() + const proxy = join(home, 'profiles', 'node_modules', 'bundle-a') + mkdirSync(proxy, { recursive: true }) + writeFileSync(join(proxy, 'package.json'), metadata) + await expect(healProfilesModuleFallback(anchor, home)).rejects.toThrow( + 'exists and is not a dsh-managed module proxy', + ) + } + } finally { + delete (process as NodeJS.Process & { pkg?: unknown }).pkg + } + }) }) diff --git a/packages/boot/app-boot/tsconfig.json b/packages/boot/app-boot/tsconfig.json index 6f0e03b8fb..c866abbc25 100644 --- a/packages/boot/app-boot/tsconfig.json +++ b/packages/boot/app-boot/tsconfig.json @@ -29,6 +29,9 @@ { "path": "../../core/system-prompt" }, + { + "path": "../../util/atomic-write" + }, { "path": "../../util/launch-environment" }, diff --git a/packages/bundle/README.i18n.yaml b/packages/bundle/README.i18n.yaml index 5f7fbd40f4..6ad818ee64 100644 --- a/packages/bundle/README.i18n.yaml +++ b/packages/bundle/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/bundle/README.md -README.md: d6b24a276fa64bb2eb80c2aad1783795e351ebc4 -README.zh.md: 36acc510cfab7979d28052ab26687dce58175155 +README.md: c0ea5be0c6f1b2457166b24a1718f6ab3aa0ffe6 +README.zh.md: 6c3520668d8f83cfbdc75652fac81f29d9bb810d diff --git a/packages/bundle/README.md b/packages/bundle/README.md index d6b24a276f..c0ea5be0c6 100644 --- a/packages/bundle/README.md +++ b/packages/bundle/README.md @@ -8,10 +8,11 @@ The manifest declaration, not this directory, defines Bundle identity. Domain pa | Package | Role | ctx key | |---|---|---| -| [`base/`](base/README.md) | The shared dsh core every profile applies first | — (patch only) | +| [`base/`](base/README.md) | The shared dsh core applied first by base-backed profiles | — (patch only) | | [`acp-app/`](acp-app/README.md) | Automation-only ACP stdio application over base | mounts the ACP bridge | | [`web-app/`](web-app/README.md) | Browser surface: web patch layer + runtime glue plugin | mounts rows | | [`headless/`](headless/README.md) | Direct one-shot task mode over base, with no Host or Web layer | mounts `headless-runner` | | [`sdk-app/`](sdk-app/README.md) | SDK stdio JSON-RPC application over base | mounts the SDK server | +| [`sdk-minimal/`](sdk-minimal/README.md) | Standalone minimal SDK application without base or Web | — (complete patch tree) | In-box bundles resolve from the dsh installation; out-of-tree bundles install into a profile through `dsh plugin --profile add `. diff --git a/packages/bundle/README.zh.md b/packages/bundle/README.zh.md index 36acc510cf..6c3520668d 100644 --- a/packages/bundle/README.zh.md +++ b/packages/bundle/README.zh.md @@ -8,10 +8,11 @@ Bundle 身份由 manifest 声明决定,而不是由本目录决定。领域包 | 包 | 职责 | ctx key | |---|---|---| -| [`base/`](base/README.zh.md) | 每个 profile 最先应用的共享 dsh 核心 | —(仅 patch) | +| [`base/`](base/README.zh.md) | 基于 base 的 profile 最先应用的共享 dsh 核心 | —(仅 patch) | | [`acp-app/`](acp-app/README.zh.md) | 运行在 base 之上的 automation-only ACP stdio 应用 | 挂载 ACP bridge | | [`web-app/`](web-app/README.zh.md) | 浏览器表层:web patch 层 + 运行时粘合插件 | 挂载多条配置行 | | [`headless/`](headless/README.zh.md) | 直接运行在 base 之上的一次性任务模式,不含 Host 或 Web 层 | 挂载 `headless-runner` | | [`sdk-app/`](sdk-app/README.zh.md) | 运行在 base 之上的 SDK stdio JSON-RPC 应用 | 挂载 SDK server | +| [`sdk-minimal/`](sdk-minimal/README.zh.md) | 不含 base 或 Web 的独立极简 SDK 应用 | 无(完整 patch 树) | 内置组合包从 dsh 安装目录解析;树外(out-of-tree)组合包通过 `dsh plugin --profile add ` 安装进 profile。 diff --git a/packages/bundle/base/README.i18n.yaml b/packages/bundle/base/README.i18n.yaml index bfcd5c6a66..0ab5140ece 100644 --- a/packages/bundle/base/README.i18n.yaml +++ b/packages/bundle/base/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/bundle/base/README.md -README.md: 74f1288b46dd20643a494acb1829dbe38c367622 -README.zh.md: dda46a89f3c2b161d7358109e4317a976eaa65c8 +README.md: 9fb1264f39aee3f3961ff4fd3c07f35b0b7cefdf +README.zh.md: 844254acc1f349a9debf38494e8d97f5f7e2d00b diff --git a/packages/bundle/base/README.md b/packages/bundle/base/README.md index 74f1288b46..9fb1264f39 100644 --- a/packages/bundle/base/README.md +++ b/packages/bundle/base/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -The shared dsh core as a profile bundle: [`cordis.patch.yml`](cordis.patch.yml) inserts every base plugin row — model adapters, the shared [`agent-default-model`](../../core/agent-default-model/README.md) selection, tools, persistence, policy, settings/credentials, telemetry, and the core spawn/fork subagent providers — over the empty profile root, as the first layer of every profile's `dsh.profile.bundles` list. The optional Codex and Claude Code providers stay outside this package and its production dependency closure; a Profile installs either [product provider Bundle](../../subagent/README.md) only when needed. The default `@deepseek-ai/dsh` production closure therefore includes neither product provider, the Claude Agent SDK, nor the Codex wrapper and platform payloads. Later bundle layers (e.g. [`dsh-web-app`](../web-app/README.md)) and the user's profile `cordis.patch.yml` override these rows by id; a patch replaces a row's whole `config`, so mode-specific values live in mode bundles, not here. The package has no runtime API; the profile composer resolves the patch through the `dsh.bundle.patch` manifest field, never through code. +The shared dsh core as a profile bundle: [`cordis.patch.yml`](cordis.patch.yml) inserts every base plugin row — model adapters, the shared [`agent-default-model`](../../core/agent-default-model/README.md) selection, tools, persistence, policy, settings/credentials, telemetry, and the core spawn/fork subagent providers — over the empty profile root, as the first layer of each base-backed profile's `dsh.profile.bundles` list. The standalone [`sdk-minimal`](../sdk-minimal/README.md) profile deliberately does not include this bundle. The optional Codex and Claude Code providers stay outside this package and its production dependency closure; a Profile installs either [product provider Bundle](../../subagent/README.md) only when needed. The default `@deepseek-ai/dsh` production closure therefore includes neither product provider, the Claude Agent SDK, nor the Codex wrapper and platform payloads. Later bundle layers (e.g. [`dsh-web-app`](../web-app/README.md)) and the user's profile `cordis.patch.yml` override these rows by id; a patch replaces a row's whole `config`, so mode-specific values live in mode bundles, not here. The package has no runtime API; the profile composer resolves the patch through the `dsh.bundle.patch` manifest field, never through code. The base module-HMR row is disabled. A profile with a tested source-module reload lifecycle enables that row explicitly; `patchReload: live` config watching is independent and uses the launcher's watch-only fallback while module HMR remains disabled. diff --git a/packages/bundle/base/README.zh.md b/packages/bundle/base/README.zh.md index dda46a89f3..844254acc1 100644 --- a/packages/bundle/base/README.zh.md +++ b/packages/bundle/base/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -以 profile 组合包形式交付的共享 dsh 核心:[`cordis.patch.yml`](cordis.patch.yml) 在空的 profile 根之上插入全部基础插件行——模型适配器、共享的 [`agent-default-model`](../../core/agent-default-model/README.zh.md) 选择、工具、持久化、策略、settings/credentials、遥测与核心 spawn/fork subagent provider——作为每个 profile 的 `dsh.profile.bundles` 列表中的第一层。可选的 Codex 与 Claude Code provider 不属于本包及其生产依赖闭包;Profile 仅在需要时安装任一[产品 provider Bundle](../../subagent/README.zh.md)。因此,默认的 `@deepseek-ai/dsh` 生产依赖闭包既不包含任一产品 provider、Claude Agent SDK,也不包含 Codex wrapper 及其平台载荷。后续的组合包层(例如 [`dsh-web-app`](../web-app/README.zh.md))和用户 profile 的 `cordis.patch.yml` 按 id 覆盖这些行;patch 会替换目标行的整个 `config`,因此模式专属的值放在各模式组合包中,而不是这里。该包没有运行时 API;profile 组合器通过 manifest(元数据清单)的 `dsh.bundle.patch` 字段解析 patch,绝不通过代码。 +以 profile 组合包形式交付的共享 dsh 核心:[`cordis.patch.yml`](cordis.patch.yml) 在空的 profile 根之上插入全部基础插件行——模型适配器、共享的 [`agent-default-model`](../../core/agent-default-model/README.zh.md) 选择、工具、持久化、策略、settings/credentials、遥测与核心 spawn/fork subagent provider——作为每个基于 base 的 profile 的 `dsh.profile.bundles` 列表中的第一层。独立的 [`sdk-minimal`](../sdk-minimal/README.zh.md) profile 刻意不包含本组合包。可选的 Codex 与 Claude Code provider 不属于本包及其生产依赖闭包;Profile 仅在需要时安装任一[产品 provider Bundle](../../subagent/README.zh.md)。因此,默认的 `@deepseek-ai/dsh` 生产依赖闭包既不包含任一产品 provider、Claude Agent SDK,也不包含 Codex wrapper 及其平台载荷。后续的组合包层(例如 [`dsh-web-app`](../web-app/README.zh.md))和用户 profile 的 `cordis.patch.yml` 按 id 覆盖这些行;patch 会替换目标行的整个 `config`,因此模式专属的值放在各模式组合包中,而不是这里。该包没有运行时 API;profile 组合器通过 manifest(元数据清单)的 `dsh.bundle.patch` 字段解析 patch,绝不通过代码。 base 的模块 HMR 配置项默认禁用。具有经过验证的源码模块重载生命周期的 profile 必须显式启用该配置项;`patchReload: live` 配置监视与之独立,在模块 HMR 保持禁用时使用启动器的仅监视 fallback。 diff --git a/packages/bundle/base/cordis.patch.yml b/packages/bundle/base/cordis.patch.yml index e7e963e59f..981e791fb4 100644 --- a/packages/bundle/base/cordis.patch.yml +++ b/packages/bundle/base/cordis.patch.yml @@ -1,4 +1,4 @@ -# The dsh-base bundle patch: the shared core of every dsh profile, applied as +# The dsh-base bundle patch: the shared core of each base-backed profile, applied as # ONE insert over the empty profile root. Later bundle patches and the user's # profile cordis.patch.yml address these rows by id, with the last write # winning per row. @@ -327,12 +327,15 @@ config: provider: spawn toolName: subagent + enableModelSelection: true backgroundMode: continuable - # Fork stays one-shot: a continuable child's `report` tool and prompt - # section precede the inherited history a fork exists to reuse; one-shot - # fork children install neither, keeping the parent's request prefix. - # See .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md. + # Fork omits model selection so provider/model stay equal to the parent and + # the inherited history remains eligible for KV Cache reuse. It stays one-shot + # because a continuable child's `report` tool and prompt section precede that + # history and invalidate the same prefix. + # See .agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.md + # and .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md. - id: tool-subagent-fork name: '@deepseek-ai/dsh-tool-subagent' config: diff --git a/packages/bundle/base/package.json b/packages/bundle/base/package.json index 2d0977a727..80f33257db 100644 --- a/packages/bundle/base/package.json +++ b/packages/bundle/base/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-base", - "description": "The shared dsh core as a profile bundle: every profile's first patch layer, inserting the base plugin rows over the empty profile root", + "description": "The shared dsh core as a profile bundle: the first patch layer of base-backed profiles, inserting core rows over the empty profile root", "version": "0.1.1-rc.2", "publishConfig": { "access": "public" diff --git a/packages/bundle/sdk-app/README.i18n.yaml b/packages/bundle/sdk-app/README.i18n.yaml index 8deaf213fa..43e0f90976 100644 --- a/packages/bundle/sdk-app/README.i18n.yaml +++ b/packages/bundle/sdk-app/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/bundle/sdk-app/README.md -README.md: 0356d6f4a99d7baef6ff7619d505392ff7f7f1d2 -README.zh.md: c70eb685954ebff42bca6c3d289ab58e46298d50 +README.md: c5022bd2096fae931fff48944bfc167280e62476 +README.zh.md: 687a19dc09679d11696a207ad17db3cf463ee901 diff --git a/packages/bundle/sdk-app/README.md b/packages/bundle/sdk-app/README.md index 0356d6f4a9..c5022bd209 100644 --- a/packages/bundle/sdk-app/README.md +++ b/packages/bundle/sdk-app/README.md @@ -2,10 +2,14 @@ English | [中文](README.zh.md) -The SDK stdio application as a `dsh` profile bundle over [`dsh-base`](../base/README.md). It inherits the base's disabled module-HMR policy; its patch sets the coding-agent persona, mounts an app-owned zero-option command provider, and starts [`dsh-sdk-jsonrpc-server`](../../sdk/server/README.md) only after that provider accepts the invocation. `dsh --profile sdk --help` therefore writes help and exits without claiming stdin or stdout. +The SDK stdio application as a `dsh` profile bundle over [`dsh-base`](../base/README.md). It inherits the base's disabled module-HMR policy; its patch sets the coding-agent persona, mounts an app-owned zero-option command provider, and starts [`dsh-sdk-jsonrpc-server`](../../sdk/server/README.md) only after that provider accepts the invocation. `dsh --profile sdk --help` therefore writes help and exits without claiming stdin or stdout. The standalone [`sdk-minimal`](../sdk-minimal/README.md) bundle reuses the same startup provider and supplies its own profile name. The startup provider binds stdin EOF to the launcher's bounded successful shutdown. SDK protocol `shutdown`, SIGINT, and SIGTERM retain their owning server or launcher paths; disposal drains the root profile tree and persistence. Stdout is reserved for newline-delimited JSON-RPC frames. The bundle disables model-generated session titles because the SDK exposes no title surface; deterministic fallback titles remain durable without an auxiliary model request. A deployment selects a different complete composition through profile bundles and patch files, not another app bin. +| Config | Default | Behavior | +|---|---|---| +| `profile` | `sdk` | Profile name rendered in command help; a bundle mounting this provider sets its own shipped profile name. | + `DSH_MAX_TOKENS_AS_SUCCESS` retains the SDK deployment mapping: unset or JSON `true` reports token-limited subagent completion as accepted, while JSON `false` reports it as an error. Provider/model and workspace cwd arrive through the SDK initialization request; the base profile owns adapters, tools, persistence, policy, settings, and credentials. ## Model Experience diff --git a/packages/bundle/sdk-app/README.zh.md b/packages/bundle/sdk-app/README.zh.md index c70eb68595..687a19dc09 100644 --- a/packages/bundle/sdk-app/README.zh.md +++ b/packages/bundle/sdk-app/README.zh.md @@ -2,10 +2,14 @@ [English](README.md) | 中文 -以 [`dsh-base`](../base/README.zh.md) 为基础的 SDK stdio 应用 `dsh` profile 组合包。它继承 base 默认禁用模块 HMR(热模块替换)的策略;其 patch 设置 coding agent(编程智能体)persona、挂载应用自有的零选项命令提供方,并且只在该提供方接受调用后启动 [`dsh-sdk-jsonrpc-server`](../../sdk/server/README.zh.md)。因此,`dsh --profile sdk --help` 会写出 help 并退出,不会占用 stdin 或 stdout。 +以 [`dsh-base`](../base/README.zh.md) 为基础的 SDK stdio 应用 `dsh` profile 组合包。它继承 base 默认禁用模块 HMR(热模块替换)的策略;其 patch 设置 coding agent(编程智能体)persona、挂载应用自有的零选项命令提供方,并且只在该提供方接受调用后启动 [`dsh-sdk-jsonrpc-server`](../../sdk/server/README.zh.md)。因此,`dsh --profile sdk --help` 会写出 help 并退出,不会占用 stdin 或 stdout。独立的 [`sdk-minimal`](../sdk-minimal/README.zh.md) 组合包复用同一个启动提供方,并提供自己的 profile 名称。 启动提供方把 stdin EOF 接到启动器的有界成功关闭流程。SDK 协议 `shutdown`、SIGINT 与 SIGTERM 继续使用各自所属的 server 或启动器路径;dispose(资源释放)会排空根 profile 配置树与持久化。stdout 专用于按换行分隔的 JSON-RPC 帧。SDK 不提供 title 表层,因此本组合包禁用模型生成的 session title;确定性的 fallback title 仍会持久化,但不发起辅助模型请求。部署通过 profile 组合包与 patch 文件选择另一套完整组合,而不是使用另一个应用 bin。 +| 配置 | 默认值 | 行为 | +|---|---|---| +| `profile` | `sdk` | 命令 help 中呈现的 profile 名称;挂载此提供方的组合包会设置自己的随附 profile 名称。 | + `DSH_MAX_TOKENS_AS_SUCCESS` 保留 SDK 部署映射:未设置或 JSON `true` 把 token 达限的 subagent 完成报告为已接受,JSON `false` 则报告为错误。模型提供方/模型与工作区 cwd 通过 SDK 初始化请求传入;base profile 拥有适配器、工具、持久化、策略、settings 与 credentials。 ## 模型体验 diff --git a/packages/bundle/sdk-app/cordis.patch.yml b/packages/bundle/sdk-app/cordis.patch.yml index aa1795168c..373e7aeb63 100644 --- a/packages/bundle/sdk-app/cordis.patch.yml +++ b/packages/bundle/sdk-app/cordis.patch.yml @@ -11,6 +11,8 @@ - insert: - id: sdk-app-startup name: '@deepseek-ai/dsh-sdk-app' + config: + profile: sdk - id: sdk-jsonrpc-server name: '@deepseek-ai/dsh-sdk-jsonrpc-server' diff --git a/packages/bundle/sdk-app/package.json b/packages/bundle/sdk-app/package.json index 87214882fb..6ab4167d8d 100644 --- a/packages/bundle/sdk-app/package.json +++ b/packages/bundle/sdk-app/package.json @@ -41,6 +41,7 @@ "dependencies": { "@deepseek-ai/dsh-cmdline": "workspace:^", "@deepseek-ai/dsh-sdk-jsonrpc-server": "workspace:^", + "@deepseek-ai/schemastery": "workspace:^", "commander": "^15.0.0" }, "peerDependencies": { diff --git a/packages/bundle/sdk-app/src/index.ts b/packages/bundle/sdk-app/src/index.ts index 9ade81a965..fec847af53 100644 --- a/packages/bundle/sdk-app/src/index.ts +++ b/packages/bundle/sdk-app/src/index.ts @@ -7,6 +7,7 @@ import { Command } from 'commander' import type { Context } from '@deepseek-ai/cordis' +import z from '@deepseek-ai/schemastery' import { exitOnStdinEnd, parseCmdline } from '@deepseek-ai/dsh-cmdline' /** Stable Cordis plugin name. */ @@ -18,18 +19,30 @@ export const inject = ['cmdlineArgs'] /** Service the JSON-RPC server row waits for before claiming stdio. */ export const SDK_APP_STARTUP_SERVICE = 'sdkAppStartup' +/** SDK stdio startup configuration. */ +export interface Config { + /** Profile name rendered in help and diagnostics (default `sdk`). */ + profile?: string +} + +/** Validate and default SDK stdio startup configuration. */ +export const Config: z = z.object({ + profile: z.string().default('sdk'), +}) + /** * Build this app's zero-option command and help. + * @param profile - selected profile name rendered in the command grammar. * @returns a fresh program for one invocation. */ -function sdkCommand(): Command { +function sdkCommand(profile: string): Command { return new Command() - .name('dsh --profile sdk') + .name(`dsh --profile ${profile}`) .description('Serve DeepSeek Harness SDK clients over stdio JSON-RPC.') .helpOption('-h, --help', 'show this help') .addHelpText('after', ` Example: - dsh --profile sdk serve one SDK runtime until its client disconnects + dsh --profile ${profile} serve one SDK runtime until its client disconnects `) } @@ -37,9 +50,10 @@ Example: * Accept an SDK profile invocation, publish readiness, and bind EOF to the * launcher's bounded shutdown. * @param ctx - plugin context carrying command-line and exit launcher values. + * @param config - selected profile identity for command help. */ -export function apply(ctx: Context): void { - const program = sdkCommand() +export function apply(ctx: Context, config: Config = {}): void { + const program = sdkCommand(config.profile ?? 'sdk') program.action(() => { exitOnStdinEnd(ctx, 'sdk-app.stdin') ctx.provide(SDK_APP_STARTUP_SERVICE, { accepted: true }) diff --git a/packages/bundle/sdk-app/tests/startup.spec.ts b/packages/bundle/sdk-app/tests/startup.spec.ts index 65f2b76c5d..ec128a1c4a 100644 --- a/packages/bundle/sdk-app/tests/startup.spec.ts +++ b/packages/bundle/sdk-app/tests/startup.spec.ts @@ -4,7 +4,7 @@ import { EventEmitter } from 'node:events' import { Context } from '@deepseek-ai/cordis' import { afterEach, describe, expect, it } from 'vitest' import { internals, provideCmdline } from '@deepseek-ai/dsh-cmdline' -import { apply, SDK_APP_STARTUP_SERVICE } from '../src/index.ts' +import { apply, type Config, SDK_APP_STARTUP_SERVICE } from '../src/index.ts' /** Controllable stdin for one startup invocation. */ class TestStdin extends EventEmitter { @@ -27,7 +27,7 @@ afterEach(() => { }) /** Run the provider with captured command output and exit requests. */ -function start(args: string[]): { ctx: Context; exits: number[]; out: () => string; stdin: TestStdin } { +function start(args: string[], config: Config = {}): { ctx: Context; exits: number[]; out: () => string; stdin: TestStdin } { const ctx = new Context() const exits: number[] = [] const stdin = new TestStdin() @@ -41,7 +41,7 @@ function start(args: string[]): { ctx: Context; exits: number[]; out: () => stri exit: code => void exits.push(code), ready: { onReady: (listener) => { listener(); return () => {} } }, }) - apply(ctx) + apply(ctx, config) return { ctx, exits, out: () => out, stdin } } @@ -62,4 +62,10 @@ describe('SDK app startup', () => { stdin.end() expect(exits).toEqual([0]) }) + + it('renders the selected SDK profile name in help', () => { + const { out } = start(['--help'], { profile: 'sdk-minimal' }) + expect(out()).toContain('Usage: dsh --profile sdk-minimal') + expect(out()).toContain('dsh --profile sdk-minimal') + }) }) diff --git a/packages/bundle/sdk-app/tsconfig.json b/packages/bundle/sdk-app/tsconfig.json index 1d644141bd..0a98d3117c 100644 --- a/packages/bundle/sdk-app/tsconfig.json +++ b/packages/bundle/sdk-app/tsconfig.json @@ -11,6 +11,9 @@ { "path": "../../../vendor/cordis" }, + { + "path": "../../../vendor/schemastery" + }, { "path": "../../runtime-diagnostics/invariants" }, diff --git a/packages/sdk/python-runtime/README.i18n.yaml b/packages/bundle/sdk-minimal/README.i18n.yaml similarity index 55% rename from packages/sdk/python-runtime/README.i18n.yaml rename to packages/bundle/sdk-minimal/README.i18n.yaml index d2b2859c7c..c573869472 100644 --- a/packages/sdk/python-runtime/README.i18n.yaml +++ b/packages/bundle/sdk-minimal/README.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write packages/sdk/python-runtime/README.md -README.md: 291ad3edaff8182007079de8e41f33e91546e5d7 -README.zh.md: 54001ccfffb356db290d7c7b38db41070d8dd661 +# pnpm run verify-translation-pairing --write packages/bundle/sdk-minimal/README.md +README.md: b33ccb429ab0291d46f0271329b957f0ac7011fa +README.zh.md: 9e4ab381f5595630cbf139b1a1e8e30cef147f39 diff --git a/packages/bundle/sdk-minimal/README.md b/packages/bundle/sdk-minimal/README.md new file mode 100644 index 0000000000..b33ccb429a --- /dev/null +++ b/packages/bundle/sdk-minimal/README.md @@ -0,0 +1,31 @@ +# `@deepseek-ai/dsh-sdk-minimal` + +English | [中文](README.zh.md) + +Standalone minimal SDK application bundle for `dsh --profile sdk-minimal`. Its single insert is the complete Cordis tree: SDK stdio startup and JSON-RPC serving, one environment-configured DeepSeek adapter, the executor-less agent spine, local subprocess and unrestricted filesystem providers, a persistent Bash PTY, the string-replace editor, and uncompressed JSONL session persistence under `$DSH_HOME/sessions`. It deliberately does not include [`dsh-base`](../base/README.md), Web, settings, managed credentials, telemetry, compaction, workspace instructions, skills, jobs tools, subagents, or any other model-facing tool. + +The profile remains part of the ordinary launcher and layering model. The bundle supplies the complete default tree; the profile patch, home patch, and ordered `--patch` files can replace rows or insert external bundles above it. `dsh plugin --profile sdk-minimal` manages persistent dependencies. The shipped template uses startup-only patches so one stdio connection never observes replacement of its server or agent dependencies. + +`DEEPSEEK_API_KEY` supplies the adapter credential. The SDK initialization request is the sole model selection; the adapter accepts that model id even when it is absent from its advisory catalog. `DSH_CONTEXT_WINDOW` sets the fallback capacity for such models, and `DSH_SYSTEM_PROMPT` replaces the default persona. The process working directory is the sandbox-policy workspace and local-filesystem root. The bundle sets `danger-full-access`; its persistent shell and editor can modify any path available to the process. + +## Model Experience + +### Minimal coding-agent composition + +#### What the model sees + +The system prompt is `DSH_SYSTEM_PROMPT` or `You are a helpful software engineer assistant.`. The only advertised tools are owner-scoped persistent `bash` and `str_replace_editor`; runtime context, workspace instructions, skills, jobs controls, compaction, and Harness identity are absent. + +#### Token effect + +One stable persona plus the two tool schemas. Tool results and ordinary conversation history grow with the session. + +#### KV Cache effect + +Stable for a fixed persona, platform, provider, model, and bundle patch stack. Profile changes take effect on the next process. + +## Known Limitations and Deferred Work + +- **The profile is POSIX-only** — this composition uses a Bash PTY; a Windows profile must select a PowerShell terminal and tool instead. +- **The composition intentionally omits shared product services** — select `dsh --profile sdk` when settings, managed credentials, policy presets, telemetry, Web tools, or the full default tool roster are required. +- **User patches can expand the tree and corrupt stdout** — profile customization is trusted application composition; a plugin that writes ordinary text to stdout can break JSON-RPC framing. diff --git a/packages/bundle/sdk-minimal/README.zh.md b/packages/bundle/sdk-minimal/README.zh.md new file mode 100644 index 0000000000..9e4ab381f5 --- /dev/null +++ b/packages/bundle/sdk-minimal/README.zh.md @@ -0,0 +1,31 @@ +# `@deepseek-ai/dsh-sdk-minimal` + +[English](README.md) | 中文 + +供 `dsh --profile sdk-minimal` 使用的独立极简 SDK 应用组合包。它的单个 insert 构成完整 Cordis 树:SDK stdio 启动与 JSON-RPC 对外服务、一个由环境配置的 DeepSeek 适配器、无执行器的 agent 主干、本地子进程与不受限文件系统提供方、持久 Bash PTY、字符串替换编辑器,以及位于 `$DSH_HOME/sessions` 的未压缩 JSONL 会话持久化。它刻意不包含 [`dsh-base`](../base/README.zh.md)、Web、settings、托管凭据、遥测、压缩(compaction)、workspace 指令、skills、jobs 工具、subagent 或任何其他面向模型的工具。 + +该 profile 仍遵循普通 launcher 与分层模型。组合包提供完整默认树;profile patch、home patch 与有序 `--patch` 文件可以在其上替换配置项或插入外部组合包。`dsh plugin --profile sdk-minimal` 管理持久依赖。随附模板仅在启动时应用 patch,因此一个 stdio 连接不会观察到服务器或 agent 依赖在运行中被替换。 + +`DEEPSEEK_API_KEY` 提供适配器凭据。SDK 初始化请求是唯一模型选择;即使该模型 id 不在适配器的建议目录中,适配器也会接受它。`DSH_CONTEXT_WINDOW` 为这类模型设置后备容量,`DSH_SYSTEM_PROMPT` 替换默认 persona。进程工作目录同时作为沙箱策略 workspace 与本地文件系统根目录。该组合包设置 `danger-full-access`;其持久 shell 与编辑器可以修改进程可访问的任何路径。 + +## 模型体验 + +### 极简 coding agent 组合 + +#### 模型看到的内容 + +系统提示词取 `DSH_SYSTEM_PROMPT`,未设置时使用 `You are a helpful software engineer assistant.`。对外公布的工具只有 agent 所有的持久 `bash` 与 `str_replace_editor`;运行时上下文、workspace 指令、skills、jobs 控制、compaction 与 Harness 身份均不存在。 + +#### Token 影响 + +一个稳定 persona 加两个工具 schema。工具结果与普通对话历史随会话增长。 + +#### KV Cache 影响 + +当 persona、平台、提供方、模型与组合包 patch 栈固定时保持稳定。Profile 变更在下一个进程生效。 + +## 已知限制与待办工作 + +- **该 profile 仅支持 POSIX** — 此组合使用 Bash PTY;Windows profile 必须改为选择 PowerShell 终端与工具。 +- **该组合刻意省略共享产品服务** — 需要 settings、托管凭据、权限策略预设、遥测、Web 工具或完整默认工具清单时,请选择 `dsh --profile sdk`。 +- **用户 patch 可以扩展配置树并破坏 stdout** — profile 自定义属于受信任的应用组合;向 stdout 写入普通文本的插件会破坏 JSON-RPC 分帧。 diff --git a/packages/bundle/sdk-minimal/cordis.patch.yml b/packages/bundle/sdk-minimal/cordis.patch.yml new file mode 100644 index 0000000000..24d707c7e9 --- /dev/null +++ b/packages/bundle/sdk-minimal/cordis.patch.yml @@ -0,0 +1,95 @@ +# Standalone minimal SDK application. Unlike the ordinary SDK profile, this +# bundle does not layer over dsh-base: this insert is the complete Cordis tree. +# User profile, home, and invocation patches still apply above it. + +- insert: + - id: sdk-app-startup + name: '@deepseek-ai/dsh-sdk-app' + config: + profile: sdk-minimal + + - id: sdk-jsonrpc-server + name: '@deepseek-ai/dsh-sdk-jsonrpc-server' + inject: [sdkAppStartup, loader] + config: + maxTokensAsSuccess: false + + - id: deepseek-llm-api-extensions + name: '@deepseek-ai/dsh-deepseek-llm-api-extensions' + + - id: session-log-deepseek + name: '@deepseek-ai/dsh-session-log-deepseek' + + - id: plugin-package-inventory-deepseek + name: '@deepseek-ai/dsh-plugin-package-inventory-deepseek' + + - id: llm-deepseek + name: '@deepseek-ai/dsh-llm-deepseek' + config: + apiKeyEnv: DEEPSEEK_API_KEY + defaultContextWindow: !!js Number(process.env.DSH_CONTEXT_WINDOW ?? 1000000) + streamIdleTimeoutMs: 172800000 + + - id: sandbox + name: '@deepseek-ai/dsh-sandbox-local' + + - id: sandbox-policy + name: '@deepseek-ai/dsh-sandbox-policy' + config: + mode: danger-full-access + workspaceRoot: !!js process.cwd() + + - id: subprocess + name: '@deepseek-ai/dsh-subprocess-local' + + - id: pty + name: '@deepseek-ai/dsh-terminal' + + - id: terminal-bash + name: '@deepseek-ai/dsh-terminal-bash' + config: + timeoutMs: 300000 + + # The editor uses the bare local filesystem; persistent Bash still consumes + # the shared danger-full-access sandbox policy above. + - id: fs-local + name: '@deepseek-ai/dsh-fs-local' + config: + cwd: !!js process.cwd() + + - id: agent-spine + name: '@deepseek-ai/dsh-agent-spine-demo' + config: + includeHarnessIdentity: false + includeRuntimeContext: false + persona: !!js process.env.DSH_SYSTEM_PROMPT ?? 'You are a helpful software engineer assistant.' + workspaceContext: false + skills: + enabled: false + toolBash: false + toolJobs: false + + - id: persistent-bash + name: '@deepseek-ai/dsh-tool-bash-persistent' + config: + timeoutMs: 300000 + description: |- + Run commands in a bash shell + * When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped. + * You don't have access to the internet via this tool. + * You do have access to a mirror of common linux and python packages via apt and pip. + * State is persistent across command calls and discussions with the user. + * To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'. + * Please avoid commands that may produce a very large amount of output. + * Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background. + + - id: str-replace-editor + name: '@deepseek-ai/dsh-tool-str-replace-editor' + config: + maxOutputChars: 16000 + + - id: sessions + name: '@deepseek-ai/dsh-session-persistence-jsonl' + config: + root: !!js dshHomePath('sessions') + compression: none diff --git a/packages/bundle/sdk-minimal/package.json b/packages/bundle/sdk-minimal/package.json new file mode 100644 index 0000000000..1b3d5da5d2 --- /dev/null +++ b/packages/bundle/sdk-minimal/package.json @@ -0,0 +1,67 @@ +{ + "name": "@deepseek-ai/dsh-sdk-minimal", + "description": "The standalone minimal SDK profile bundle: JSON-RPC, one DeepSeek adapter, persistent shell, editor, and JSONL sessions", + "version": "0.1.1-rc.2", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", + "directory": "packages/bundle/sdk-minimal" + }, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, + "./cordis.patch.yml": "./cordis.patch.yml", + "./src/*": "./src/*", + "./package.json": "./package.json" + }, + "files": [ + "lib/index.js", + "lib/invariant.js", + "cordis.patch.yml", + "lib/types/**/*.d.ts" + ], + "license": "MIT", + "dsh": { + "bundle": { + "patch": "./cordis.patch.yml" + } + }, + "dependencies": { + "@deepseek-ai/dsh-agent-spine-demo": "workspace:^", + "@deepseek-ai/dsh-deepseek-llm-api-extensions": "workspace:^", + "@deepseek-ai/dsh-fs-local": "workspace:^", + "@deepseek-ai/dsh-llm-deepseek": "workspace:^", + "@deepseek-ai/dsh-plugin-package-inventory-deepseek": "workspace:^", + "@deepseek-ai/dsh-sandbox-local": "workspace:^", + "@deepseek-ai/dsh-sandbox-policy": "workspace:^", + "@deepseek-ai/dsh-sdk-app": "workspace:^", + "@deepseek-ai/dsh-sdk-jsonrpc-server": "workspace:^", + "@deepseek-ai/dsh-session-log-deepseek": "workspace:^", + "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^", + "@deepseek-ai/dsh-subprocess-local": "workspace:^", + "@deepseek-ai/dsh-terminal": "workspace:^", + "@deepseek-ai/dsh-terminal-bash": "workspace:^", + "@deepseek-ai/dsh-tool-bash-persistent": "workspace:^", + "@deepseek-ai/dsh-tool-str-replace-editor": "workspace:^" + }, + "peerDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/cordis": "workspace:^" + }, + "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/cordis": "workspace:^" + } +} diff --git a/packages/bundle/sdk-minimal/src/index.ts b/packages/bundle/sdk-minimal/src/index.ts new file mode 100644 index 0000000000..a5f162161e --- /dev/null +++ b/packages/bundle/sdk-minimal/src/index.ts @@ -0,0 +1,9 @@ +/** + * @deepseek-ai/dsh-sdk-minimal — the standalone minimal SDK profile bundle. + * The package's substance is `cordis.patch.yml`, declared by the + * `dsh.bundle.patch` manifest field and resolved by the profile composer; + * this module carries no runtime interface. + * @module @deepseek-ai/dsh-sdk-minimal + */ + +export {} diff --git a/packages/sdk/python-runtime/src/invariant.ts b/packages/bundle/sdk-minimal/src/invariant.ts similarity index 51% rename from packages/sdk/python-runtime/src/invariant.ts rename to packages/bundle/sdk-minimal/src/invariant.ts index 79bb5ec1f6..e2f480504a 100644 --- a/packages/sdk/python-runtime/src/invariant.ts +++ b/packages/bundle/sdk-minimal/src/invariant.ts @@ -1,23 +1,20 @@ /** - * Package-owned invariant companion for `@deepseek-ai/dsh-sdk-python-runtime`. - * @module @deepseek-ai/dsh-sdk-python-runtime/invariant + * Package-owned invariant companion for `@deepseek-ai/dsh-sdk-minimal`. + * @module @deepseek-ai/dsh-sdk-minimal/invariant */ -/* jscpd:ignore-start */ import type { Context } from '@deepseek-ai/cordis' import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' -const PACKAGE_NAME = '@deepseek-ai/dsh-sdk-python-runtime' +const PACKAGE_NAME = '@deepseek-ai/dsh-sdk-minimal' /** Cordis companion plugin name. */ -export const name = 'sdk-python-runtime-invariant' -/** Service required before the companion can reserve package ownership. */ +export const name = 'sdk-minimal-bundle-invariant' +/** Service required before the companion can register. */ export const inject = ['invariants'] -/** - * No runtime invariant: this composition package owns no independent event stream or mutable data; - * Loader and built-entry tests cover its wiring. - */ +// No runtime invariant: the package is a static patch-list carrier whose +// inserted rows own their runtime relationships and invariant companions. const install: InvariantInstaller = () => {} /** @@ -27,4 +24,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts b/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts new file mode 100644 index 0000000000..f8c23e92ac --- /dev/null +++ b/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts @@ -0,0 +1,64 @@ +/** The standalone SDK-minimal bundle's complete declared Cordis tree. */ + +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import * as yaml from 'js-yaml' +import { describe, expect, it } from 'vitest' +import { entryListSchema } from '@deepseek-ai/cordis-plugin-include' + +describe('dsh-sdk-minimal bundle', () => { + it('declares one standalone allowlisted tree with every row dependency', () => { + const root = fileURLToPath(new URL('..', import.meta.url)) + const manifest = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as { + dependencies?: Record + dsh?: { bundle?: { patch?: string } } + } + expect(manifest.dsh?.bundle?.patch).toBe('./cordis.patch.yml') + const patches = yaml.load( + readFileSync(resolve(root, manifest.dsh!.bundle!.patch!), 'utf8'), + { schema: entryListSchema }, + ) as Array<{ insert?: Array<{ id?: string; inject?: string[]; name?: string; config?: Record }> }> + expect(patches).toHaveLength(1) + const rows = patches[0]?.insert ?? [] + expect(rows.map(row => [row.id, row.name])).toEqual([ + ['sdk-app-startup', '@deepseek-ai/dsh-sdk-app'], + ['sdk-jsonrpc-server', '@deepseek-ai/dsh-sdk-jsonrpc-server'], + ['deepseek-llm-api-extensions', '@deepseek-ai/dsh-deepseek-llm-api-extensions'], + ['session-log-deepseek', '@deepseek-ai/dsh-session-log-deepseek'], + ['plugin-package-inventory-deepseek', '@deepseek-ai/dsh-plugin-package-inventory-deepseek'], + ['llm-deepseek', '@deepseek-ai/dsh-llm-deepseek'], + ['sandbox', '@deepseek-ai/dsh-sandbox-local'], + ['sandbox-policy', '@deepseek-ai/dsh-sandbox-policy'], + ['subprocess', '@deepseek-ai/dsh-subprocess-local'], + ['pty', '@deepseek-ai/dsh-terminal'], + ['terminal-bash', '@deepseek-ai/dsh-terminal-bash'], + ['fs-local', '@deepseek-ai/dsh-fs-local'], + ['agent-spine', '@deepseek-ai/dsh-agent-spine-demo'], + ['persistent-bash', '@deepseek-ai/dsh-tool-bash-persistent'], + ['str-replace-editor', '@deepseek-ai/dsh-tool-str-replace-editor'], + ['sessions', '@deepseek-ai/dsh-session-persistence-jsonl'], + ]) + expect(rows.find(row => row.id === 'sdk-app-startup')?.config).toEqual({ profile: 'sdk-minimal' }) + expect(rows.find(row => row.id === 'sdk-jsonrpc-server')).toMatchObject({ + inject: ['sdkAppStartup', 'loader'], + config: { maxTokensAsSuccess: false }, + }) + expect(rows.find(row => row.id === 'llm-deepseek')?.config).toEqual({ + apiKeyEnv: 'DEEPSEEK_API_KEY', + defaultContextWindow: { __jsExpr: 'Number(process.env.DSH_CONTEXT_WINDOW ?? 1000000)' }, + streamIdleTimeoutMs: 172800000, + }) + expect(rows.find(row => row.id === 'agent-spine')?.config).toMatchObject({ + includeHarnessIdentity: false, + includeRuntimeContext: false, + workspaceContext: false, + skills: { enabled: false }, + toolBash: false, + toolJobs: false, + }) + expect(Object.keys(manifest.dependencies ?? {}).sort()).toEqual( + [...new Set(rows.map(row => row.name).filter((name): name is string => name !== undefined))].sort(), + ) + }) +}) diff --git a/packages/sdk/python-runtime/tsconfig.json b/packages/bundle/sdk-minimal/tsconfig.json similarity index 74% rename from packages/sdk/python-runtime/tsconfig.json rename to packages/bundle/sdk-minimal/tsconfig.json index ffd1ce9e41..8f58ed6e28 100644 --- a/packages/sdk/python-runtime/tsconfig.json +++ b/packages/bundle/sdk-minimal/tsconfig.json @@ -11,12 +11,6 @@ { "path": "../../../vendor/cordis" }, - { - "path": "../../../vendor/loader" - }, - { - "path": "../../boot/app-boot" - }, { "path": "../../runtime-diagnostics/invariants" } diff --git a/packages/bundle/web-app/cordis.patch.yml b/packages/bundle/web-app/cordis.patch.yml index f22884cc77..18a0d3911a 100644 --- a/packages/bundle/web-app/cordis.patch.yml +++ b/packages/bundle/web-app/cordis.patch.yml @@ -41,6 +41,11 @@ # `dsh.client` rows are the browser roster the modules node half scans into # window.__DSH_BOOT__; the modules row is simultaneously a host row. - insert: + # Host-owned opt-in sampled when a new Web session receives its preset + # delegation tools. The Models page edits this settings namespace. + - id: subagent-model-selection-settings + name: '@deepseek-ai/dsh-tool-subagent/model-selection-settings' + - id: code-runtime name: '@deepseek-ai/dsh-code-runtime-worker-thread' diff --git a/packages/bundle/web-app/package.json b/packages/bundle/web-app/package.json index 2f26a9daed..68c43da265 100644 --- a/packages/bundle/web-app/package.json +++ b/packages/bundle/web-app/package.json @@ -45,6 +45,7 @@ }, "dependencies": { "@deepseek-ai/dsh-agent-presets": "workspace:^", + "@deepseek-ai/dsh-tool-subagent": "workspace:^", "@deepseek-ai/dsh-api-remotes": "workspace:^", "@deepseek-ai/dsh-app-boot": "workspace:^", "@deepseek-ai/dsh-client-connection": "workspace:^", diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index a6102ddac0..85e96fbdca 100644 --- a/packages/client/ui-conversation/README.i18n.yaml +++ b/packages/client/ui-conversation/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-conversation/README.md -README.md: 4c9665b680fe1922770403d88a04dffc755481ad -README.zh.md: 8bf3db2cb1401427f29016f8dbddcd9d27ec9635 +README.md: 08d1408bea404a80da491078aa013989835fccaf +README.zh.md: 874aea8865e6f74a64da2c249087bdad032d98d5 diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index 4c9665b680..08d1408bea 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -20,6 +20,8 @@ View selection is deterministic: a registered persisted selection wins, otherwis The resident composer survives no-Session and Session transitions. The no-Session state keeps the same textarea mounted but inert while the Workspace picker connects a blank Session. Draft text is mirrored into the per-Session Conversation store. Queue operations address exact queue occurrences through the scoped `ctx.conversation` service. Busy Enter behavior is stored in the Host-backed `ui-conversation` settings namespace. +An ordinary running composer keeps Stop as its primary pointer action while its draft is empty or an owner block makes input unavailable. Actionable text or attachments switch the same seat to Queue Send; clearing or successfully submitting the draft restores Stop. Keyboard Queue/Steer selection remains governed by the busy-Enter setting, while continuable subagents keep independent Send and Stop actions ([decision](../../../.agents/notes/implemented/bug-fix/2026-08-20-running-draft-primary-send.md)). + ## Temporary composer entries `conversation.composer` is a generic chain. Its complete owner currency is: diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index 8bf3db2cb1..874aea8865 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -20,6 +20,8 @@ View 选择规则固定:有效且已注册的持久化选择优先,其次是 常驻 composer 在无 Session 与有 Session 之间保持挂载。无 Session 时,同一个 textarea 保持 inert,Workspace picker 连接 blank Session;草稿文本镜像到逐 Session Conversation store。Queue 操作通过 scoped `ctx.conversation` service 寻址准确的 queue occurrence。繁忙时 Enter 行为保存在 Host-backed `ui-conversation` settings namespace。 +普通 composer 运行期间,草稿为空或 owner block 使输入不可用时,主指针操作保持为 Stop。可提交文字或附件会把同一位置切换为 Queue Send;清空或成功提交草稿后恢复 Stop。键盘 Queue/Steer 选择仍由繁忙态 Enter 设置决定,可继续 subagent 则保留相互独立的 Send 与 Stop 操作([决策](../../../.agents/notes/implemented/bug-fix/2026-08-20-running-draft-primary-send.zh.md))。 + ## 临时 composer entry `conversation.composer` 是通用 chain,其完整 owner currency 为: diff --git a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx index 8bedb2d260..0a137c8010 100644 --- a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx @@ -554,10 +554,10 @@ export function InputBar({ if (el !== null) toggleCommandMenu?.(selectionOf(el)) } - // Ordinary sessions retain their primary Send/Stop toggle. A continuable - // child keeps Send as the primary action and exposes Stop independently so - // pointer users can queue follow-ups while its current turn is running. - const primaryStops = running && subagent === null + // An ordinary running session keeps Stop while the composer is empty or + // owner-blocked; an actionable draft gets the existing Queue action. A + // continuable child keeps Send primary and exposes Stop independently. + const primaryStops = running && subagent === null && (empty || blocked !== undefined) const interruptible = running && continuable const primaryLabel = primaryStops ? t('input.stop') : t('input.send') const onPrimary = (): void => { diff --git a/packages/client/ui-conversation/tests/input-bar.client.spec.tsx b/packages/client/ui-conversation/tests/input-bar.client.spec.tsx index f36b1aa1c4..17064a97a5 100644 --- a/packages/client/ui-conversation/tests/input-bar.client.spec.tsx +++ b/packages/client/ui-conversation/tests/input-bar.client.spec.tsx @@ -64,6 +64,7 @@ interface BenchOptions { subagent?: Exclude disabled?: boolean inert?: boolean + blocked?: { readonly reason: string } workspacePickerOpen?: boolean onRequestWorkspace?: () => void promptError?: SessionSnapshot['promptError'] @@ -186,6 +187,7 @@ function bench(over?: BenchOptions) { renderSlot, variant: over?.variant ?? 'composer', ...(over?.inert === true ? { disabled: true } : {}), + ...(over?.blocked !== undefined ? { blocked: over.blocked } : {}), ...(over?.workspacePickerOpen !== undefined ? { workspacePickerOpen: over.workspacePickerOpen } : {}), ...(over?.onRequestWorkspace !== undefined ? { onRequestWorkspace: over.onRequestWorkspace } : {}), ...(over?.placeholder !== undefined ? { placeholder: over.placeholder } : {}), @@ -196,7 +198,9 @@ function bench(over?: BenchOptions) { } const view = render() const textarea = view.container.querySelector('textarea')! + const sendableDraft = (over?.draft?.trim() ?? '') !== '' || (over?.attachments?.length ?? 0) > 0 const primaryStops = over?.running === true && over.subagent === undefined + && (!sendableDraft || over.blocked !== undefined) const button = view.container.querySelector( `button[aria-label="${primaryStops ? '停止生成' : '发送消息'}"]`, )! @@ -606,15 +610,53 @@ describe('Enter semantics', () => { }) describe('running and lock semantics', () => { - it('running keeps the input free (typing + Enter queue) while the primary turns stop', () => { - const { textarea, button, stop, sink } = bench({ running: true, draft: '排队消息' }) + it('running switches the primary between Stop and Queue Send with the draft', async () => { + const { textarea, button, stop, sink } = bench({ running: true, busyEnter: 'steer' }) expect(textarea.disabled).toBe(false) - fireEvent.change(textarea, { target: { value: '排队消息2' } }) - fireEvent.keyDown(textarea, { key: 'Enter' }) - expect(sink).toHaveBeenCalledWith('排队消息2', [], 'queue', expect.any(AbortSignal)) expect(button.getAttribute('aria-label')).toBe('停止生成') fireEvent.click(button) expect(stop).toHaveBeenCalledTimes(1) + + fireEvent.change(textarea, { target: { value: '排队消息' } }) + expect(button.getAttribute('aria-label')).toBe('发送消息') + fireEvent.change(textarea, { target: { value: ' ' } }) + expect(button.getAttribute('aria-label')).toBe('停止生成') + fireEvent.change(textarea, { target: { value: '排队消息2' } }) + expect(button.getAttribute('aria-label')).toBe('发送消息') + fireEvent.click(button) + expect(sink).toHaveBeenCalledWith('排队消息2', [], 'queue', expect.any(AbortSignal)) + await vi.waitFor(() => { expect(button.getAttribute('aria-label')).toBe('停止生成') }) + expect(stop).toHaveBeenCalledTimes(1) + }) + + it('running treats an attachment-only draft as Send', async () => { + const attachment = { + kind: 'image' as const, + id: 'draft-1' as DraftAttachmentId, + file: new File([Uint8Array.of(1)], 'pixel.png', { type: 'image/png' }), + previewUrl: 'blob:pixel', + } + const { button, sink } = bench({ running: true, attachments: [attachment] }) + expect(button.getAttribute('aria-label')).toBe('发送消息') + fireEvent.click(button) + expect(sink).toHaveBeenCalledWith('', ['draft-1'], 'queue', expect.any(AbortSignal)) + await vi.waitFor(() => { expect(button.getAttribute('aria-label')).toBe('停止生成') }) + }) + + it('running blocked composer keeps Stop with a retained draft', () => { + const { button, sink, stop, textarea } = bench({ + running: true, + draft: '保留的草稿', + blocked: { reason: '请选择可用模型' }, + placeholder: '请选择可用模型', + }) + expect(textarea.disabled).toBe(true) + expect(textarea.placeholder).toBe('请选择可用模型') + expect(button.getAttribute('aria-label')).toBe('停止生成') + expect(button.disabled).toBe(false) + fireEvent.click(button) + expect(stop).toHaveBeenCalledTimes(1) + expect(sink).not.toHaveBeenCalled() }) it('running plain Enter follows the busy-state Steer preference', () => { diff --git a/packages/client/ui-settings-models/README.i18n.yaml b/packages/client/ui-settings-models/README.i18n.yaml index 0eb68f97f2..add13d5562 100644 --- a/packages/client/ui-settings-models/README.i18n.yaml +++ b/packages/client/ui-settings-models/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-settings-models/README.md -README.md: cc430c91b9c4124fc70d0ec205b5870012dd5554 -README.zh.md: 62358adbc6f055697efe33e29e579f0aec64efc3 +README.md: 0daa9c5288f169138a5961e8fd6bee7818dba89b +README.zh.md: b2f80b845302d29728a0574b82f565c96acf59f2 diff --git a/packages/client/ui-settings-models/README.md b/packages/client/ui-settings-models/README.md index cc430c91b9..0daa9c5288 100644 --- a/packages/client/ui-settings-models/README.md +++ b/packages/client/ui-settings-models/README.md @@ -4,6 +4,8 @@ English | [中文](README.zh.md) Models settings and product-onboarding plugin. The same client Cordis plugin registers the Models page plus two ordered first-run dialogs: a versioned internal-testing notice and the conditional official-DeepSeek credential step. Both steps share one modal wrapper and remain sequenced by `settings.onboarding`. The Models plane joins three wire domains into one shared snapshot — `llm.providers` (the configurable-provider directory with each route's live/dormant state), `settings.describe` (serialized schemas, layered redacted values, secret slots), and `credentials.describe` (value-free configured/source/writable badges) — and renders provider rows with one editor card at a time, without presenting route liveness as provider status. +When the Host advertises the `subagent-model-selection` settings namespace, Models also renders a localized switch above the provider rows. It defaults off and writes only `{ enabled }` through `settings.update` with the namespace revision. The Host samples it while composing a new top-level Session; changing it does not reconfigure running Sessions, while child Sessions inherit their parent's recorded decision. + Rows are the *configured* providers (their profile resolves in the owning namespace); a whole-section provider whose key is not configured anywhere renders as its open setup card instead of a row, but only in the first-run posture — while no provider is registered with the credential its profile names — and only until the user closes that card, after which it is an ordinary row carrying the missing-key dot. Each card kind owns its own open state, so closing one never discards a draft in another. The add flow is a card carrying the dormant-directory provider select — a bare-mounted `llm-pi-ai` offers its whole installed catalog before any route exists. The pi-ai card additionally edits that route's **model list** and can ask the provider what it serves. A row labels API-key state with a green solid dot only when a referenced credential is confirmed configured, and with a red solid dot only when a named reference is confirmed missing; reference-free provider-native authentication and unavailable credential enrichment remain unmarked. The editor is a hand-written card per adapter family: the primary field is a single **API key** input — the page never asks for an environment-variable name; a typed key stores **write-only** through `credentials.set` under the profile's reference, deriving `_API_KEY` when the profile has none, and the pi-ai profile records that derivation as `apiKeyEnv`, so `settings.yaml` never carries a key value. Leaving a new pi-ai provider's key blank saves a reference-free profile and therefore preserves provider-native authentication such as the Bedrock credential chain or Vertex ADC. A successful Apply emits a local accessible status message without echoing secret material. The collapsed 自定义设置 fold carries the curated extras — `baseURL` for both families (the deepseek placeholder shows the public endpoint), each adapter's model catalog, and the **display name** and **API protocol** of a pi-ai route the adapter does not ship. Those two are what a hand-declared route names for itself: the create card asks for both because nothing can default them, so the editor reaches both rather than leaving them to `settings.yaml`. Clearing the name unsets it and the route falls back to its id, which is what the placeholder shows; the protocol has no such fallback. A catalog route gets neither — it defaults its name from its catalog entry, and its models each carry their own protocol, so a route-level one could only override every one of them. The Provider ID stays fixed: it is the settings key, the name every other namespace and every logged session references, and the stem of a credential reference the page cannot read back to move. Reasoning effort is deliberately NOT among them: it is a per-model capability and the models under one provider disagree about which levels they accept, so a provider-scoped control could only be set to a value some of them reject — which would hide even the models that support the level. The composer's model picker offers each model its own levels, and a switch there records provider, model, and effort together as the default for the next session. The profile field stays in `settings.yaml` for a deployment that knows its route. Each DeepSeek row edits `id`, optional display `name`, and optional `contextWindow`/`maxTokens`; existing fields outside that curated set survive edits, while every other profile field stays owned by `settings.yaml`. A row is deletable only when the user layer alone carries it (removal restores the composition base), and its localized confirmation dialog names the provider in the title, description, and final action. A row is tagged **Custom** when the directory entry says the owning adapter ships nothing under that key. The tag follows that answer alone: having a stored profile does not make a route custom — narrowing a shipped provider's models stores one too — and an adapter that reports nothing leaves its rows untagged rather than being read as shipped. The notice step owns its exact copy in `src/client/locales.ts` and its acknowledgement version in `src/onboarding-copy.ts`. On loopback it compares and writes `ui-onboarding.welcomeNoticeVersion` through the existing settings API; only an explicit Continue records the current version. A non-loopback browser cannot use that Host-only namespace, so acknowledgement is process-local and the notice returns after reload. diff --git a/packages/client/ui-settings-models/README.zh.md b/packages/client/ui-settings-models/README.zh.md index 62358adbc6..b2f80b8453 100644 --- a/packages/client/ui-settings-models/README.zh.md +++ b/packages/client/ui-settings-models/README.zh.md @@ -4,6 +4,8 @@ 模型设置与产品引导插件。同一个 client Cordis 插件会注册 Models 页面和两个有序的首次使用弹窗:版本化内测声明,以及按条件显示的 DeepSeek 官方凭据步骤。两个步骤共用同一套弹窗组件,并继续由 `settings.onboarding` 排序。Models 平面把三个协议领域汇聚为一个共享快照:`llm.providers`(可配置提供方目录,含每条路由的存活/休眠状态)、`settings.describe`(序列化 schema、分层脱敏值、secret slot)与 `credentials.describe`(不含值的 configured/source/writable 徽标);页面据此渲染提供方行,一次只展开一张编辑卡片,且不把路由存活状态呈现为提供方状态。 +Host 公布 `subagent-model-selection` settings namespace 时,Models 还会在提供方行上方渲染本地化开关。它默认关闭,只通过 `settings.update` 携带 namespace revision 写入 `{ enabled }`。Host 会在组合新的顶层 Session 时读取它;修改设置不会重新配置运行中的 Session,而子 Session 会继承父级已记录的决定。 + 行是*已配置*的提供方(其 profile 在所属 namespace 中解析得出);其配置键未在任何位置配置的整分节提供方会渲染为其展开的设置卡片而非一行,但仅限首次运行姿态——即尚无任何提供方已注册且备齐其 profile 所指名的凭据——且仅持续到用户关闭该卡片为止,此后它就是一行带缺失密钥点的普通行。每一类卡片各自持有自己的展开状态,因此关掉其中一张绝不会丢弃另一张里的草稿。「新增」流程则是一张承载休眠目录提供方选择框的卡片——裸挂载的 `llm-pi-ai` 在任何路由存在之前就能提供其完整的已安装 catalog。pi-ai 卡片还会编辑该路由的**模型列表**,并可查询提供方所提供的模型。只有确认引用的凭据已配置时,行才会以绿色实心点标示 API 密钥状态;只有确认具名引用缺失时,才会以红色实心点标示。无引用的提供方原生认证以及无法取得凭据补充信息时都不显示状态点。编辑器是每个适配器家族各一张的手写卡片:主字段是单独一个 **API 密钥**输入框——页面从不询问环境变量名;键入的密钥经 `credentials.set` 以**只写**方式存入 profile 的引用之下,profile 没有引用时便派生 `_API_KEY`,pi-ai profile 会把这次派生记录为 `apiKeyEnv`,因此 `settings.yaml` 从不携带密钥值。为新的 pi-ai 提供方留空密钥会保存一个不带引用的 profile,因此能保留提供方原生认证,例如 Bedrock 凭据链或 Vertex ADC。「应用」成功后会发出本地无障碍状态消息,且绝不回显任何机密内容。收起的「自定义设置」折叠区承载精选的额外字段——两个家族都有 `baseURL`(deepseek 的占位符显示公共端点)、各适配器自己的模型目录,以及适配器未提供的那类 pi-ai 路由的**显示名称**与 **API 协议**。这两个字段是手工声明路由为自己命名的东西:创建卡片之所以索要它们,正因为没有东西能为它们兜底,因此编辑器也够得着这两个,而不是把它们留给 `settings.yaml`。清空名称即取消设置,路由退回自己的 id——占位符显示的就是它;协议没有这样的兜底。内置目录路由两个都不给:它的名称由目录条目兜底,它的每个模型各自带着自己的协议,路由级协议只可能把它们全部覆盖掉。Provider ID 保持固定:它是 settings 的键、是其他每个 namespace 与每一条已记录会话引用的名字,也是页面读不回、因而搬不走的凭据引用词干。推理等级刻意**不在**其中:它是按模型的能力,而同一提供方下各模型接受的档位并不一致,因此提供方级的控件只可能被设成其中一些模型会拒绝的值——那会连支持该档位的模型也一并隐藏。输入框的模型选择器为每个模型提供它自己的档位,在那里切换会把提供方、模型、推理等级一并记为下一个会话的默认值。profile 字段仍留在 `settings.yaml`,供清楚自己路由的部署使用。每条 DeepSeek 模型行可编辑 `id`、可选的显示名称 `name` 与可选的 `contextWindow`/`maxTokens`;精选集合以外的现有字段会在编辑后保留,其余每个 profile 字段仍归 `settings.yaml` 所有。只有当某行仅由用户层承载时它才可删除(删除会还原组合 base),其本地化确认对话框会在标题、说明和最终操作中点名该提供方。当目录条目表明拥有该路由的适配器在这个键下什么都没有时,该行会带上 **自定义** 标签。标签只跟随这个答案:存了 profile 并不使一条路由成为自定义——收窄一个内置提供方的模型同样会存下 profile——而什么都不回答的适配器,其路由保持无标签,不会被当成内置。 声明步骤在 `src/client/locales.ts` 中持有完整文案,并在 `src/onboarding-copy.ts` 中持有确认版本。回环访问会通过既有 settings API 比较并写入 `ui-onboarding.welcomeNoticeVersion`;只有明确点击「继续」才会记录当前版本。非回环浏览器无法使用这项仅限 Host 的 namespace,因此确认仅在当前进程有效,重载后声明会再次出现。 diff --git a/packages/client/ui-settings-models/src/client/ModelsSection.module.css b/packages/client/ui-settings-models/src/client/ModelsSection.module.css index 3719535a3a..1c767386dd 100644 --- a/packages/client/ui-settings-models/src/client/ModelsSection.module.css +++ b/packages/client/ui-settings-models/src/client/ModelsSection.module.css @@ -40,6 +40,87 @@ color: var(--dsw-alias-state-success-primary); } +.preferenceCard { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 8px 16px; + margin-top: 4px; + padding: 14px; + border: 1px solid var(--dsw-alias-border-l2); + border-radius: 12px; +} + +.preferenceCopy { + min-width: 0; +} + +.preferenceTitle { + margin: 0; + font-size: 14px; + line-height: 22px; + font-weight: 500; + color: var(--dsw-alias-label-primary); +} + +.preferenceDescription { + margin: 2px 0 0; + font-size: 12px; + line-height: 18px; + color: var(--dsw-alias-label-tertiary); +} + +.switch { + box-sizing: border-box; + position: relative; + width: 36px; + height: 20px; + padding: 2px; + border: 0; + border-radius: 10px; + background: var(--dsw-alias-border-l3); + cursor: pointer; +} + +.switchOn { + background: var(--dsw-alias-brand-primary); +} + +.switch:disabled { + cursor: default; + opacity: 0.5; +} + +.switch:focus-visible { + outline: none; + box-shadow: 0 0 0 2px var(--dsw-alias-border-l3); +} + +.switchThumb { + display: block; + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--dsw-alias-label-primary-foreground); + transition: transform 120ms ease; +} + +.switchOn .switchThumb { + transform: translateX(16px); +} + +.preferenceStatus, +.preferenceCard > .error { + grid-column: 1 / -1; +} + +.preferenceStatus { + margin: 0; + font-size: 12px; + line-height: 18px; + color: var(--dsw-alias-state-success-primary); +} + .rows { list-style: none; /* Extra air between the title/intro block and the first provider card. */ @@ -623,7 +704,8 @@ select.input { } @media (prefers-reduced-motion: reduce) { - .customizedSummary::before { + .customizedSummary::before, + .switchThumb { transition: none; } } diff --git a/packages/client/ui-settings-models/src/client/ModelsSection.tsx b/packages/client/ui-settings-models/src/client/ModelsSection.tsx index 7f178564d6..9501185e87 100644 --- a/packages/client/ui-settings-models/src/client/ModelsSection.tsx +++ b/packages/client/ui-settings-models/src/client/ModelsSection.tsx @@ -22,6 +22,7 @@ import { deriveKeyRef, messageOf, protocolChoices, providerUsable } from './stor import type { ModelsSettingsStore, ProviderRow } from './store.ts' import type { SettingsSchemaOperations } from './schema-operations.ts' import { ProviderEditor, type ProviderEditorProps } from './ProviderEditor.tsx' +import { SubagentModelSelectionCard } from './SubagentModelSelectionCard.tsx' import type { en } from './locales.ts' import styles from './ModelsSection.module.css' @@ -278,12 +279,24 @@ function Loaded({ injected }: { injected: ModelsSectionFace }): ReactNode { // one whose schema names the protocols one may speak; without it mounted // there is nothing to declare and the entry point stays disabled. const protocols = protocolChoices(state.namespaces.get('llm-pi-ai'), schema) + const subagentModelSelection = state.namespaces.get('subagent-model-selection') return (

{t('title')}

{t('intro')}

{!state.writable && state.status === 'ready' ?

{t('readOnly')}

: null} + {subagentModelSelection === undefined + ? null + : ( + + )} {savedIdentity === undefined ? null : ( diff --git a/packages/client/ui-settings-models/src/client/SubagentModelSelectionCard.tsx b/packages/client/ui-settings-models/src/client/SubagentModelSelectionCard.tsx new file mode 100644 index 0000000000..3b62f28e60 --- /dev/null +++ b/packages/client/ui-settings-models/src/client/SubagentModelSelectionCard.tsx @@ -0,0 +1,87 @@ +/** User control for model-selectable subagent delegation in new sessions. */ + +import { useState } from 'react' +import type { ReactNode } from 'react' +import type { IApiClient, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client' +import type { ModelsSettingsStore } from './store.ts' +import type { en } from './locales.ts' +import { messageOf } from './store.ts' +import styles from './ModelsSection.module.css' + +/** Props for the Host-owned subagent model-selection preference. */ +export interface SubagentModelSelectionCardProps { + /** Current redacted namespace view. */ + namespace: SettingsNamespaceView + /** Whether the settings provider accepts writes. */ + writable: boolean + /** Settings wire face. */ + api: Pick + /** Models page controller to refresh after a commit. */ + controller: ModelsSettingsStore + /** Localized Models copy. */ + t: (key: keyof typeof en) => string +} + +/** Read the schema-validated resolved boolean from a namespace view. */ +function enabledOf(namespace: SettingsNamespaceView): boolean { + if (typeof namespace.value !== 'object' || namespace.value === null) return false + return (namespace.value as { enabled?: unknown }).enabled === true +} + +/** Render and persist the default-off new-session preference. */ +export function SubagentModelSelectionCard({ + namespace, + writable, + api, + controller, + t, +}: SubagentModelSelectionCardProps): ReactNode { + const [saving, setSaving] = useState(false) + const [saved, setSaved] = useState(false) + const [error, setError] = useState(undefined) + const enabled = enabledOf(namespace) + + const toggle = (): void => { + setSaving(true) + setSaved(false) + setError(undefined) + void api.settings.update({ + ns: namespace.ns, + patch: { enabled: !enabled }, + expectedRevision: namespace.revision, + }).then(async (response) => { + if (!response.result.ok) throw new Error(response.result.error.message) + controller.acceptNamespace(response.result.value) + await controller.load() + setSaved(true) + }).catch((reason: unknown) => { + setError(messageOf(reason)) + }).finally(() => { setSaving(false) }) + } + + return ( +
+
+

+ {t('subagentModelSelectionTitle')} +

+

{t('subagentModelSelectionDescription')}

+
+ + {saved + ?

{t('subagentModelSelectionSaved')}

+ : null} + {error === undefined ? null :

{error}

} +
+ ) +} diff --git a/packages/client/ui-settings-models/src/client/locales.ts b/packages/client/ui-settings-models/src/client/locales.ts index f1b0718ba5..176e33fe5e 100644 --- a/packages/client/ui-settings-models/src/client/locales.ts +++ b/packages/client/ui-settings-models/src/client/locales.ts @@ -5,6 +5,10 @@ export const en = { nav: 'Models', title: 'Models', intro: 'Enter your API keys to use models from the following providers.', + subagentModelSelectionTitle: 'Subagent model selection', + subagentModelSelectionDescription: 'Allow new sessions to choose a provider, model, and reasoning effort for subagents. Running sessions do not change.', + subagentModelSelectionToggle: 'Allow subagents to choose models', + subagentModelSelectionSaved: 'Saved. New sessions use this setting.', edit: 'Edit', editProvider: 'Edit {provider}', remove: 'Delete', @@ -109,6 +113,10 @@ export const zh: { [Key in keyof typeof en]: string } = { nav: '模型', title: '模型', intro: '填入各提供方的 API 密钥即可使用其模型。', + subagentModelSelectionTitle: 'Subagent 自选模型', + subagentModelSelectionDescription: '允许新会话为 subagent 选择提供方、模型和推理强度。运行中的会话不会改变。', + subagentModelSelectionToggle: '允许 subagent 自选模型', + subagentModelSelectionSaved: '已保存,新会话将使用此设置。', edit: '编辑', editProvider: '编辑 {provider}', remove: '删除', diff --git a/packages/client/ui-settings-models/src/client/store.ts b/packages/client/ui-settings-models/src/client/store.ts index 349798acdd..2b17c9fc4b 100644 --- a/packages/client/ui-settings-models/src/client/store.ts +++ b/packages/client/ui-settings-models/src/client/store.ts @@ -124,6 +124,15 @@ export class ModelsSettingsStore { private readonly describeFace: SettingsDescribeFace, ) {} + /** + * Fold one successful settings write into the shared mirror before rejoining + * this page's rows. + * @param view - namespace view returned by the settings wire method. + */ + acceptNamespace(view: SettingsNamespaceView): void { + this.describeFace.acceptView(view) + } + /** * Refresh the whole page snapshot: the provider directory and the mirror's * settings answer in parallel, then one batched credential describe over diff --git a/packages/client/ui-settings-models/tests/components.client.spec.tsx b/packages/client/ui-settings-models/tests/components.client.spec.tsx index 8a3b8fce0a..5d0cbe8fed 100644 --- a/packages/client/ui-settings-models/tests/components.client.spec.tsx +++ b/packages/client/ui-settings-models/tests/components.client.spec.tsx @@ -8,6 +8,7 @@ import type { RpcResponse, SettingsNamespaceView } from '@deepseek-ai/dsh-api-re import { ModelsSection, needsSetup, providerCopy, providerTargetLabel, removeProviderProfile, } from '../src/client/ModelsSection.tsx' +import { SubagentModelSelectionCard } from '../src/client/SubagentModelSelectionCard.tsx' import type { ModelsSectionInjected, ModelsSectionProps } from '../src/client/ModelsSection.tsx' import { pathOps } from '../src/client/ProviderEditor.tsx' import { @@ -122,6 +123,14 @@ function wireNamespaces(): SettingsNamespaceView[] { secrets: [], revision: 0, }, + { + ns: 'subagent-model-selection', + schema: JSON.parse(JSON.stringify(Schema.object({ enabled: Schema.boolean().default(false) }).toJSON())) as unknown, + value: { enabled: false }, + applies: 'live', + secrets: [], + revision: 4, + }, ] } @@ -143,9 +152,10 @@ function scriptedFace(overrides: { set?: ReturnType unset?: ReturnType } = {}) { - const update = overrides.update ?? vi.fn(() => Promise.resolve(ok(wireNamespaces()[2]))) - const replace = overrides.replace ?? vi.fn(() => Promise.resolve(ok(wireNamespaces()[2]))) - const mutate = overrides.mutate ?? vi.fn(() => Promise.resolve(ok(wireNamespaces()[2]))) + const providerNamespace = wireNamespaces().find(view => view.ns === 'llm-pi-ai')! + const update = overrides.update ?? vi.fn(() => Promise.resolve(ok(providerNamespace))) + const replace = overrides.replace ?? vi.fn(() => Promise.resolve(ok(providerNamespace))) + const mutate = overrides.mutate ?? vi.fn(() => Promise.resolve(ok(providerNamespace))) const set = overrides.set ?? vi.fn(() => Promise.resolve(ok({}))) const unset = overrides.unset ?? vi.fn(() => Promise.resolve(ok({}))) const face = { @@ -236,6 +246,71 @@ describe('ModelsSection', () => { expect(document.body.textContent).toBe('') }) + it('persists the default-off subagent model-selection switch for new sessions', async () => { + const enabledNamespace: SettingsNamespaceView = { + ...wireNamespaces().find(view => view.ns === 'subagent-model-selection')!, + value: { enabled: true }, + user: { enabled: true }, + revision: 5, + } + const update = vi.fn(() => Promise.resolve(ok(enabledNamespace))) + await mountSection({ update }) + + const toggle = screen.getByRole('switch', { name: en.subagentModelSelectionToggle }) + expect(toggle.getAttribute('aria-checked')).toBe('false') + fireEvent.click(toggle) + + await waitFor(() => { expect(toggle.getAttribute('aria-checked')).toBe('true') }) + expect(update).toHaveBeenCalledWith({ + ns: 'subagent-model-selection', + patch: { enabled: true }, + expectedRevision: 4, + }) + expect(screen.getByRole('status').textContent).toBe(en.subagentModelSelectionSaved) + }) + + it('reports rejected subagent model-selection updates and permits a retry', async () => { + const update = vi.fn() + .mockResolvedValueOnce(fail('revision changed')) + .mockResolvedValueOnce(ok({ + ...wireNamespaces().find(view => view.ns === 'subagent-model-selection')!, + value: { enabled: true }, + revision: 5, + })) + await mountSection({ update }) + + const toggle = screen.getByRole('switch', { name: en.subagentModelSelectionToggle }) + fireEvent.click(toggle) + expect((await screen.findByRole('alert')).textContent).toBe('revision changed') + + fireEvent.click(toggle) + await waitFor(() => { expect(toggle.getAttribute('aria-checked')).toBe('true') }) + expect(screen.queryByRole('alert')).toBeNull() + }) + + it('keeps malformed and read-only subagent preferences off', () => { + const namespace = { + ...wireNamespaces().find(view => view.ns === 'subagent-model-selection')!, + value: null, + } as unknown as SettingsNamespaceView + const update = vi.fn() + render( + , + ) + + const toggle = screen.getByRole('switch', { name: en.subagentModelSelectionToggle }) + expect(toggle.getAttribute('aria-checked')).toBe('false') + expect((toggle as HTMLButtonElement).disabled).toBe(true) + fireEvent.click(toggle) + expect(update).not.toHaveBeenCalled() + }) + it('renders the unkeyed whole-section provider as an open setup card in the first-run posture', async () => { await mountFirstRun() // Nothing is reachable yet, and DeepSeek has no configured credential and diff --git a/packages/core/agent-loop/README.i18n.yaml b/packages/core/agent-loop/README.i18n.yaml index 2a5445de6b..88d4e6da7d 100644 --- a/packages/core/agent-loop/README.i18n.yaml +++ b/packages/core/agent-loop/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/core/agent-loop/README.md -README.md: 907bbe75fd687388be044916f4c4509e1c552177 -README.zh.md: 76a73155a3d9f811931cf0f86c227e6ab3dc1c32 +README.md: 1b233ae1203171930ef5b58de93ec67381ec4918 +README.zh.md: 81af654072f23c5280e2e14bc891972b5e1f37d5 diff --git a/packages/core/agent-loop/README.md b/packages/core/agent-loop/README.md index 907bbe75fd..1b233ae120 100644 --- a/packages/core/agent-loop/README.md +++ b/packages/core/agent-loop/README.md @@ -42,6 +42,7 @@ interface Config { id: string // required provider?: string model?: string + reasoningEffort?: string // non-empty initial reasoning effort maxTokens?: number // positive per-request output-token cap resumeSessionId?: string // load this persisted session instead of creating one cwd?: string // optional workspace cwd for the fresh session @@ -49,7 +50,7 @@ interface Config { } ``` -Configured agents start automatically. A model call requires both `provider` and `model`; `agent/request` may supply a missing pair before dispatch. An optional positive `maxTokens` seeds each conversation request's output cap and is logged in its request header. `maxParallelToolCalls` bounds every agent's rolling pool for parallel-safe calls and defaults to `10`; it is also the whole of the `agent-loop` Settings section, so a user layer over this entry caps the next tool group without a restart, and a value that is not a positive integer is refused at the write rather than at that group. `agents` is deliberately absent from that section — it is consumed once when the service starts, so a stored change could only look like it had an effect. `cwd` applies only to fresh sessions, while `resumeSessionId` retains persisted metadata. Configured agents use the deployment persona, and programmatic setup can shadow it per agent. This plugin supplies the per-agent `provider`, `model`, and `cwd` prompt variables; harness identity and deployment persona belong to `dsh-system-prompt`. +Configured agents start automatically. A model call requires both `provider` and `model`; `agent/request` may supply a missing pair before dispatch. An optional non-empty `reasoningEffort` seeds the request's reasoning setting; `agent/request` may override it, and adapter resolution validates the effective value recorded in the request header. An optional positive `maxTokens` seeds each conversation request's output cap and is logged in its request header. `maxParallelToolCalls` bounds every agent's rolling pool for parallel-safe calls and defaults to `10`; it is also the whole of the `agent-loop` Settings section, so a user layer over this entry caps the next tool group without a restart, and a value that is not a positive integer is refused at the write rather than at that group. `agents` is deliberately absent from that section — it is consumed once when the service starts, so a stored change could only look like it had an effect. `cwd` applies only to fresh sessions, while `resumeSessionId` retains persisted metadata. Configured agents use the deployment persona, and programmatic setup can shadow it per agent. This plugin supplies the per-agent `provider`, `model`, and `cwd` prompt variables; harness identity and deployment persona belong to `dsh-system-prompt`. ### Internal concrete driver diff --git a/packages/core/agent-loop/README.zh.md b/packages/core/agent-loop/README.zh.md index 76a73155a3..81af654072 100644 --- a/packages/core/agent-loop/README.zh.md +++ b/packages/core/agent-loop/README.zh.md @@ -42,6 +42,7 @@ interface Config { id: string // required provider?: string model?: string + reasoningEffort?: string // non-empty initial reasoning effort maxTokens?: number // positive per-request output-token cap resumeSessionId?: string // load this persisted session instead of creating one cwd?: string // optional workspace cwd for the fresh session @@ -49,7 +50,7 @@ interface Config { } ``` -通过配置创建的 agent 会自动启动。模型调用同时需要 `provider` 和 `model`;`agent/request` 可以在分发前补齐缺失的这一对值。可选的正数 `maxTokens` 会为每次对话请求提供初始输出上限,并记录在请求 header 中。`maxParallelToolCalls` 限制每个 agent 针对并行安全调用使用的滚动池,默认值为 `10`;它同时也是 `agent-loop` Settings 段的全部内容,因此叠加在该条目之上的用户层无需重启即可限制下一组工具调用,而非正整数的值会在写入时被拒绝,而不是到那一组时才失败。`agents` 刻意不在该段中——它在服务启动时被消费一次,所以存储的改动只会看起来生效。`cwd` 仅应用于全新会话,而 `resumeSessionId` 保留持久化元数据。通过配置创建的 agent 使用部署 persona;编程式 setup 可以按 agent 遮蔽它。该插件为每个 agent 提供 `provider`、`model` 和 `cwd` 提示词变量;harness 身份与部署 persona 属于 `dsh-system-prompt`。 +通过配置创建的 agent 会自动启动。模型调用同时需要 `provider` 和 `model`;`agent/request` 可以在分发前补齐缺失的这一对值。可选的非空 `reasoningEffort` 会提供请求的初始推理强度;`agent/request` 可以覆盖它,适配器解析会校验记录在请求 header 中的最终值。可选的正数 `maxTokens` 会为每次对话请求提供初始输出上限,并记录在请求 header 中。`maxParallelToolCalls` 限制每个 agent 针对并行安全调用使用的滚动池,默认值为 `10`;它同时也是 `agent-loop` Settings 段的全部内容,因此叠加在该条目之上的用户层无需重启即可限制下一组工具调用,而非正整数的值会在写入时被拒绝,而不是到那一组时才失败。`agents` 刻意不在该段中——它在服务启动时被消费一次,所以存储的改动只会看起来生效。`cwd` 仅应用于全新会话,而 `resumeSessionId` 保留持久化元数据。通过配置创建的 agent 使用部署 persona;编程式 setup 可以按 agent 遮蔽它。该插件为每个 agent 提供 `provider`、`model` 和 `cwd` 提示词变量;harness 身份与部署 persona 属于 `dsh-system-prompt`。 ### 包内部具体驱动器 diff --git a/packages/core/agent-loop/src/agent.ts b/packages/core/agent-loop/src/agent.ts index 3ef1ec7aa4..6bf7517903 100644 --- a/packages/core/agent-loop/src/agent.ts +++ b/packages/core/agent-loop/src/agent.ts @@ -438,11 +438,12 @@ export class ReactLoopAgent implements Agent { const persistedHeader = session.requestHeader() const persistedConfig = persistedHeader?.config const route = { provider: this.options.provider ?? '', model: this.options.model ?? '' } - const reasoningEffort = persistedConfig?.provider === route.provider + const persistedReasoningEffort = persistedConfig?.provider === route.provider && persistedConfig.model === route.model && persistedHeader?.adapterDefaults?.reasoningEffort !== true ? persistedConfig.reasoningEffort : undefined + const reasoningEffort = this.options.reasoningEffort ?? persistedReasoningEffort const maxTokens = this.options.maxTokens const seedConfig = deepFreeze(structuredClone( this.requestHeaderLogged diff --git a/packages/core/agent-loop/src/index.ts b/packages/core/agent-loop/src/index.ts index 371154a7c9..38ee7dfef5 100644 --- a/packages/core/agent-loop/src/index.ts +++ b/packages/core/agent-loop/src/index.ts @@ -19,7 +19,7 @@ import type { ResumeAgentOptions, SessionStartSource, } from '@deepseek-ai/dsh-agent' -import { errorChain } from '@deepseek-ai/dsh-llm' +import { errorChain, ReasoningEffortId } from '@deepseek-ai/dsh-llm' import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings' import { SessionId, SessionPreparation } from '@deepseek-ai/dsh-session' import type { Session, SessionHeader } from '@deepseek-ai/dsh-session' @@ -304,6 +304,7 @@ export class AgentLoop extends Service implements AgentFactory { sessionId: z.string().min(1), provider: z.string(), model: z.string(), + reasoningEffort: z.string().min(1) as z>, maxTokens: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER), cwd: z.string(), resumeSessionId: z.string(), diff --git a/packages/core/agent-loop/tests/loop.spec.ts b/packages/core/agent-loop/tests/loop.spec.ts index 2105b86f42..4082b43452 100644 --- a/packages/core/agent-loop/tests/loop.spec.ts +++ b/packages/core/agent-loop/tests/loop.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import LlmRuntime, { createUserMessage, CallId, LlmError, StreamChunk } from '@deepseek-ai/dsh-llm' +import LlmRuntime, { createUserMessage, CallId, LlmError, ReasoningEffortId, StreamChunk } from '@deepseek-ai/dsh-llm' import SessionStore, { SessionId, TurnEndReason } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools' @@ -77,6 +77,34 @@ describe('agent loop', () => { expect(adapter.requests[0]?.maxTokens).toBe(256) }) + it('seeds an AgentOptions reasoning effort into the first model request', async () => { + const effort = ReasoningEffortId('high') + const adapter = new MockAdapter([textResponse('reasoned')], { + efforts: [{ id: effort, name: 'High' }], + defaultEffort: effort, + }) + const ctx = await harness(adapter) + const agent = ctx.agentLoop.create( + SessionId('configured-reasoning-effort'), + { provider: 'mock', model: 'mock', reasoningEffort: effort }, + ) + + send(agent, 'use the configured reasoning effort') + await waitForIdle(ctx, agent) + + expect(adapter.requests[0]?.reasoningEffort).toBe(effort) + }) + + it('validates reasoning effort in declarative agent config', () => { + const effort = ReasoningEffortId('high') + expect(AgentLoop.Config({ + agents: [{ id: 'configured-agent', reasoningEffort: effort }], + }).agents[0]?.reasoningEffort).toBe(effort) + expect(() => AgentLoop.Config({ + agents: [{ id: 'configured-agent', reasoningEffort: ReasoningEffortId('') }], + })).toThrow() + }) + it('cancels queued wakeup work together with an active maintenance task', async () => { const adapter = new MockAdapter([textResponse('park reply')]) const ctx = await harness(adapter) @@ -1429,7 +1457,11 @@ describe('agent loop', () => { }) it('creates agents from config on startup', async () => { - const adapter = new MockAdapter([textResponse('from config')]) + const effort = ReasoningEffortId('high') + const adapter = new MockAdapter([textResponse('from config')], { + efforts: [{ id: effort, name: 'High' }], + defaultEffort: effort, + }) const ctx = new Context() await ctx.plugin(LlmRuntime) await ctx.plugin(SessionStore) @@ -1437,7 +1469,7 @@ describe('agent loop', () => { await ctx.plugin(ToolRuntime) await ctx.plugin(AgentRegistry) await ctx.plugin(AgentLoop, { - agents: [{ id: SessionId('config-agent'), provider: 'mock', model: 'mock' }], + agents: [{ id: SessionId('config-agent'), provider: 'mock', model: 'mock', reasoningEffort: effort }], }) ctx.llm.registerAdapter(['mock'], adapter) @@ -1451,6 +1483,9 @@ describe('agent loop', () => { send(agent, 'hi') await waitForIdle(ctx, agent) expect(adapter.requests).toHaveLength(1) + expect(adapter.requests[0]?.reasoningEffort).toBe(effort) + const header = agent.session.events.find(event => event.type === 'request/header') + expect(header?.type === 'request/header' && header.data.header.config.reasoningEffort).toBe(effort) }) it('attaches config agent cwd to the fresh session header', async () => { diff --git a/packages/core/agent/README.i18n.yaml b/packages/core/agent/README.i18n.yaml index 4dd5627b2e..a9d78f3fad 100644 --- a/packages/core/agent/README.i18n.yaml +++ b/packages/core/agent/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/core/agent/README.md -README.md: f6b698e93b254c97786155e7d2c7e81f07c0d981 -README.zh.md: 5e3da8a5d09d06fa4fc9726406aae9c6d5e69c3e +README.md: 70b396d787de5d95332c379ff20ab92c64065857 +README.zh.md: fee72f3cd1fb456ae639d6444fe3fe914c41220a diff --git a/packages/core/agent/README.md b/packages/core/agent/README.md index f6b698e93b..70b396d787 100644 --- a/packages/core/agent/README.md +++ b/packages/core/agent/README.md @@ -14,7 +14,7 @@ Tracks live agents and carries the initiating Agent through asynchronous driver The scoped-registration surface: `Agent.ctx` is the agent's scope context (`dsh-scope`, key = the agent) — register tools/sections/variables/listeners through it for that agent alone, all unwound on disposal. `agentEvents(ctx, agent)` is the fused dispatcher for ordinary agent-subject operations (carrier + injected subject in one move); its notification mode invokes every listener and contains both synchronous throws and returned-promise rejections. The registry lifecycle pair reuses one stable routing carrier. `assembleContextFor(agent)` builds the per-agent assembly context (`agent` + `scope` together). `installModelSelection(agentCtx, selection)` snapshots a mutable provider/model/reasoning-effort selection during prompt assembly, applies its provider and model to prompt variables, and applies the complete selection to request routing for one step; an absent selected effort clears an inherited effort so adapter/provider defaults apply. `CreateAgentOptions.setup(agentCtx)` and `ResumeAgentOptions.setup(agentCtx)` compose a fresh or resumed agent's scoped world while both objects remain unpublished. Setup is trusted, composition-only same-process code: drive the agent only after creation resolves. -`AgentOptions` supplies the initial provider/model route and an optional positive `maxTokens` output cap. The concrete loop resolves any exact-model adapter default, records the effective cap in the request header, and applies it to each conversation-model request; an explicit Agent option wins, while omission leaves the adapter or provider route default in control. +`AgentOptions` supplies the initial provider/model route, optional adapter-owned `reasoningEffort`, and optional positive `maxTokens` output cap. The concrete loop validates exact-model reasoning support, resolves adapter defaults, records the effective values in the request header, and applies them to each conversation-model request; an explicit Agent option wins, while omission leaves the adapter or provider route default in control. - `ctx.agents.register(agent: Agent): () => void` — record an **already-constructed** agent. Disposed with the calling fiber. - Advanced ordered lifecycle: `enter(agent, owner): () => void` enforces `agent.id === agent.session.id`, performs the authoritative ID collision check, and inserts without announcing; `owner` explicitly records the live creator-agent relation (or `undefined` for a root), independently of durable session lineage. `announce(agent)` emits `agent/created` exactly once. A detach requested synchronously by a creation listener is deferred until that dispatch unwinds, and every detach checks the captured entry object, so a stale capability cannot delete a later same-ID replacement. The async factory uses this split; ordinary plugins use `register()`. diff --git a/packages/core/agent/README.zh.md b/packages/core/agent/README.zh.md index 5e3da8a5d0..fee72f3cd1 100644 --- a/packages/core/agent/README.zh.md +++ b/packages/core/agent/README.zh.md @@ -14,7 +14,7 @@ Agent 接口、注册表、进程本地发起方作用域,以及 `agent/*` 事 带作用域的注册接口:`Agent.ctx` 是 agent 的作用域上下文(`dsh-scope`,键 = 该 agent)。通过它注册工具/段/变量/监听器,只对该 agent 生效,并在 dispose(资源释放)时全部撤销。`agentEvents(ctx, agent)` 是普通 agent 主体操作的融合分发器(一次完成载体 + 注入主体);其通知 mode 会调用每个监听器,并同时收容同步抛出和返回 Promise 的拒绝。注册表生命周期对复用一个稳定路由载体。`assembleContextFor(agent)` 构建按 agent 的组装上下文(同时包含 `agent` + `scope`)。`installAgentLlmTarget(agentCtx, target)` 在提示词组装期间快照可变的提供方/模型/推理(reasoning)强度选择,将路由应用到提示词变量,并将完整目标应用到一个步骤的请求路由;如果没有选定推理强度,则会清除继承的推理强度,使该目标使用适配器/提供方默认值。`CreateAgentOptions.setup(agentCtx)` 和 `ResumeAgentOptions.setup(agentCtx)` 在新建或恢复的 agent 尚未发布时,组合其带作用域的世界。Setup 是受信任、仅用于组合的同进程代码:只有创建完成后才能驱动 agent。 -`AgentOptions` 提供初始的提供方/模型路由,以及可选的正数 `maxTokens` 输出上限。具体循环会解析确切模型的适配器默认值,把生效上限记录到请求 header,并应用到每次对话模型请求;显式 Agent 选项优先,省略时由适配器或提供方路由默认值控制。 +`AgentOptions` 提供初始的提供方/模型路由、可选且由适配器定义的 `reasoningEffort`,以及可选的正数 `maxTokens` 输出上限。具体循环会校验确切模型支持的推理强度、解析适配器默认值,把生效值记录到请求 header,并应用到每次对话模型请求;显式 Agent 选项优先,省略时由适配器或提供方路由默认值控制。 - `ctx.agents.register(agent: Agent): () => void`:记录一个 **已经构造完成** 的 agent。随调用 fiber dispose。 - 高级有序生命周期:`enter(agent, owner): () => void` 强制 `agent.id === agent.session.id`,执行权威 ID 冲突检查,并在不通知的情况下插入;`owner` 显式记录实时创建方 agent 关系(根 agent 为 `undefined`),与持久会话谱系无关。`announce(agent)` 恰好发出一次 `agent/created`。创建监听器同步请求的 detach 会延后到该次分发结束;每次 detach 都会检查捕获的条目对象,因此陈旧能力无法删除后续使用同一 ID 的替代项。异步工厂使用这一拆分;普通插件使用 `register()`。 diff --git a/packages/core/agent/src/runtime-types.ts b/packages/core/agent/src/runtime-types.ts index df7449d406..3f8f7c512b 100644 --- a/packages/core/agent/src/runtime-types.ts +++ b/packages/core/agent/src/runtime-types.ts @@ -7,7 +7,7 @@ import type { Context } from '@deepseek-ai/cordis' import type { Scoped } from '@deepseek-ai/dsh-scope' -import type { LlmCallConfig, LlmFailure, ResolvedRetryPolicy } from '@deepseek-ai/dsh-llm' +import type { LlmCallConfig, LlmFailure, ReasoningEffortId, ResolvedRetryPolicy } from '@deepseek-ai/dsh-llm' import type { AgentCancelCause, Session, UserMessage } from '@deepseek-ai/dsh-session' export type { AgentCancelCause } from '@deepseek-ai/dsh-session' import type { Inbox } from './inbox.ts' @@ -27,6 +27,8 @@ export interface AgentOptions { provider?: string /** Model id interpreted by the selected provider adapter. */ model?: string + /** Adapter-owned reasoning effort for the selected provider/model route. */ + reasoningEffort?: ReasoningEffortId /** Maximum output tokens for each conversation-model request. */ maxTokens?: number } diff --git a/packages/core/session/src/known-event-types.ts b/packages/core/session/src/known-event-types.ts index b4e7117541..c005211567 100644 --- a/packages/core/session/src/known-event-types.ts +++ b/packages/core/session/src/known-event-types.ts @@ -49,6 +49,7 @@ export const KNOWN_SESSION_EVENT_TYPES: ReadonlySet = new Set([ 'step/end', 'step/start', 'subagent/descriptor', + 'subagent/model-selection-enabled', 'team/member', 'team/message/delivered', 'team/message/queued', diff --git a/packages/core/system-prompt/README.i18n.yaml b/packages/core/system-prompt/README.i18n.yaml index dd68b6a139..c34ff9547a 100644 --- a/packages/core/system-prompt/README.i18n.yaml +++ b/packages/core/system-prompt/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/core/system-prompt/README.md -README.md: d750a507e628e7609af542227e4528d4d4934ce8 -README.zh.md: ec5b32d742b96c8044a3707c35f15e30ba642b4f +README.md: a52aa3e4c2782993fed5a525cc827aba4e3eaeb0 +README.zh.md: cf0ba43aa2f2f47ea61ef13c74fbf182fbd9f2ee diff --git a/packages/core/system-prompt/README.md b/packages/core/system-prompt/README.md index d750a507e6..a52aa3e4c2 100644 --- a/packages/core/system-prompt/README.md +++ b/packages/core/system-prompt/README.md @@ -11,6 +11,7 @@ System prompt assembly registry. Plugins contribute ordered sections, tool schem | `includeHarnessIdentity` | `true` | Include the fixed `You are an AI agent powered by DeepSeek Harness.` order-−100 opener. Set false only when a compatibility deployment owns the complete system prompt. | | `includeRuntimeContext` | `true` | Include ordered dynamic contexts in assembly. When false, context providers are not evaluated and contexts added by `system-prompt/assemble` listeners are discarded after the waterfall; other services and their enforcement remain active. | | `persona` | `''` | The global deployment-persona default: the ONE config-authored prompt fragment, rendered as the order-0 `deployment:persona` section unless an agent-scoped contribution shadows it. A template — complete `{{…}}` groups are interpreted strictly against the registered variables (the shipped loop registers `{{model}}`/`{{cwd}}`), with no escape syntax for literal braces yet. Empty ⇒ the section is dropped at render. | +| `personaComplete` | `false` | Treat `persona` as the complete system prompt after assembly. Other sections remain registered but are omitted from model requests; tool schemas and variables remain available. | | `toolOrder` | — | Explicit model-facing tool order, as a list of `ToolSchema.name`s with one `''` rest entry (`TOOL_ORDER_REST`): listed tools take their listed position, unlisted tools land at the rest entry in lexicographic name order. Absent ⇒ plain lexicographic name order. Applied to the collected tools BEFORE the `system-prompt/assemble` waterfall — like the sections' `order` sort, it canonicalizes what the registry contributed (registration order is a plugin-load artifact), and a waterfall listener that mutates the list owns the determinism of what it emits. Misconfiguration fails loud: a list without exactly one rest entry, or with duplicates, throws at load; a listed name with no registered tool rejects every `assemble()`; a tool provider returning the reserved rest-entry name also rejects. Under the shipped loop the turn fails before any model request. Why a central list and not per-plugin weights: [Explicit model-facing tool order](../../../.agents/notes/implemented/feature/2026-07-06-explicit-tool-order.md). | ## Service: `SystemPrompt` (ctx key: `systemPrompt`) diff --git a/packages/core/system-prompt/README.zh.md b/packages/core/system-prompt/README.zh.md index ec5b32d742..cf0ba43aa2 100644 --- a/packages/core/system-prompt/README.zh.md +++ b/packages/core/system-prompt/README.zh.md @@ -11,6 +11,7 @@ | `includeHarnessIdentity` | `true` | 是否包含顺序为 −100 的固定开场白 `You are an AI agent powered by DeepSeek Harness.`。仅当兼容性部署拥有完整系统提示词时设为 false。 | | `includeRuntimeContext` | `true` | 是否在组装中包含有序动态上下文。设为 false 时不会求值上下文提供方,并会在 waterfall 后丢弃 `system-prompt/assemble` 监听器添加的上下文;其他服务及其强制机制仍然生效。 | | `persona` | `''` | 全局部署 persona 默认值:唯一由配置提供的提示词片段,渲染为顺序为 0 的 `deployment:persona` 段,除非 agent 作用域的贡献将其遮蔽。它是模板,完整的 `{{…}}` 组会严格按已注册变量解释(随附循环注册 `{{model}}`/`{{cwd}}`),目前没有表达字面量花括号的转义语法。为空 ⇒ 渲染时删除该段。 | +| `personaComplete` | `false` | 在组装后将 `persona` 作为完整系统提示词。其他段仍保持注册,但不会进入模型请求;工具 schema 与变量仍然可用。 | | `toolOrder` | 无 | 显式指定面向模型的工具顺序。该列表由 `ToolSchema.name` 组成,并且必须恰好包含一个 `''` 其余项标记(`TOOL_ORDER_REST`):已列工具按列表位置排列,未列工具则按名称字典序插入该标记所在的位置。缺席 ⇒ 直接按名称字典序排列。该顺序会在 `system-prompt/assemble` waterfall(瀑布式事件)之前应用于已收集的工具。与段的 `order` 排序一样,它会规范化注册表贡献的内容;注册顺序只是插件加载时序的产物。修改列表的 waterfall 监听器对其输出的确定性负责。配置错误会明确失败:列表没有恰好一个其余项或存在重复项,会在加载时抛出;已列名称没有对应已注册工具,会使每次 `assemble()` 被拒绝;工具提供方返回保留的其余项名称也会被拒绝。在随附循环下,轮次会在任何模型请求前失败。为何采用中心列表而非每插件权重,见[显式面向模型工具顺序](../../../.agents/notes/implemented/feature/2026-07-06-explicit-tool-order.zh.md)。 | ## 服务:`SystemPrompt`(ctx 键:`systemPrompt`) diff --git a/packages/core/system-prompt/src/index.ts b/packages/core/system-prompt/src/index.ts index ffc052e0b9..ec36b32432 100644 --- a/packages/core/system-prompt/src/index.ts +++ b/packages/core/system-prompt/src/index.ts @@ -193,6 +193,8 @@ export interface Config { * `deployment:persona` shadows it; `{{variable}}` references are strict. */ persona?: string + /** Treat the deployment persona as the complete system prompt (default false). */ + personaComplete?: boolean /** * Model-facing tool names in order, with {@link TOOL_ORDER_REST} exactly once. * Invalid fields fail at load and unknown names fail at assembly; known names @@ -340,6 +342,7 @@ export class SystemPrompt extends Service { includeHarnessIdentity: z.boolean().default(true), includeRuntimeContext: z.boolean().default(true), persona: z.string().default(''), + personaComplete: z.boolean().default(false), // Preserve omission because an explicit empty order lacks the rest marker. toolOrder: z.array(z.string()).default(undefined as unknown as string[]), }) @@ -366,6 +369,7 @@ export class SystemPrompt extends Service { order: PERSONA_ORDER, // The fallback narrows the optional input type; the schema already defaults it. text: config.persona ?? '', + complete: config.personaComplete ?? false, }) if (!(config.includeRuntimeContext ?? true)) this.suppressRuntimeContext() } diff --git a/packages/core/system-prompt/tests/system-prompt.spec.ts b/packages/core/system-prompt/tests/system-prompt.spec.ts index cf196892a7..c4018103d3 100644 --- a/packages/core/system-prompt/tests/system-prompt.spec.ts +++ b/packages/core/system-prompt/tests/system-prompt.spec.ts @@ -49,6 +49,21 @@ describe('SystemPrompt', () => { expect(renderPrompt(assembly)).toBe('You are a helpful software engineer assistant.') }) + it('can make the deployment persona the complete system prompt', async () => { + const ctx = new Context() + await ctx.plugin(SystemPrompt, { + persona: 'You are a focused SDK agent.', + personaComplete: true, + }) + ctx.systemPrompt.section({ name: 'tool:future', order: 100, text: 'Future tool guidance.' }) + + const assembly = await ctx.systemPrompt.assemble() + expect(assembly.sections).toEqual([ + { name: 'deployment:persona', text: 'You are a focused SDK agent.' }, + ]) + expect(renderPrompt(assembly)).toBe('You are a focused SDK agent.') + }) + it('can suppress runtime context without evaluating providers or accepting waterfall additions', async () => { const ctx = new Context() await ctx.plugin(SystemPrompt, { includeRuntimeContext: false }) diff --git a/packages/core/tools/tests/gen-tool-catalog.spec.ts b/packages/core/tools/tests/gen-tool-catalog.spec.ts index ce2007c34b..38d68deae6 100644 --- a/packages/core/tools/tests/gen-tool-catalog.spec.ts +++ b/packages/core/tools/tests/gen-tool-catalog.spec.ts @@ -30,7 +30,7 @@ describe('gen-tool-catalog collectToolCatalog', () => { 'cordis_inspect_query', 'cordis_inspect_self', 'cordis_run', 'cordis_stop', 'cordis_undefine', 'create_goal', 'edit', 'exit_plan_mode', 'followup_task', 'get_goal', 'glob', 'grep', 'interrupt_agent', 'interrupt_agent', 'job_kill', 'job_list', 'job_output', - 'list_agents', 'list_agents', 'lsp', 'pwsh', 'pwsh', 'ralph', + 'list_agents', 'list_agents', 'list_subagent_models', 'lsp', 'pwsh', 'pwsh', 'ralph', 'read', 'read_image', 'report', 'run_code', 'schedule_create', 'schedule_delete', 'schedule_list', 'send_message', 'send_message', 'session_event_read', 'session_event_search', 'session_event_trace', 'session_search', 'session_trace', 'skill', 'spawn_teammate', @@ -88,7 +88,7 @@ describe('gen-tool-catalog collectToolCatalog', () => { // agents surface this one package as both `subagent` and `subagent_fork`. const catalog = await collectToolCatalog() const subagent = catalog.find(entry => entry.pkg === '@deepseek-ai/dsh-tool-subagent') - expect(subagent?.schemas.map(s => s.name)).toEqual(['subagent']) + expect(subagent?.schemas.map(s => s.name)).toEqual(['list_subagent_models', 'subagent']) expect(subagent?.note).toMatch(/subagent_fork/) }) }) diff --git a/packages/examples/README.i18n.yaml b/packages/examples/README.i18n.yaml index c3e1d67e92..c0c43e838d 100644 --- a/packages/examples/README.i18n.yaml +++ b/packages/examples/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/examples/README.md -README.md: 1193ae404bb7f1a69636f45ff36eee82ec648b2c -README.zh.md: 935bbd1329082f857a692df913a89e7007054b63 +README.md: 2d17e60ed990c8cb77ebfd5823ba2f67e63e8837 +README.zh.md: deea149a11dc70a329bb27c6d851d62d46c6d16e diff --git a/packages/examples/README.md b/packages/examples/README.md index 1193ae404b..2d17e60ed9 100644 --- a/packages/examples/README.md +++ b/packages/examples/README.md @@ -8,7 +8,7 @@ Pre-composed plugin bundles a thin leaf `cordis.yml` loads instead of assembling |---|---|---| | [`agent-spine-demo/`](agent-spine-demo/README.md) | `@deepseek-ai/dsh-agent-spine-demo` | Reusable agent-spine bundle | -`agent-spine-demo` is the shared bundle. Product SDK, ACP, and one-shot execution belong to `dsh --profile sdk`, `dsh --profile acp`, and `dsh --profile headless`; no package in this directory provides an application entry. +`agent-spine-demo` is the shared bundle. Product SDK, ACP, and one-shot execution belong to `dsh --profile sdk` / `dsh --profile sdk-minimal`, `dsh --profile acp`, and `dsh --profile headless`; no package in this directory provides an application entry. These packages are not product API. Product seams and entry points remain in their owning groups; demo bundles select concrete compositions. diff --git a/packages/examples/README.zh.md b/packages/examples/README.zh.md index 935bbd1329..deea149a11 100644 --- a/packages/examples/README.zh.md +++ b/packages/examples/README.zh.md @@ -8,7 +8,7 @@ |---|---|---| | [`agent-spine-demo/`](agent-spine-demo/README.zh.md) | `@deepseek-ai/dsh-agent-spine-demo` | 可复用的 agent-spine(智能体主干)组合包 | -`agent-spine-demo` 是共享组合包。产品 SDK、ACP 与一次性执行分别由 `dsh --profile sdk`、`dsh --profile acp` 和 `dsh --profile headless` 提供;本目录没有任何包提供应用入口。 +`agent-spine-demo` 是共享组合包。产品 SDK、ACP 与一次性执行分别由 `dsh --profile sdk`/`dsh --profile sdk-minimal`、`dsh --profile acp` 和 `dsh --profile headless` 提供;本目录没有任何包提供应用入口。 这些包不是产品 API。产品 seam 与产品入口仍位于各自的归属组;演示组合包选择具体组合。 diff --git a/packages/examples/agent-spine-demo/README.i18n.yaml b/packages/examples/agent-spine-demo/README.i18n.yaml index 1144fb7047..dedce4969d 100644 --- a/packages/examples/agent-spine-demo/README.i18n.yaml +++ b/packages/examples/agent-spine-demo/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/examples/agent-spine-demo/README.md -README.md: 509e4e6d6b5885fb0c685e4ee34fe8078944415b -README.zh.md: 6b429d139a40daa516735b1192af8924e8c9f7cd +README.md: 28e1496a8c941f6524ec4b4dfbfad52d17df5d71 +README.zh.md: c2838bb4cf78d6ac863bac2eba4d2e4df335fa55 diff --git a/packages/examples/agent-spine-demo/README.md b/packages/examples/agent-spine-demo/README.md index 509e4e6d6b..28e1496a8c 100644 --- a/packages/examples/agent-spine-demo/README.md +++ b/packages/examples/agent-spine-demo/README.md @@ -55,11 +55,11 @@ This applies the [Service Definition / Service Provider / Consumer separation](. ```ts import type { Config } from '@deepseek-ai/dsh-agent-spine-demo' -// { agents?, maxParallelToolCalls?, includeHarnessIdentity?, includeRuntimeContext?, persona?, toolOrder?, tools?, dshHome?, sessionTitle?, skills?, workspaceContext, toolBash?, jobs?, toolJobs?, goals?, invariants? } +// { agents?, maxParallelToolCalls?, includeHarnessIdentity?, includeRuntimeContext?, persona?, personaComplete?, toolOrder?, tools?, dshHome?, sessionTitle?, skills?, workspaceContext, toolBash?, jobs?, toolJobs?, goals?, invariants? } // workspaceContext requires { maxBytes } or false; the other owner schemas supply defaults. ``` -The bundle forwards each field to the child that owns it. App packages supply any pre-created agents: headless and JSON-RPC compositions create `main`, while the ACP app creates agents on demand at `session/new`. `includeRuntimeContext: false` is forwarded to `dsh-system-prompt` and suppresses all dynamic context snapshots for fresh sessions without disabling their policy services. Prompt, tool, title, skill, agent-instructions, invariant, goal, and task settings retain the schemas and defaults documented by their owning packages; `jobs.maxConcurrentJobsPerOwner` configures the local provider independently of the model-facing `toolJobs` controls. `pickSpineConfig()` copies only fields owned by this bundle, and conflicting `dshHome` values fail during composition. +The bundle forwards each field to the child that owns it. App packages supply any pre-created agents: headless and JSON-RPC compositions create `main`, while the ACP app creates agents on demand at `session/new`. `includeRuntimeContext: false` suppresses all dynamic context snapshots for fresh sessions without disabling their policy services; `personaComplete: true` makes the deployment persona the sole system-prompt section. Prompt, tool, title, skill, agent-instructions, invariant, goal, and task settings retain the schemas and defaults documented by their owning packages; `jobs.maxConcurrentJobsPerOwner` configures the local provider independently of the model-facing `toolJobs` controls. `pickSpineConfig()` copies only fields owned by this bundle, and conflicting `dshHome` values fail during composition. For example, `{ invariants: { enabled: true, package_allowlist: ['^@deepseek-ai/dsh-'], package_blocklist: ['agent-loop$'] } }` keeps the package-owned companions mounted but suppresses the blocked owner. Blocklist matches override allowlist matches; see [`dsh-invariants`](../../runtime-diagnostics/invariants/README.md) for regex and lifecycle rules. diff --git a/packages/examples/agent-spine-demo/README.zh.md b/packages/examples/agent-spine-demo/README.zh.md index 6b429d139a..c2838bb4cf 100644 --- a/packages/examples/agent-spine-demo/README.zh.md +++ b/packages/examples/agent-spine-demo/README.zh.md @@ -55,11 +55,11 @@ ```ts import type { Config } from '@deepseek-ai/dsh-agent-spine-demo' -// { agents?, maxParallelToolCalls?, includeHarnessIdentity?, includeRuntimeContext?, persona?, toolOrder?, tools?, dshHome?, sessionTitle?, skills?, workspaceContext, toolBash?, jobs?, toolJobs?, goals?, invariants? } +// { agents?, maxParallelToolCalls?, includeHarnessIdentity?, includeRuntimeContext?, persona?, personaComplete?, toolOrder?, tools?, dshHome?, sessionTitle?, skills?, workspaceContext, toolBash?, jobs?, toolJobs?, goals?, invariants? } // workspaceContext requires { maxBytes } or false; the other owner schemas supply defaults. ``` -组合包将每个字段转发给拥有它的子节点。应用包提供预创建的 agent:无头和 JSON-RPC 组合会创建 `main`,ACP 应用则在 `session/new` 按需创建 agent。`includeRuntimeContext: false` 会转发给 `dsh-system-prompt`,为新建会话抑制所有动态上下文快照,但不禁用其策略服务。提示词、工具、标题、skill、工作区上下文、不变式、目标和任务设置沿用其所属包记录的 schema 与默认值;`jobs.maxConcurrentJobsPerOwner` 配置本地 Service Provider,并与面向模型的 `toolJobs` 控制工具相互独立。`pickSpineConfig()` 只复制该组合包拥有的字段,`dshHome` 值冲突会在组合时失败。 +组合包将每个字段转发给拥有它的子节点。应用包提供预创建的 agent:无头和 JSON-RPC 组合会创建 `main`,ACP 应用则在 `session/new` 按需创建 agent。`includeRuntimeContext: false` 会为新建会话抑制所有动态上下文快照,但不禁用其策略服务;`personaComplete: true` 会让部署 persona 成为唯一系统提示词段。提示词、工具、标题、skill、工作区上下文、不变式、目标和任务设置沿用其所属包记录的 schema 与默认值;`jobs.maxConcurrentJobsPerOwner` 配置本地 Service Provider,并与面向模型的 `toolJobs` 控制工具相互独立。`pickSpineConfig()` 只复制该组合包拥有的字段,`dshHome` 值冲突会在组合时失败。 例如,`{ invariants: { enabled: true, package_allowlist: ['^@deepseek-ai/dsh-'], package_blocklist: ['agent-loop$'] } }` 会让包拥有的配套插件保持挂载,但抑制被阻止的拥有者。Blocklist 匹配优先于 allowlist 匹配;正则表达式与生命周期规则见 [`dsh-invariants`](../../runtime-diagnostics/invariants/README.zh.md)。 diff --git a/packages/examples/agent-spine-demo/src/index.ts b/packages/examples/agent-spine-demo/src/index.ts index 87098c38da..a60ebd9e43 100644 --- a/packages/examples/agent-spine-demo/src/index.ts +++ b/packages/examples/agent-spine-demo/src/index.ts @@ -70,9 +70,10 @@ export interface GoalConfig { * Bundle config: each field forwarded verbatim to the child that owns it — * `agents` to the agent loop (an app that pre-creates no agents, like the ACP * bridge, simply omits it), `includeHarnessIdentity`, `includeRuntimeContext`, - * `persona`, and `toolOrder` to the system-prompt plugin (the fixed opener, - * dynamic-context policy, deployment persona, and explicit model-facing tool - * order), the `tools` object to the tool registry (its presentation `mode`), + * `persona`, `personaComplete`, and `toolOrder` to the system-prompt plugin + * (the fixed opener, dynamic-context policy, deployment persona completeness, + * and explicit model-facing tool order), the `tools` object to the tool + * registry (its presentation `mode`), * `dshHome` to bash environment and local skill discovery, `sessionTitle` to * the fallback title service, `skills` to the * skill registry/local provider/tool consumer, `workspaceContext` to the @@ -100,6 +101,8 @@ export interface Config { includeRuntimeContext?: SystemPromptConfig['includeRuntimeContext'] /** The deployment persona (see dsh-system-prompt's `Config`). */ persona?: SystemPromptConfig['persona'] + /** Whether the deployment persona is the complete system prompt. */ + personaComplete?: SystemPromptConfig['personaComplete'] /** The explicit model-facing tool order (see dsh-system-prompt's `Config`). */ toolOrder?: SystemPromptConfig['toolOrder'] /** The tool registry's config — its presentation `mode` (see dsh-tools' `Config`). */ @@ -185,6 +188,7 @@ export function pickSpineConfig(config: Omit): Omit): Omit { includeHarnessIdentity: false, includeRuntimeContext: false, persona: 'You are a helpful software engineer assistant.', + personaComplete: true, workspaceContext: false, skills: { enabled: false }, toolBash: false, @@ -723,6 +724,7 @@ describe('dsh-agent-spine-demo bundle', () => { expect(ctx.tools.schemas()).toEqual([]) ctx.systemPrompt.context({ name: 'policy', order: 0, text: 'hidden policy' }) expect((await ctx.systemPrompt.assemble()).contexts).toEqual([]) + ctx.systemPrompt.section({ name: 'hidden', order: 100, text: 'hidden guidance' }) expect(renderPrompt(await ctx.systemPrompt.assemble())) .toBe('You are a helpful software engineer assistant.') @@ -736,6 +738,7 @@ describe('dsh-agent-spine-demo bundle', () => { includeHarnessIdentity: false, includeRuntimeContext: false, persona: 'You are merged.', + personaComplete: true, toolOrder: ['zulu'], tools: { mode: 'native' as const }, dshHome: '/tmp/dsh-home', @@ -754,6 +757,7 @@ describe('dsh-agent-spine-demo bundle', () => { includeHarnessIdentity: appConfig.includeHarnessIdentity, includeRuntimeContext: appConfig.includeRuntimeContext, persona: appConfig.persona, + personaComplete: appConfig.personaComplete, toolOrder: appConfig.toolOrder, tools: appConfig.tools, dshHome: appConfig.dshHome, diff --git a/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-architecture-review/session.jsonl b/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-architecture-review/session.jsonl index 291a18ba1e..7fd433c32a 100644 --- a/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-architecture-review/session.jsonl +++ b/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-architecture-review/session.jsonl @@ -171,7 +171,7 @@ {"type":"session/end-seed","data":{},"seq":169,"time":1787472100000} {"type":"turn/start","data":{"turn":29},"seq":170,"time":1787472100001} {"type":"user/message","data":{"id":"preview-review-user","role":"user","content":[{"type":"text","text":"Review whether the preview fixture is isolated from future WebFS data."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":171,"time":1787472100002} -{"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"fork","label":"Review preview architecture"},"seq":172,"time":1787472100003} +{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"fork","label":"Review preview architecture"},"seq":172,"time":1787472100003} {"type":"step/start","data":{"turn":29,"step":1},"seq":173,"time":1787472100004} {"type":"assistant/message","data":{"turn":29,"step":1,"message":{"id":"preview-review-assistant","role":"assistant","content":[{"type":"text","text":"The bundled fixture is static image content; future WebFS state remains user-owned."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":174,"time":1787472100005} {"type":"step/end","data":{"turn":29,"step":1},"seq":175,"time":1787472100006} diff --git a/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-follow-up-builder/session.jsonl b/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-follow-up-builder/session.jsonl index 6353e854c4..03b884adc6 100644 --- a/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-follow-up-builder/session.jsonl +++ b/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-follow-up-builder/session.jsonl @@ -1,7 +1,7 @@ {"type":"session","version":0,"id":"preview-follow-up-builder","createdAt":1787472200000,"cwd":"/dsh/workspace","parentSession":"preview-showcase","origin":"subagent","delegationDepth":1,"agentPreset":"standard"} {"type":"turn/start","data":{"turn":1},"seq":0,"time":1787472200000} {"type":"user/message","data":{"id":"preview-builder-user","role":"user","content":[{"type":"text","text":"Check that the Preview workspace can support follow-up tasks."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":1,"time":1787472200001} -{"type":"subagent/descriptor","data":{"version":2,"mode":"continuable","provider":"spawn","label":"Continue preview verification"},"seq":2,"time":1787472200002} +{"type":"subagent/descriptor","data":{"version":3,"mode":"continuable","provider":"spawn","label":"Continue preview verification"},"seq":2,"time":1787472200002} {"type":"step/start","data":{"turn":1,"step":1},"seq":3,"time":1787472200003} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"id":"preview-builder-assistant","role":"assistant","content":[{"type":"text","text":"This child is continuable and ready for another verification turn."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":4,"time":1787472200004} {"type":"step/end","data":{"turn":1,"step":1},"seq":5,"time":1787472200005} diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 0798c8f31c..320c44b9aa 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -195,7 +195,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ }, { signature: 'async recompose(agentCtx: Context, id: string): Promise', - description: 'Re-link one agent to a different preset\'s standing composition.\n\nOnly valid while the agent has produced nothing: swapping tools mid conversation would leave logged tool calls the new composition cannot make. The CALLER owns that check — this method does not read session history.\n\nThe swap is a parent re-link, not an unmount: standing mounts are shared and permanent, so the old composition stays for its other agents and the new one is ensured BEFORE the link moves. An unknown or unusable preset therefore throws with the agent exactly as it was — there is no torn-down state to restore. The re-link runs through the binding this roster kept from the agent\'s mount — dsh-scope\'s only re-link authority. An agent that never composed one has nothing to re-link: the switch is then the agent\'s first bind, exactly a mount.', + description: 'Re-link one agent to a different preset\'s standing composition.\n\nOnly valid while the agent has produced nothing: swapping tools mid conversation would leave logged tool calls the new composition cannot make. The CALLER owns that check — this method does not read session history.\n\nThe swap is a parent re-link, not an unmount: standing mounts are shared and permanent, so the old composition stays for its other agents and the new one is ensured BEFORE the link moves. An unknown or unusable preset therefore throws with the agent exactly as it was — there is no torn-down state to restore. The re-link runs through the binding this roster kept from the agent\'s mount — dsh-scope\'s only re-link authority. An agent that never composed one has nothing to re-link: the switch is then the agent\'s first bind, exactly a mount. A committed re-link emits `tools/change` because changing the parent scope changes the Agent\'s resolved tool set without adding or removing registry entries.', parameters: [{ name: 'agentCtx', description: 'the agent\'s scope context.' }, { name: 'id', description: 'the preset to compose the agent from instead.' }], returns: 'the preset now installed.', throws: ['when the preset is unknown or its composition is unusable.'], @@ -459,7 +459,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ { signature: 'readImageRequest( ref: ImageAttachmentRef, policy: ImageRequestPolicy, signal?: AbortSignal, ): Promise', description: 'Generate or read one deterministic model-request version from the stored normalized image.', - parameters: [{ name: 'ref', description: 'durable provider-independent normalized attachment reference.' }, { name: 'policy', description: 'exact route pixel and encoded-byte budget.' }, { name: 'signal', description: 'optional cancellation.' }], + parameters: [{ name: 'ref', description: 'durable provider-independent normalized attachment reference.' }, { name: 'policy', description: 'exact route pixel budget and encoded-byte target; a target no ladder quality meets yields the smallest ladder output.' }, { name: 'signal', description: 'optional cancellation.' }], returns: 'request bytes and the cache/upload identity covering every transform input.', }, ], @@ -1906,6 +1906,19 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ }, ], }, + { + key: 'subagentModelSelection', + summary: 'Singleton settings owner read by delegation tools when an Agent is published.', + description: 'Singleton settings owner read by delegation tools when an Agent is published.', + methods: [ + { + signature: 'currentEnabled(): boolean', + description: 'Read the preference for the next eligible Agent publication.', + parameters: [], + returns: 'whether that Agent should receive model-selectable delegation.', + }, + ], + }, { key: 'subagents', summary: 'Named provider registry with one-shot runs, durable discovery, and continuable-child operations.', @@ -3124,7 +3137,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'AgentOptions', - declaration: 'export interface AgentOptions {\n provider?: string;\n model?: string;\n maxTokens?: number;\n}', + declaration: 'export interface AgentOptions {\n provider?: string;\n model?: string;\n reasoningEffort?: ReasoningEffortId;\n maxTokens?: number;\n}', }, { name: 'AgentPreset', @@ -3416,7 +3429,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'ContinuableSubagentDescriptorData', - declaration: 'export interface ContinuableSubagentDescriptorData extends SubagentDescriptorBase {\n readonly mode: \'continuable\';\n readonly label: string;\n readonly agentProvider?: string;\n readonly agentModel?: string;\n readonly persona?: string;\n readonly toolFilter?: ToolRestriction;\n}', + declaration: 'export interface ContinuableSubagentDescriptorData extends SubagentDescriptorBase {\n readonly mode: \'continuable\';\n readonly label: string;\n readonly agentProvider?: string;\n readonly agentModel?: string;\n readonly agentReasoningEffort?: ReasoningEffortId;\n readonly persona?: string;\n readonly toolFilter?: ToolRestriction;\n}', }, { name: 'CordisDynamicPackageId', @@ -4928,7 +4941,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SubagentCapabilities', - declaration: 'export interface SubagentCapabilities {\n readonly outputSchema: boolean;\n readonly depthLimit: boolean;\n readonly toolFilter: boolean;\n readonly persona: boolean;\n}', + declaration: 'export interface SubagentCapabilities {\n readonly agentOptions: boolean;\n readonly outputSchema: boolean;\n readonly depthLimit: boolean;\n readonly toolFilter: boolean;\n readonly persona: boolean;\n}', }, { name: 'SubagentDescendantListEntry', diff --git a/packages/llm/llm-deepseek/README.i18n.yaml b/packages/llm/llm-deepseek/README.i18n.yaml index 75d595a147..57ecda0a84 100644 --- a/packages/llm/llm-deepseek/README.i18n.yaml +++ b/packages/llm/llm-deepseek/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/llm/llm-deepseek/README.md -README.md: 629a1ebe00e5b14ac575a7b088a2e737e89d0c07 -README.zh.md: d5f11f8f2f8b9053685ba553a69fe71e40458ca3 +README.md: 7433bb75104506ec2409c659f3d30058abc6f9a4 +README.zh.md: 7dcdfeac17b0bfca70a293760061182292edb531 diff --git a/packages/llm/llm-deepseek/README.md b/packages/llm/llm-deepseek/README.md index 629a1ebe00..7433bb7510 100644 --- a/packages/llm/llm-deepseek/README.md +++ b/packages/llm/llm-deepseek/README.md @@ -50,9 +50,9 @@ The package root exposes the Cordis plugin contract and `DeepSeekAdapter`; wire contextWindow: 512000 ``` -The plugin registers the single provider route `deepseek-official` together with its resolved `retryPolicy`; omission resolves to normal mode with five retries. A request selects it with `provider: deepseek-official`; its `model` is passed through as the wire `model` string, so changing DeepSeek models does not require lifecycle-time registration. Omitting `models` advertises `deepseek-v4-flash`, `deepseek-v4-pro`, and the image-capable `deepseek-v4-flash-vision-exp`, each with a 1,000,000-token context window; an explicit list replaces those defaults, while `models: []` advertises none. Catalog entries are exposed through `ctx.llm.listModels('deepseek-official')` for clients such as ACP editors and the Web selector, but remain advisory: unlisted model ids still pass through unchanged as text-only routes. An omitted entry name defaults to its id, and omitted `inputModalities` means `text` only. +The plugin registers the single provider route `deepseek-official` together with its resolved `retryPolicy`; omission resolves to normal mode with five retries. A request selects it with `provider: deepseek-official`; its `model` is passed through as the wire `model` string, so changing DeepSeek models does not require lifecycle-time registration. Omitting `models` advertises `deepseek-v4-flash` as the fast, economical choice for focused work, `deepseek-v4-pro` as the stronger, higher-cost choice for complex or quality-critical work, and the image-capable `deepseek-v4-flash-vision-exp`; each has a 1,000,000-token context window. An explicit list replaces those defaults, while `models: []` advertises none. Catalog entries are exposed through `ctx.llm.listModels('deepseek-official')` for clients such as ACP editors, the Web selector, and model discovery tools, but remain advisory: unlisted model ids still pass through unchanged as text-only routes. An omitted entry name defaults to its id, and omitted `inputModalities` means `text` only. -An image-capable catalog entry declares `inputModalities: [text, image]` and may set `imagePixelBudget` to an exact positive integer or `low`; omission uses 640,000 total pixels, while `low` selects 512 by 512 total pixels. `imageMaxBytes` defaults to 1MiB. The attachment store scales by `min(1, sqrt(pixelBudget / (width * height)))` and rounds inward to keep the pixel count at or below the hard cap, so a 2048 by 1024 normalized attachment becomes about 1130 by 565 instead of a forced square. Request encoders run lazily: low-color images try PNG (palette only without alpha) then WebP 85 and 80, other alpha images try WebP 85 then 80, and other opaque images try JPEG 85 then 80; dimensions shrink only when both quality attempts exceed 1MiB. Concurrent generation of one `variantId` shares one transform. A caller can cancel its own wait without interrupting other waiters; the transform stops when no waiter remains. The adapter normally uploads the exact derived request bytes through `POST /files` and sends `{type: "file", file_id}` blocks. A failed or timed-out file-id resolution rebuilds the whole chat request with those same request versions as base64 data URLs; one request never mixes file ids and inline images. Every retained image is preceded by text naming the complete attachment id and actual request dimensions. When the attachment provider exposes a host object and the current filesystem maps it into the tool execution world, the text also includes that read-only path and the matching extension for a writable copy. This access is resolved independently from the deterministic request version and its `variantId`. The descriptor states that the preview and normalized image may differ from the upload. User, tool-result, agent-loop, compaction, and direct `ctx.llm.stream` requests all use this projection. Text-only routes receive stable attachment placeholders while durable history keeps its image references. +An image-capable catalog entry declares `inputModalities: [text, image]` and may set `imagePixelBudget` to an exact positive integer or `low`; omission uses 640,000 total pixels, while `low` selects 512 by 512 total pixels. `imageMaxBytes` defaults to 1MiB. The attachment store scales by `min(1, sqrt(pixelBudget / (width * height)))` and rounds inward to keep the pixel count at or below the hard cap, so a 2048 by 1024 normalized attachment becomes about 1130 by 565 instead of a forced square. Request encoders run lazily: alpha images try WebP (effort 0) at 85, 75, then 60, and opaque images try JPEG at those qualities; when every quality exceeds 1MiB the smallest output is used. Concurrent generation of one `variantId` shares one transform. A caller can cancel its own wait without interrupting other waiters; the transform stops when no waiter remains. The adapter normally uploads the exact derived request bytes through `POST /files` and sends `{type: "file", file_id}` blocks. A failed or timed-out file-id resolution rebuilds the whole chat request with those same request versions as base64 data URLs; one request never mixes file ids and inline images. Every retained image is preceded by text naming the complete attachment id and actual request dimensions. When the attachment provider exposes a host object and the current filesystem maps it into the tool execution world, the text also includes that read-only path and the matching extension for a writable copy. This access is resolved independently from the deterministic request version and its `variantId`. The descriptor states that the preview and normalized image may differ from the upload. User, tool-result, agent-loop, compaction, and direct `ctx.llm.stream` requests all use this projection. Text-only routes receive stable attachment placeholders while durable history keeps its image references. `maxRequestFilesBytes` and `maxImagesPerRequest` bound the retained request versions at 128MiB and 600 images by default. The byte and count quanta must not exceed their corresponding bounds. Before attachment reads, the adapter uses each route's request-version byte cap as a conservative upper bound and removes the oldest over-budget prefix; only retained normalized attachments are read and transformed. Exact derived lengths are checked again without restoring omitted images. When the byte bound is crossed, the oldest prefix advances past the next 64MiB boundary; 129 one-megabyte images remove the oldest 65 and retain 64MiB, and that prefix stays unchanged until durable history exceeds 192MiB. Count overflow advances independently in `imageOffloadCountQuantum` steps. Each removed image becomes its own model-visible placeholder with its display name or attachment id and, when available, normalized dimensions, media type, and current read-only local path. This high-watermark projection avoids changing an old request prefix after every new image. @@ -66,7 +66,7 @@ Concurrent resolution of one scoped `variantId` shares one Files upload with wai `maxTokens` is the adapter-configured output cap for conversation requests and defaults to 256,000. A catalog entry may carry its own `maxTokens`, which wins for that model; an entry without one, and any unlisted pass-through id, resolve to the profile value, so adding a per-model cap changes one model rather than the route. Exact-model resolution exposes the winner as `defaultMaxTokens`; `LlmRuntime` materializes that value into `GenerateOptions.maxTokens` before the agent loop writes `request/header`, so the wire request remains reconstructable. An explicit request or `AgentOptions.maxTokens` value wins and is serialized as `max_tokens`. The adapter does not clamp this request budget against `contextWindow`; deployments with a smaller context or provider output limit must configure a compatible `maxTokens`. -The same exact-model result exposes ordered `off`, `low`, `high`, and `max` efforts under `reasoning` for every pass-through model when deployment policy permits thinking. `reasoningEffort` selects the deployment default and falls back to `high` when omitted. `agent/request` can replace it on each conversation step; the resolved value is logged in `request/header`. `low`, `high`, and `max` enable thinking and serialize as the same official top-level `reasoning_effort` value; adapter-owned `off` instead serializes `thinking.type: disabled` and omits `reasoning_effort`. An unsupported value fails with `UNSUPPORTED_REASONING_EFFORT` before network I/O. +The same exact-model result exposes ordered `off`, `low`, `high`, and `max` efforts with selection guidance under `reasoning` for every pass-through model when deployment policy permits thinking. `reasoningEffort` selects the advertised default and falls back to `high` when omitted. `agent/request` can replace it on each conversation step; the resolved value is logged in `request/header`. `low`, `high`, and `max` enable thinking and serialize as the same official top-level `reasoning_effort` value; adapter-owned `off` instead serializes `thinking.type: disabled` and omits `reasoning_effort`. An unsupported value fails with `UNSUPPORTED_REASONING_EFFORT` before network I/O. `thinking: disabled` is a deployment lock that publishes only `off` with `off` as its default. Omitting `reasoningEffort` or configuring it as `off` is valid; configuring `low`, `high`, or `max` fails plugin loading, and a direct per-request attempt to enable thinking fails before network I/O. A request with `GenerateOptions.purpose: 'session-title'` also forces thinking disabled and omits the already-resolved effort, reserving its bounded output for visible title text without changing conversation or compaction defaults. diff --git a/packages/llm/llm-deepseek/README.zh.md b/packages/llm/llm-deepseek/README.zh.md index d5f11f8f2f..7dcdfeac17 100644 --- a/packages/llm/llm-deepseek/README.zh.md +++ b/packages/llm/llm-deepseek/README.zh.md @@ -50,9 +50,9 @@ harness LLM(大语言模型)seam 的 DeepSeek chat-completions 适配器: contextWindow: 512000 ``` -该插件注册唯一提供方路由 `deepseek-official`,并一同注册解析后的 `retryPolicy`;省略时会解析为 normal 模式并重试五次。请求使用 `provider: deepseek-official` 选择该路由;其 `model` 会作为协议 `model` 字符串原样传递,因此更改 DeepSeek 模型不需要生命周期时注册。省略 `models` 会公布 `deepseek-v4-flash`、`deepseek-v4-pro` 与支持图片输入的 `deepseek-v4-flash-vision-exp`,三者的上下文窗口均为 1,000,000 token;显式列表会替换这些默认值,`models: []` 则不公布任何模型。Catalog 配置项通过 `ctx.llm.listModels('deepseek-official')` 公开给 ACP(Agent Client Protocol)编辑器和 Web 选择器等客户端,但仍只提供建议:未列出模型 id 仍原样传递,并按纯文本路由处理。省略配置项 name 默认为其 id,省略 `inputModalities` 则表示仅支持 `text`。 +该插件注册唯一提供方路由 `deepseek-official`,并一同注册解析后的 `retryPolicy`;省略时会解析为 normal 模式并重试五次。请求使用 `provider: deepseek-official` 选择该路由;其 `model` 会作为协议 `model` 字符串原样传递,因此更改 DeepSeek 模型不需要生命周期时注册。省略 `models` 会公布适合聚焦任务、快速且经济的 `deepseek-v4-flash`,适合复杂或质量关键任务、更强但成本更高的 `deepseek-v4-pro`,以及支持图片输入的 `deepseek-v4-flash-vision-exp`;三者的上下文窗口均为 1,000,000 token。显式列表会替换这些默认值,`models: []` 则不公布任何模型。Catalog 配置项通过 `ctx.llm.listModels('deepseek-official')` 公开给 ACP(Agent Client Protocol)编辑器、Web 选择器和模型发现工具等客户端,但仍只提供建议:未列出模型 id 仍原样传递,并按纯文本路由处理。省略配置项 name 默认为其 id,省略 `inputModalities` 则表示仅支持 `text`。 -支持图片的 catalog 配置项声明 `inputModalities: [text, image]`,并可把 `imagePixelBudget` 设为确切正整数或 `low`;省略时使用总像素 640,000,`low` 选择总像素 512×512。`imageMaxBytes` 默认值为 1MiB。附件存储按 `min(1, sqrt(pixelBudget / (width * height)))` 缩放,并向预算内取整,确保总像素不超过硬上限。因此 2048×1024 规范化附件会得到约 1130×565 的请求版本,而不会被强制变成正方形。请求编码按需执行:低色数图片先尝试 PNG,只有不带 alpha 通道时才使用 palette,再尝试质量 85 和 80 的 WebP;其他透明图片依次尝试质量 85 和 80 的 WebP;其他非透明图片依次尝试质量 85 和 80 的 JPEG。两个质量档均超过 1MiB 时才缩小尺寸。同一 `variantId` 的并发生成共享一次变换。调用方可以单独取消等待,不会中断其他等待方;没有等待方时才会停止变换。适配器通常通过 `POST /files` 上传确切的派生请求字节,再发送 `{type: "file", file_id}` 块。File ID 解析失败或超时后,适配器会用相同请求版本的 base64 data URL 重新组装整个 chat 请求;同一请求不会混用 file ID 和内联图片。每张保留图片前都有文本,写明完整附件 ID 和实际请求尺寸。附件提供方给出宿主对象且当前文件系统能够将其映射到工具执行环境时,文本还会给出该只读路径,并指出复制到可写路径时应使用的匹配扩展名。该访问方式独立于确定性的请求版本及其 `variantId`。描述也会说明预览和规范化图片可能与上传图片不同。User、工具结果、agent loop、压缩和直接 `ctx.llm.stream` 请求都使用该投影。纯文本路由会收到稳定的附件占位文本,持久历史继续保留图片引用。 +支持图片的 catalog 配置项声明 `inputModalities: [text, image]`,并可把 `imagePixelBudget` 设为确切正整数或 `low`;省略时使用总像素 640,000,`low` 选择总像素 512×512。`imageMaxBytes` 默认值为 1MiB。附件存储按 `min(1, sqrt(pixelBudget / (width * height)))` 缩放,并向预算内取整,确保总像素不超过硬上限。因此 2048×1024 规范化附件会得到约 1130×565 的请求版本,而不会被强制变成正方形。请求编码按需执行:透明图片依次尝试质量 85、75、60 的 WebP(effort 0);非透明图片依次尝试这些质量的 JPEG。全部质量档都超过 1MiB 时使用其中最小的产物。同一 `variantId` 的并发生成共享一次变换。调用方可以单独取消等待,不会中断其他等待方;没有等待方时才会停止变换。适配器通常通过 `POST /files` 上传确切的派生请求字节,再发送 `{type: "file", file_id}` 块。File ID 解析失败或超时后,适配器会用相同请求版本的 base64 data URL 重新组装整个 chat 请求;同一请求不会混用 file ID 和内联图片。每张保留图片前都有文本,写明完整附件 ID 和实际请求尺寸。附件提供方给出宿主对象且当前文件系统能够将其映射到工具执行环境时,文本还会给出该只读路径,并指出复制到可写路径时应使用的匹配扩展名。该访问方式独立于确定性的请求版本及其 `variantId`。描述也会说明预览和规范化图片可能与上传图片不同。User、工具结果、agent loop、压缩和直接 `ctx.llm.stream` 请求都使用该投影。纯文本路由会收到稳定的附件占位文本,持久历史继续保留图片引用。 `maxRequestFilesBytes` 和 `maxImagesPerRequest` 限制请求中保留的请求版本,默认值分别为 128MiB 和 600 张。字节和数量步长不得超过对应上限。读取附件前,适配器以路由的请求版本字节上限作为保守上界,移除超预算的最旧前缀,只读取并转换保留的规范化附件。系统随后用确切派生长度再次检查,但不会重新加入已省略图片。字节数越过上限时,被移除的最旧前缀会越过下一个 64MiB 边界。由 1MiB 图片组成的历史达到 129MiB 时会移除最旧的 65 张并保留 64MiB;直到持久历史超过 192MiB,这个前缀才再次变化。图片数量超限时则按 `imageOffloadCountQuantum` 独立递增。每张被移除的图片都有自己的模型可见占位文本,其中包含显示名称或附件 ID;如果当前提供方支持,还会包含规范化尺寸、媒体类型和当前只读本地路径。这种定量投影不会因每新增一张图片就改写较早的请求前缀。 @@ -66,7 +66,7 @@ harness LLM(大语言模型)seam 的 DeepSeek chat-completions 适配器: `maxTokens` 是适配器为对话请求配置的输出上限,默认值为 256,000。Catalog 配置项可以自带 `maxTokens`,它对该模型胜出;不含该上限的配置项以及任何未列出原样传递 id 都解析为 profile 值,因此新增按模型的上限只改变一个模型,而非整条路由。确切模型解析会将胜出值公开为 `defaultMaxTokens`;`LlmRuntime` 会在 agent loop(智能体循环)写入 `request/header` 前,将该值填入 `GenerateOptions.maxTokens`,从而仍可根据持久记录重建协议请求。显式的请求值或 `AgentOptions.maxTokens` 值优先,并会序列化为 `max_tokens`。适配器不会根据 `contextWindow` 自动调低该请求预算;上下文或提供方输出上限较小的部署必须配置与其相容的 `maxTokens`。 -同一确切模型结果会在部署策略允许思考时,为每个原样传递模型在 `reasoning` 下公开有序的 `off`、`low`、`high` 和 `max` 推理(reasoning)强度。`reasoningEffort` 选择部署默认值,省略时回退为 `high`。`agent/request` 可以在每个会话步骤替换它;解析后的值会记录在 `request/header`。`low`、`high` 和 `max` 会启用思考,并以同名值序列化为官方顶层 `reasoning_effort`;适配器持有的 `off` 则序列化为 `thinking.type: disabled`,且省略 `reasoning_effort`。不支持的值会在网络 I/O 前以 `UNSUPPORTED_REASONING_EFFORT` 失败。 +同一确切模型结果会在部署策略允许思考时,为每个原样传递模型在 `reasoning` 下公开有序且附带选择指引的 `off`、`low`、`high` 和 `max` 推理(reasoning)强度。`reasoningEffort` 选择公布的默认值,省略时回退为 `high`。`agent/request` 可以在每个会话步骤替换它;解析后的值会记录在 `request/header`。`low`、`high` 和 `max` 会启用思考,并以同名值序列化为官方顶层 `reasoning_effort`;适配器持有的 `off` 则序列化为 `thinking.type: disabled`,且省略 `reasoning_effort`。不支持的值会在网络 I/O 前以 `UNSUPPORTED_REASONING_EFFORT` 失败。 `thinking: disabled` 是部署锁定:它只公布 `off`,并以 `off` 为默认值。省略 `reasoningEffort` 或将其配置为 `off` 均有效;配置 `low`、`high` 或 `max` 会使插件加载失败,直接按请求启用思考也会在网络 I/O 前失败。携带 `GenerateOptions.purpose: 'session-title'` 的请求也会强制禁用思考并省略已解析的推理强度,将有界输出保留给可见标题文本,不改变会话或压缩(compaction)默认值。 diff --git a/packages/llm/llm-deepseek/src/adapter.ts b/packages/llm/llm-deepseek/src/adapter.ts index fccf6793d8..35212d32d6 100644 --- a/packages/llm/llm-deepseek/src/adapter.ts +++ b/packages/llm/llm-deepseek/src/adapter.ts @@ -61,7 +61,7 @@ export interface DeepSeekCatalogModel { inputModalities?: ModelModality[] /** Total-pixel budget for one deterministic request preview, or the 512-by-512 `low` preset. */ imagePixelBudget?: number | 'low' - /** Encoded-byte cap for one deterministic request preview. */ + /** Encoded-byte target for one deterministic request preview; the smallest quality-ladder output is used when no quality fits. */ imageMaxBytes?: number } @@ -150,7 +150,7 @@ export const DEFAULT_MAX_IMAGES_PER_REQUEST = 600 export const DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET = 640_000 /** Total-pixel budget matching provider low-detail image input. */ export const DEFAULT_LOW_DETAIL_IMAGE_PIXEL_BUDGET = 512 * 512 -/** Encoded-byte cap for one deterministic model-request image. */ +/** Encoded-byte target for one deterministic model-request image; the smallest quality-ladder output is used when no quality fits. */ export const DEFAULT_REQUEST_IMAGE_MAX_BYTES = 1024 * 1024 /** Deterministic raw-byte removal step. */ export const DEFAULT_IMAGE_OFFLOAD_BYTE_QUANTUM = 64 * 1024 * 1024 @@ -173,13 +173,33 @@ const LOW_REASONING_EFFORT = ReasoningEffortId('low') const HIGH_REASONING_EFFORT = ReasoningEffortId('high') const MAX_REASONING_EFFORT = ReasoningEffortId('max') const REASONING_EFFORTS = [ - { id: OFF_REASONING_EFFORT, name: 'Off' }, - { id: LOW_REASONING_EFFORT, name: 'Low' }, - { id: HIGH_REASONING_EFFORT, name: 'High' }, - { id: MAX_REASONING_EFFORT, name: 'Max' }, + { + id: OFF_REASONING_EFFORT, + name: 'Off', + description: 'Use for simple tasks that do not need reasoning.', + }, + { + id: LOW_REASONING_EFFORT, + name: 'Low', + description: 'Prefer for routine or latency-sensitive tasks.', + }, + { + id: HIGH_REASONING_EFFORT, + name: 'High', + description: 'The default balance for most tasks.', + }, + { + id: MAX_REASONING_EFFORT, + name: 'Max', + description: 'Reserve for the hardest quality-first tasks.', + }, ] as const const OFF_ONLY_REASONING_EFFORTS = [ - { id: OFF_REASONING_EFFORT, name: 'Off' }, + { + id: OFF_REASONING_EFFORT, + name: 'Off', + description: 'Use for simple tasks that do not need reasoning.', + }, ] as const /** Marks a failed file-id resolution that may be retried as an inline request. */ diff --git a/packages/llm/llm-deepseek/src/index.ts b/packages/llm/llm-deepseek/src/index.ts index 0d895a5e3b..da0c9e24b5 100644 --- a/packages/llm/llm-deepseek/src/index.ts +++ b/packages/llm/llm-deepseek/src/index.ts @@ -82,8 +82,18 @@ const DEFAULT_API_KEY_ENV = 'DEEPSEEK_API_KEY' const PROVIDER = 'deepseek-official' const DEFAULT_MODELS: DeepSeekCatalogModel[] = [ - { id: 'deepseek-v4-flash', name: 'DeepSeek-V4-Flash', contextWindow: DEFAULT_CONTEXT_WINDOW }, - { id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro', contextWindow: DEFAULT_CONTEXT_WINDOW }, + { + id: 'deepseek-v4-flash', + name: 'DeepSeek-V4-Flash', + description: 'Fast, efficient, and economical; suited to focused, routine, or parallel tasks.', + contextWindow: DEFAULT_CONTEXT_WINDOW, + }, + { + id: 'deepseek-v4-pro', + name: 'DeepSeek-V4-Pro', + description: 'Stronger agentic coding, knowledge, and difficult reasoning; suited to complex or quality-critical tasks at higher cost.', + contextWindow: DEFAULT_CONTEXT_WINDOW, + }, { id: 'deepseek-v4-flash-vision-exp', name: 'DeepSeek-V4-Flash-Vision-Exp', diff --git a/packages/llm/llm-deepseek/tests/adapter.spec.ts b/packages/llm/llm-deepseek/tests/adapter.spec.ts index 9366c32ce8..3f87737930 100644 --- a/packages/llm/llm-deepseek/tests/adapter.spec.ts +++ b/packages/llm/llm-deepseek/tests/adapter.spec.ts @@ -1205,7 +1205,11 @@ describe('DeepSeekAdapter against a mock server', () => { await expect(ctx.llm.resolveModelInfo('deepseek-official', 'deepseek-v4-flash')) .resolves.toMatchObject({ reasoning: { - efforts: [{ id: ReasoningEffortId('off'), name: 'Off' }], + efforts: [{ + id: ReasoningEffortId('off'), + name: 'Off', + description: 'Use for simple tasks that do not need reasoning.', + }], defaultEffort: ReasoningEffortId('off'), }, }) @@ -1665,8 +1669,20 @@ describe('plugin registration and config', () => { await ctx.plugin(LlmDeepSeek, { baseURL: 'http://127.0.0.1:1' }) expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek-official', name: 'DeepSeek' }]) await expect(ctx.llm.listModels('deepseek-official')).resolves.toEqual([ - { provider: 'deepseek-official', id: 'deepseek-v4-flash', name: 'DeepSeek-V4-Flash', inputModalities: ['text'] }, - { provider: 'deepseek-official', id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro', inputModalities: ['text'] }, + { + provider: 'deepseek-official', + id: 'deepseek-v4-flash', + name: 'DeepSeek-V4-Flash', + description: 'Fast, efficient, and economical; suited to focused, routine, or parallel tasks.', + inputModalities: ['text'], + }, + { + provider: 'deepseek-official', + id: 'deepseek-v4-pro', + name: 'DeepSeek-V4-Pro', + description: 'Stronger agentic coding, knowledge, and difficult reasoning; suited to complex or quality-critical tasks at higher cost.', + inputModalities: ['text'], + }, { provider: 'deepseek-official', id: 'deepseek-v4-flash-vision-exp', name: 'DeepSeek-V4-Flash-Vision-Exp', inputModalities: ['text', 'image'] }, ]) await expect(ctx.llm.resolveModelInfo('deepseek-official', 'deepseek-v4-flash')) @@ -1678,10 +1694,10 @@ describe('plugin registration and config', () => { defaultMaxTokens: 256_000, reasoning: { efforts: [ - { id: ReasoningEffortId('off'), name: 'Off' }, - { id: ReasoningEffortId('low'), name: 'Low' }, - { id: ReasoningEffortId('high'), name: 'High' }, - { id: ReasoningEffortId('max'), name: 'Max' }, + { id: ReasoningEffortId('off'), name: 'Off', description: 'Use for simple tasks that do not need reasoning.' }, + { id: ReasoningEffortId('low'), name: 'Low', description: 'Prefer for routine or latency-sensitive tasks.' }, + { id: ReasoningEffortId('high'), name: 'High', description: 'The default balance for most tasks.' }, + { id: ReasoningEffortId('max'), name: 'Max', description: 'Reserve for the hardest quality-first tasks.' }, ], defaultEffort: ReasoningEffortId('high'), }, @@ -1708,10 +1724,10 @@ describe('plugin registration and config', () => { .resolves.toMatchObject({ reasoning: { efforts: [ - { id: ReasoningEffortId('off'), name: 'Off' }, - { id: ReasoningEffortId('low'), name: 'Low' }, - { id: ReasoningEffortId('high'), name: 'High' }, - { id: ReasoningEffortId('max'), name: 'Max' }, + { id: ReasoningEffortId('off'), name: 'Off', description: 'Use for simple tasks that do not need reasoning.' }, + { id: ReasoningEffortId('low'), name: 'Low', description: 'Prefer for routine or latency-sensitive tasks.' }, + { id: ReasoningEffortId('high'), name: 'High', description: 'The default balance for most tasks.' }, + { id: ReasoningEffortId('max'), name: 'Max', description: 'Reserve for the hardest quality-first tasks.' }, ], defaultEffort: ReasoningEffortId(effort), }, @@ -1729,7 +1745,11 @@ describe('plugin registration and config', () => { await expect(ctx.llm.resolveModelInfo('deepseek-official', 'unlisted-pass-through')) .resolves.toMatchObject({ reasoning: { - efforts: [{ id: ReasoningEffortId('off'), name: 'Off' }], + efforts: [{ + id: ReasoningEffortId('off'), + name: 'Off', + description: 'Use for simple tasks that do not need reasoning.', + }], defaultEffort: ReasoningEffortId('off'), }, }) @@ -1761,7 +1781,11 @@ describe('plugin registration and config', () => { const adapter = adapterOf({ thinking: 'disabled', reasoningEffort: 'off' }) await expect(adapter.resolveModel('deepseek-official', 'pass-through')).resolves.toMatchObject({ reasoning: { - efforts: [{ id: ReasoningEffortId('off'), name: 'Off' }], + efforts: [{ + id: ReasoningEffortId('off'), + name: 'Off', + description: 'Use for simple tasks that do not need reasoning.', + }], defaultEffort: ReasoningEffortId('off'), }, }) @@ -1772,8 +1796,20 @@ describe('plugin registration and config', () => { await ctx.plugin(LlmRuntime) LlmDeepSeek.apply(ctx, { baseURL: 'http://127.0.0.1:1' }) await expect(ctx.llm.listModels('deepseek-official')).resolves.toEqual([ - { provider: 'deepseek-official', id: 'deepseek-v4-flash', name: 'DeepSeek-V4-Flash', inputModalities: ['text'] }, - { provider: 'deepseek-official', id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro', inputModalities: ['text'] }, + { + provider: 'deepseek-official', + id: 'deepseek-v4-flash', + name: 'DeepSeek-V4-Flash', + description: 'Fast, efficient, and economical; suited to focused, routine, or parallel tasks.', + inputModalities: ['text'], + }, + { + provider: 'deepseek-official', + id: 'deepseek-v4-pro', + name: 'DeepSeek-V4-Pro', + description: 'Stronger agentic coding, knowledge, and difficult reasoning; suited to complex or quality-critical tasks at higher cost.', + inputModalities: ['text'], + }, { provider: 'deepseek-official', id: 'deepseek-v4-flash-vision-exp', name: 'DeepSeek-V4-Flash-Vision-Exp', inputModalities: ['text', 'image'] }, ]) }) diff --git a/packages/llm/llm-pi-ai/README.i18n.yaml b/packages/llm/llm-pi-ai/README.i18n.yaml index 6121212098..034878a1ce 100644 --- a/packages/llm/llm-pi-ai/README.i18n.yaml +++ b/packages/llm/llm-pi-ai/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/llm/llm-pi-ai/README.md -README.md: 1ae0e641f7ffaebc1c2c1060e143c72a40631396 -README.zh.md: 17bf059ea9bb3fbbb80cadc0c9eefed76af26abd +README.md: 31e40e5f0fa3c1e7e0ae0df05aa0a76d54d120b0 +README.zh.md: cd40804ce5908aebd0c35011ad1d56879834164d diff --git a/packages/llm/llm-pi-ai/README.md b/packages/llm/llm-pi-ai/README.md index 1ae0e641f7..31e40e5f0f 100644 --- a/packages/llm/llm-pi-ai/README.md +++ b/packages/llm/llm-pi-ai/README.md @@ -123,7 +123,7 @@ A model that carries reasoning metadata — from the installed catalog or from i A model **without** that metadata — a hand-declared one whose entry declares no `reasoningEfforts`, and a catalog model pi-ai marks as non-reasoning — exposes no `reasoning` at all. pi-ai reports such a model as supporting the single level `off`, but `off` is translated to *omitting* the reasoning option, which is byte-for-byte the request that naming no effort already produces: selecting it could not disable anything, so a provider whose own default is to think would keep thinking with `off` shown as selected. Reporting the capability as unavailable leaves a surface offering the provider's default and nothing that misrepresents it. The profile `reasoning` value, including `off`, is the deployment default when configured; omitting it preserves the provider default. Per-request `GenerateOptions.reasoningEffort` takes precedence, and a level absent from the exact model capability fails the REQUEST with `UNSUPPORTED_REASONING_EFFORT` before network I/O instead of being clamped. Describing a model never fails that way: the models under one provider disagree about which levels they accept, so `resolveModel` reports a profile level the exact model cannot take as no default at all rather than throwing. A throw there would take the whole provider out of every model catalog built over it — one mis-set profile field hiding even the models that do support the level — so a bad configuration surfaces where it is acted on, not where it is described. pi-ai's common stream options represent `off` by omitting `reasoning`. -Supported profile fields are `apiKeyEnv`, `displayName`, `api`, `baseURL`, `models`, `modelOverrides`, `compat`, `defaultContextWindow`, `defaultMaxTokens`, `defaultInput`, `headers`, `reasoning`, `thinkingBudgets`, `cacheRetention`, `transport`, `timeoutMs`, `websocketConnectTimeoutMs`, `streamIdleTimeoutMs`, `maxRequestImageBytes`, `requestImagePixelBudget`, `requestImageMaxBytes`, and `retryPolicy`. Each resolved profile retry policy is captured with that provider route; omission uses the shared bounded normal default of five retries. The stream-idle interval is a positive finite Node timer delay, defaults to five minutes, and covers only an outstanding provider read, not consumer think time. Every image route derives a deterministic request version from the provider-independent normalized attachment under `requestImagePixelBudget` (default 2048 by 2048 total pixels) and `requestImageMaxBytes` (default 1MiB raw bytes). Before reading attachments, `maxRequestImageBytes` applies to conservative request-version upper bounds and replaces the oldest over-budget images with per-image text; exact base64 lengths are checked again after retained versions are generated. The 20MiB default can retain fifteen maximum-size 1MiB versions after base64 expansion while leaving request-body headroom. The same version feeds inline base64. Its descriptor exposes the attachment id and actual request-image dimensions, plus a normalized-object path only when the attachment provider exposes a host object and the current filesystem maps it into the tool execution world. The path is resolved separately from the request version and its `variantId`. Harness app attribution wins a conflicting configured header name. +Supported profile fields are `apiKeyEnv`, `displayName`, `api`, `baseURL`, `models`, `modelOverrides`, `compat`, `defaultContextWindow`, `defaultMaxTokens`, `defaultInput`, `headers`, `reasoning`, `thinkingBudgets`, `cacheRetention`, `transport`, `timeoutMs`, `websocketConnectTimeoutMs`, `streamIdleTimeoutMs`, `maxRequestImageBytes`, `requestImagePixelBudget`, `requestImageMaxBytes`, and `retryPolicy`. Each resolved profile retry policy is captured with that provider route; omission uses the shared bounded normal default of five retries. The stream-idle interval is a positive finite Node timer delay, defaults to five minutes, and covers only an outstanding provider read, not consumer think time. Every image route derives a deterministic request version from the provider-independent normalized attachment under `requestImagePixelBudget` (default 2048 by 2048 total pixels) and `requestImageMaxBytes` (default 1MiB raw bytes). Before reading attachments, `maxRequestImageBytes` applies to conservative request-version upper bounds and replaces the oldest over-budget images with per-image text; exact base64 lengths are checked again after retained versions are generated. The 20MiB default can retain fifteen 1MiB-target versions after base64 expansion while leaving request-body headroom. The same version feeds inline base64. Its descriptor exposes the attachment id and actual request-image dimensions, plus a normalized-object path only when the attachment provider exposes a host object and the current filesystem maps it into the tool execution world. The path is resolved separately from the request version and its `variantId`. Harness app attribution wins a conflicting configured header name. The adapter forces pi-ai's SDK `maxRetries` to zero so one `stream()` call makes one provider request. The removed profile fields `maxRetries` and `maxRetryDelayMs` fail load instead of silently multiplying or hiding the separately composed agent-level retry budget. Idle expiry aborts the SDK's stable request signal and surfaces `TIMEOUT`; an earlier caller abort remains `ABORTED`. diff --git a/packages/llm/llm-pi-ai/README.zh.md b/packages/llm/llm-pi-ai/README.zh.md index 17bf059ea9..cd40804ce5 100644 --- a/packages/llm/llm-pi-ai/README.zh.md +++ b/packages/llm/llm-pi-ai/README.zh.md @@ -124,7 +124,7 @@ pi-ai 依据提供方 id 与 baseURL 决定每个请求的形状:系统提示 **没有**这份元数据的模型——条目未声明 `reasoningEfforts` 的手工声明模型,以及 pi-ai 标记为不具备推理能力的 catalog 模型——完全不公开 `reasoning`。pi-ai 会把这类模型报告为只支持 `off` 一档,但 `off` 会被翻译成*省略* reasoning 选项,而那与「不点名任何档位」产出的请求逐字节相同:选它关不掉任何东西,于是自身默认就在思考的提供方,会在界面显示 `off` 被选中的同时继续思考。把该能力报告为不可用,界面就只剩提供方默认这一项,不会再出现自相矛盾的控件。配置 profile 的 `reasoning` 值(包括 `off`)在存在时是部署默认值;省略它会保留提供方默认值。每次请求的 `GenerateOptions.reasoningEffort` 优先;未出现在确切模型能力中的档位会让**请求**在网络 I/O 前以 `UNSUPPORTED_REASONING_EFFORT` 失败,而不会被自动调整。**描述**一个模型则从不这样失败:同一提供方下各模型接受的档位并不一致,因此 `resolveModel` 对该模型拿不下的 profile 档位报告为「没有默认值」,而不是抛错。在那里抛错会让整个提供方从任何基于它构建的模型目录中消失——一个配错的 profile 字段连支持该档位的模型也一并藏起来——所以坏配置暴露在被执行处,而不是被描述处。pi-ai 的通用流选项通过省略 `reasoning` 表示 `off`。 -受支持的 profile 字段是 `apiKeyEnv`、`displayName`、`api`、`baseURL`、`models`、`modelOverrides`、`compat`、`defaultContextWindow`、`defaultMaxTokens`、`defaultInput`、`headers`、`reasoning`、`thinkingBudgets`、`cacheRetention`、`transport`、`timeoutMs`、`websocketConnectTimeoutMs`、`streamIdleTimeoutMs`、`maxRequestImageBytes`、`requestImagePixelBudget`、`requestImageMaxBytes` 和 `retryPolicy`。每条 profile 解析后的重试策略会随该提供方路由一同捕获;省略时使用共享的有界 normal 默认值并重试五次。流空闲间隔必须是正的有限 Node 定时器延迟,默认为五分钟,且只覆盖未完成提供方读取,不包括消费方思考时间。每条图片路由从提供方无关的规范化附件派生确定性请求版本,受 `requestImagePixelBudget`(默认总像素 2048×2048)和 `requestImageMaxBytes`(默认原始字节 1MiB)约束。读取附件前,`maxRequestImageBytes` 先按请求版本的保守上界把超预算的最旧图片替换为逐图文本;保留版本生成后再用确切 base64 长度检查。20MiB 默认值可保留十五个按 1MiB 上限生成的请求版本,并为请求正文留下余量。同一版本用于内联 base64。对应描述会公开附件 ID 和实际请求图片尺寸;只有附件提供方给出宿主对象且当前文件系统能够将其映射到工具执行环境时,描述才会加入规范化对象路径。该路径独立于请求版本及其 `variantId`。若已配置标头中有同名项,则以 Harness 应用归因为准。 +受支持的 profile 字段是 `apiKeyEnv`、`displayName`、`api`、`baseURL`、`models`、`modelOverrides`、`compat`、`defaultContextWindow`、`defaultMaxTokens`、`defaultInput`、`headers`、`reasoning`、`thinkingBudgets`、`cacheRetention`、`transport`、`timeoutMs`、`websocketConnectTimeoutMs`、`streamIdleTimeoutMs`、`maxRequestImageBytes`、`requestImagePixelBudget`、`requestImageMaxBytes` 和 `retryPolicy`。每条 profile 解析后的重试策略会随该提供方路由一同捕获;省略时使用共享的有界 normal 默认值并重试五次。流空闲间隔必须是正的有限 Node 定时器延迟,默认为五分钟,且只覆盖未完成提供方读取,不包括消费方思考时间。每条图片路由从提供方无关的规范化附件派生确定性请求版本,受 `requestImagePixelBudget`(默认总像素 2048×2048)和 `requestImageMaxBytes`(默认原始字节 1MiB)约束。读取附件前,`maxRequestImageBytes` 先按请求版本的保守上界把超预算的最旧图片替换为逐图文本;保留版本生成后再用确切 base64 长度检查。20MiB 默认值可保留十五个按 1MiB 目标生成的请求版本,并为请求正文留下余量。同一版本用于内联 base64。对应描述会公开附件 ID 和实际请求图片尺寸;只有附件提供方给出宿主对象且当前文件系统能够将其映射到工具执行环境时,描述才会加入规范化对象路径。该路径独立于请求版本及其 `variantId`。若已配置标头中有同名项,则以 Harness 应用归因为准。 适配器强制 pi-ai SDK `maxRetries` 为零,因此一次 `stream()` 调用只会发起一次提供方请求。已移除 profile 字段 `maxRetries` 和 `maxRetryDelayMs` 会使加载失败,而不是静默倍增或隐藏单独组合的 agent(智能体)级重试预算。空闲超时会 abort SDK 的稳定请求信号,并以 `TIMEOUT` 呈现;较早的调用方 abort 仍为 `ABORTED`。 diff --git a/packages/llm/llm-pi-ai/src/config.ts b/packages/llm/llm-pi-ai/src/config.ts index 28e409e734..e5a7e608b9 100644 --- a/packages/llm/llm-pi-ai/src/config.ts +++ b/packages/llm/llm-pi-ai/src/config.ts @@ -54,7 +54,7 @@ export const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300_000 export const DEFAULT_MAX_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024 /** Default total-pixel budget preserves the complete 2048px normalized attachment. */ export const DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET = 2048 * 2048 -/** Default raw encoded-byte cap before inline base64 expansion. */ +/** Default raw encoded-byte target before inline base64 expansion; the smallest quality-ladder output is used when no quality fits. */ export const DEFAULT_REQUEST_IMAGE_MAX_BYTES = 1024 * 1024 /** Context capacity assumed for a model neither configuration nor the catalog sizes. */ @@ -169,7 +169,10 @@ export interface PiAiProviderProfile { maxRequestImageBytes?: number /** Total-pixel budget for each deterministic inline request version. */ requestImagePixelBudget?: number - /** Raw encoded-byte cap for each deterministic inline request version. */ + /** + * Raw encoded-byte target for each deterministic inline request version; + * the smallest quality-ladder output is used when no quality fits. + */ requestImageMaxBytes?: number /** Provider-owned model-request retry policy; omission uses normal mode with five retries. */ retryPolicy?: RetryPolicyConfig @@ -190,7 +193,7 @@ export interface ResolvedPiAiProviderProfile maxRequestImageBytes: number /** Positive total-pixel request-version budget after defaulting. */ requestImagePixelBudget: number - /** Positive raw request-version byte cap after defaulting. */ + /** Positive raw request-version byte target after defaulting; the smallest quality-ladder output is used when no quality fits. */ requestImageMaxBytes: number /** Immutable retry policy captured with this provider route. */ retryPolicy: ResolvedRetryPolicy diff --git a/packages/preset/agent-presets/README.i18n.yaml b/packages/preset/agent-presets/README.i18n.yaml index 42601edbd4..a4f3fed8d6 100644 --- a/packages/preset/agent-presets/README.i18n.yaml +++ b/packages/preset/agent-presets/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/preset/agent-presets/README.md -README.md: e204868e43ba07b78eff31293cc33cf790ff0085 -README.zh.md: f11fa5f092b9d4de1abb5c1464b6931d749c2970 +README.md: 3e92f02518c5a36412c9448a26d32958c217f79c +README.zh.md: 4600589749a063df924f9c961cc449506ba2af9f diff --git a/packages/preset/agent-presets/README.md b/packages/preset/agent-presets/README.md index e204868e43..3e92f02518 100644 --- a/packages/preset/agent-presets/README.md +++ b/packages/preset/agent-presets/README.md @@ -16,7 +16,7 @@ Discovery is unmemoized: `list()` and `resolve()` re-read the roots on every cal - `ctx.agentPresets.mount(agentCtx, id?): Promise` Compose one agent from a preset — ensure its standing mount (single-flight) and parent the agent's scope key to it — returning the preset for the caller to record. Refuses a broken preset up front with its discovery-reported reason, so every unloadable shape fails the same way before the loader is involved. - `ctx.agentPresets.composeFrom(agentCtx, parentCtx): string | undefined` Join one agent to the standing composition another already runs on, returning the preset id joined — `undefined` when the parent joined none, which is the rosterless deployment and not an error. A bind rather than a mount, so it is synchronous and has no composition failure mode; it still rejects a caller error (an unscoped context, or an agent that already joined). - `ctx.agentPresets.composedPreset(agentCtx): string | undefined` The preset one LIVE agent runs on, read from its scope chain rather than from its session — the only answer available for an agent whose durable header is still being built. -- `ctx.agentPresets.recompose(agentCtx, id): Promise` Re-link one agent to a different preset's standing composition. Valid only while the agent has produced nothing — **the caller owns that check**; the new mount is ensured before the link moves, so a failure leaves the agent as it was. Refuses a broken preset like `mount()`. +- `ctx.agentPresets.recompose(agentCtx, id): Promise` Re-link one agent to a different preset's standing composition. Valid only while the agent has produced nothing — **the caller owns that check**; the new mount is ensured before the link moves, so a failure leaves the agent as it was. A committed re-link emits `tools/change`, because the Agent's resolved tool set changed without a registry entry changing. Refuses a broken preset like `mount()`. - `ctx.agentPresets.standingKeyFor(id?): Promise` The standing scope key a host reader with no agent (a cold transcript read) resolves preset registrations in; ensures the mount without starting an agent, session, or turn. Refuses a broken preset like `mount()`. - `ctx.agentPresets.roots: readonly PresetRoot[]` The roots this roster scans — every configured root in order, then the derived harness-home root. Not `config.roots`: read this to answer whether a roster is composed at all, so one derivation decides it. - `ctx.agentPresets.authorable: boolean` Whether any of those roots has `user` trust, and therefore whether a preset can be created at all. diff --git a/packages/preset/agent-presets/README.zh.md b/packages/preset/agent-presets/README.zh.md index f11fa5f092..4600589749 100644 --- a/packages/preset/agent-presets/README.zh.md +++ b/packages/preset/agent-presets/README.zh.md @@ -16,7 +16,7 @@ - `ctx.agentPresets.mount(agentCtx, id?): Promise` 用一个 preset 组装一个 agent——确保其常驻挂载(并发去重)并把 agent 的 scope key 认父到它——返回该 preset 供调用方记录。对损坏的 preset 直接以发现时记下的原因拒绝,所以每种不可加载的形态都在加载器介入之前以同一方式失败。 - `ctx.agentPresets.composeFrom(agentCtx, parentCtx): string | undefined` 让一个 agent 加入另一个 agent 已在运行的常驻组装,返回所加入的 preset id——父方未加入任何 preset 时返回 `undefined`,那是无 roster 的部署,不是错误。这是认父而非挂载,因此同步、且自身没有组装失败模式;调用方用错(上下文无 scope、agent 已加入过)仍会拒绝。 - `ctx.agentPresets.composedPreset(agentCtx): string | undefined` 某个**活着的** agent 正在运行的 preset,从其 scope 链读取而不是从其会话读取——对于持久化 header 尚在构建中的 agent,这是唯一能拿到的答案。 -- `ctx.agentPresets.recompose(agentCtx, id): Promise` 把一个 agent 重链到另一个 preset 的常驻组装。仅在该 agent 尚无任何产出时合法——**由调用方负责该检查**;新挂载在链移动之前确保完成,失败时 agent 原封不动。与 `mount()` 一样拒绝损坏的 preset。 +- `ctx.agentPresets.recompose(agentCtx, id): Promise` 把一个 agent 重链到另一个 preset 的常驻组装。仅在该 agent 尚无任何产出时合法——**由调用方负责该检查**;新挂载在链移动之前确保完成,失败时 agent 原封不动。重链提交后会发出 `tools/change`,因为 Agent 解析到的工具集已经变化、但注册表条目本身没有增删。与 `mount()` 一样拒绝损坏的 preset。 - `ctx.agentPresets.standingKeyFor(id?): Promise` 没有 agent 的宿主读取方(冷读记录)解析 preset 注册所用的常驻 scope key;确保挂载而不启动任何 agent、会话或轮次。与 `mount()` 一样拒绝损坏的 preset。 - `ctx.agentPresets.roots: readonly PresetRoot[]` 本 roster 实际扫描的根目录——全部已配置根目录按序在前,随后是推导出的 harness home 根目录。它不是 `config.roots`:判断「是否已组装 roster」应读它,从而由同一处推导决定。 - `ctx.agentPresets.authorable: boolean` 上述根目录中是否有任一具备 `user` 信任级别,因而 preset 是否可创建。 diff --git a/packages/preset/agent-presets/package.json b/packages/preset/agent-presets/package.json index dfdf08cbbe..a20ab0a87c 100644 --- a/packages/preset/agent-presets/package.json +++ b/packages/preset/agent-presets/package.json @@ -48,6 +48,7 @@ "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-settings": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", + "@deepseek-ai/dsh-tools": "workspace:^", "@deepseek-ai/cordis": "workspace:^" }, "dependencies": { diff --git a/packages/preset/agent-presets/presets/code/agent.cordis.yml b/packages/preset/agent-presets/presets/code/agent.cordis.yml index 3333a980c0..1302329c25 100644 --- a/packages/preset/agent-presets/presets/code/agent.cordis.yml +++ b/packages/preset/agent-presets/presets/code/agent.cordis.yml @@ -189,8 +189,13 @@ config: provider: spawn toolName: subagent + modelSelectionSettings: true backgroundMode: continuable + # Fork omits model selection so provider/model stay equal to the parent and + # the inherited history remains eligible for KV Cache reuse. This preset + # keeps fork continuable and accepts its child-scoped `report` additions invalidating + # that prefix; issue #2124 tracks cache-preserving continuable fork. - id: tool-subagent-fork name: '@deepseek-ai/dsh-tool-subagent' config: diff --git a/packages/preset/agent-presets/presets/cordis/agent.cordis.yml b/packages/preset/agent-presets/presets/cordis/agent.cordis.yml index f23907c655..b016eae3a1 100644 --- a/packages/preset/agent-presets/presets/cordis/agent.cordis.yml +++ b/packages/preset/agent-presets/presets/cordis/agent.cordis.yml @@ -176,8 +176,13 @@ config: provider: spawn toolName: subagent + modelSelectionSettings: true backgroundMode: continuable + # Fork omits model selection so provider/model stay equal to the parent and + # the inherited history remains eligible for KV Cache reuse. This preset + # keeps fork continuable and accepts its child-scoped `report` additions invalidating + # that prefix; issue #2124 tracks cache-preserving continuable fork. - id: tool-subagent-fork name: '@deepseek-ai/dsh-tool-subagent' config: diff --git a/packages/preset/agent-presets/presets/standard/agent.cordis.yml b/packages/preset/agent-presets/presets/standard/agent.cordis.yml index 5cb19e1e24..c21c5e4d79 100644 --- a/packages/preset/agent-presets/presets/standard/agent.cordis.yml +++ b/packages/preset/agent-presets/presets/standard/agent.cordis.yml @@ -188,8 +188,13 @@ config: provider: spawn toolName: subagent + modelSelectionSettings: true backgroundMode: continuable + # Fork omits model selection so provider/model stay equal to the parent and + # the inherited history remains eligible for KV Cache reuse. This preset + # keeps fork continuable and accepts its child-scoped `report` additions invalidating + # that prefix; issue #2124 tracks cache-preserving continuable fork. - id: tool-subagent-fork name: '@deepseek-ai/dsh-tool-subagent' config: diff --git a/packages/preset/agent-presets/src/index.ts b/packages/preset/agent-presets/src/index.ts index e22dee7275..8f44e2003c 100644 --- a/packages/preset/agent-presets/src/index.ts +++ b/packages/preset/agent-presets/src/index.ts @@ -27,6 +27,8 @@ import z from '@deepseek-ai/schemastery' import { bindScopeParent, createScope, scopeOf, type Scope, type ScopeKey, type ScopeParentBinding } from '@deepseek-ai/dsh-scope' // Type-only: resolves the `agent/created` lifecycle event this service watches. import type {} from '@deepseek-ai/dsh-agent' +// Type-only: resolves the registry notification emitted after scope reparenting. +import type {} from '@deepseek-ai/dsh-tools' import { settingsNamespace, type SettingsScope, type default as SettingsService } from '@deepseek-ai/dsh-settings' import { dshHomePath } from '@deepseek-ai/dsh-home-paths' import { discoverPresets, SHIPPED_PRESET_ROOT, USER_PRESET_DIR } from './discovery.ts' @@ -455,7 +457,9 @@ export class AgentPresets extends Service { * state to restore. The re-link runs through the binding this roster kept * from the agent's mount — dsh-scope's only re-link authority. An agent * that never composed one has nothing to re-link: the switch is then the - * agent's first bind, exactly a mount. + * agent's first bind, exactly a mount. A committed re-link emits + * `tools/change` because changing the parent scope changes the Agent's + * resolved tool set without adding or removing registry entries. * @param agentCtx - the agent's scope context. * @param id - the preset to compose the agent from instead. * @returns the preset now installed. @@ -474,6 +478,14 @@ export class AgentPresets extends Service { } else { binding.rebind(standing.key) } + // Reparenting changes every scope-layered tool view without adding or + // removing a registration. Publish the registry's normal invalidation so + // Agent-owned overlays can reconcile with the new ancestry. + try { + this.ctx.emit('tools/change') + } catch (error: unknown) { + this.ctx.logger.warn(`agent-presets: tools/change listener failed after recomposing an Agent: ${String(error)}`) + } return preset } diff --git a/packages/preset/agent-presets/tests/mount.spec.ts b/packages/preset/agent-presets/tests/mount.spec.ts index 5b22e26ec6..83f6da3102 100644 --- a/packages/preset/agent-presets/tests/mount.spec.ts +++ b/packages/preset/agent-presets/tests/mount.spec.ts @@ -484,6 +484,27 @@ describe('replacing a composition', () => { expect(toolNames(ctx)).toEqual([]) }) + it('notifies tool views after reparenting and contains notification failures', async () => { + const handle = await ctx.agents.create({ + sessionId: SessionId('sess-tool-change'), + setup: async (agentCtx: Context) => void await ctx.agentPresets.mount(agentCtx, 'standard'), + }) + await ctx.agentPresets.standingKeyFor('minimal') + let changes = 0 + const stopCounting = ctx.on('tools/change', () => { changes += 1 }) + await ctx.agentPresets.recompose(handle.agent.ctx, 'minimal') + expect(changes).toBe(1) + stopCounting() + + const warnings: string[] = [] + ctx.logger.warn = ((message: unknown) => { warnings.push(String(message)) }) as typeof ctx.logger.warn + const stopThrowing = ctx.on('tools/change', () => { throw new Error('listener failed') }) + await expect(ctx.agentPresets.recompose(handle.agent.ctx, 'standard')).resolves.toMatchObject({ id: 'standard' }) + expect(ctx.agentPresets.composedPreset(handle.agent.ctx)).toBe('standard') + expect(warnings).toEqual([expect.stringContaining('tools/change listener failed')]) + stopThrowing() + }) + it('leaves the agent on its previous composition when the new one is unknown', async () => { const handle = await ctx.agents.create({ sessionId: SessionId('sess-unknown'), diff --git a/packages/preset/agent-presets/tsconfig.json b/packages/preset/agent-presets/tsconfig.json index 405ca21e2b..b40cf776ad 100644 --- a/packages/preset/agent-presets/tsconfig.json +++ b/packages/preset/agent-presets/tsconfig.json @@ -30,6 +30,9 @@ { "path": "../../core/system-prompt" }, + { + "path": "../../core/tools" + }, { "path": "../../settings/settings" }, diff --git a/packages/sdk/README.i18n.yaml b/packages/sdk/README.i18n.yaml index 05788b05a5..e46b9b6698 100644 --- a/packages/sdk/README.i18n.yaml +++ b/packages/sdk/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/sdk/README.md -README.md: 663fa95dc56ecd71c7a2639b6edb45caacbab860 -README.zh.md: 0343ad477f9fa522666cd8d4440bfe645f309c2e +README.md: 7227e9c2c93b3f870d0affc74471d7801f37d4ef +README.zh.md: 3b6f5556cb539abafd1a1e8c866f09a44a2d2f1a diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 663fa95dc5..7227e9c2c9 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -2,11 +2,10 @@ English | [中文](README.zh.md) -This group contains the protocol stack for driving a Harness runtime from another process. The TypeScript client launches the matching `dsh` CLI with a named profile and ordered patches; the private Python carrier preserves the current packaged direct-config runtime until Python moves through the same profile path. The [TypeScript SDK decision](../../.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md) owns the client contract, and the [toolchain removal](../../.agents/notes/implemented/simplification/2026-08-11-remove-sdk-project-toolchain.md) owns the product boundary. +This group contains the protocol stack for driving a Harness runtime from another process. The TypeScript and Python clients both launch `dsh` with a named profile and ordered patches; no package in this group defines a separate application. The [TypeScript SDK decision](../../.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md) owns the client contract, and the [Python profile-runtime decision](../../.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md) owns the packaged Python launch. | Package | Role | |---|---| | [`protocol/`](protocol/README.md) | Defines the SDK runtime wire protocol | | [`client/`](client/README.md) | Drives a Harness runtime through the TypeScript client API | | [`server/`](server/README.md) | Serves out-of-process SDK clients over stdio JSON-RPC | -| [`python-runtime/`](python-runtime/README.md) | Private direct-config carrier for the temporarily unchanged Python SDK runtime | diff --git a/packages/sdk/README.zh.md b/packages/sdk/README.zh.md index 0343ad477f..3b6f5556cb 100644 --- a/packages/sdk/README.zh.md +++ b/packages/sdk/README.zh.md @@ -2,11 +2,10 @@ [English](README.md) | 中文 -本组包含用于从另一进程驱动 Harness 运行时的协议栈。TypeScript 客户端通过具名 profile 与有序 patch 启动匹配版本的 `dsh` CLI;私有 Python 载体在 Python 迁移到同一 profile 路径之前,保留当前打包后的直读配置运行时。[TypeScript SDK 决策](../../.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md)负责客户端约定,[工具链移除](../../.agents/notes/implemented/simplification/2026-08-11-remove-sdk-project-toolchain.zh.md)负责产品边界。 +本组包含用于从另一进程驱动 Harness 运行时的协议栈。TypeScript 与 Python 客户端都通过具名 profile 与有序 patch 启动 `dsh`;本组没有任何包定义独立应用。[TypeScript SDK 决策](../../.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md)负责客户端约定,[Python profile 运行时决策](../../.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md)负责打包后的 Python 启动。 | 包 | 职责 | |---|---| | [`protocol/`](protocol/README.zh.md) | 定义 SDK 运行时通信协议 | | [`client/`](client/README.zh.md) | 通过 TypeScript 客户端 API 驱动 Harness 运行时 | | [`server/`](server/README.zh.md) | 通过 stdio JSON-RPC 为进程外 SDK 客户端提供服务 | -| [`python-runtime/`](python-runtime/README.zh.md) | 为暂时保持不变的 Python SDK 运行时提供私有直读配置载体 | diff --git a/packages/sdk/python-runtime/README.md b/packages/sdk/python-runtime/README.md deleted file mode 100644 index 291ad3edaf..0000000000 --- a/packages/sdk/python-runtime/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# @deepseek-ai/dsh-sdk-python-runtime - -English | [中文](README.zh.md) - -Private direct-config carrier for the temporarily unchanged Python SDK runtime. Its [`jsonrpc`](../server/README.md) entry serves SDK clients over newline-delimited stdio, while an external `cordis.yml` composes the spine, backends, and serving plugin. This npm package exposes no public bin and is not published; the Python SDK's existing `dsh-jsonrpc-agent-pkg--` [single-executable runtime](../../../.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md) packages `lib/packaged-bin.js` from the closed deploy tree. Bare plugins resolve from that tree, while relative plugins remain configuration-relative. - -## Config discovery - -The first non-empty channel wins: `$DSH_CORDIS_CONFIG`, then positional `argv[2]`. If neither names an existing file, the packaged entry prints one-line usage to stderr and exits 1; there is no working-directory or built-in fallback. [`dsh-app-boot`](../../boot/app-boot/README.md) makes plugin load failures fatal. This protocol does not use `DSH_SNAPSHOT`. - -A config without `dsh-sdk-jsonrpc-server` is valid and serves nothing; the carrier does not designate a server plugin. - -## Exit lifecycle - -stdin EOF and `SIGTERM` dispose the root to quiescence and exit 0; `SIGINT` exits 130 after the same disposal. EOF may cut off an in-flight turn as documented in the [distribution Agent Note](../../../.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md). The `jsonrpc` plugin owns response-before-exit protocol shutdown; both paths are idempotent and safe to race. - -## stdout is the protocol - -stdout carries only JSON-RPC frames. The carrier and boot guards diagnose on stderr, and the config must omit stdout loggers. - -## Model Experience - -Indirectly, through the plugins loaded from the external `cordis.yml`, which own every model-bound prompt, schema, message, and result; this carrier adds none of its own. - -#### KV Cache effect - -No direct invalidation; the named consumer owns any request-prefix changes. - -## Known Limitations and Deferred Work - -- **Temporary direct-config exception** — this private carrier remains outside `dsh --profile sdk` only to preserve the current Python executable and wheel behavior; the later Python runtime migration deletes it and then renames the executable family. -- **The carrier cannot prove that the config serves JSON-RPC** — a valid config with no `dsh-sdk-jsonrpc-server` entry boots successfully and serves nothing. -- **No built-in or default config exists** — every launch must provide `DSH_CORDIS_CONFIG` or a positional path, and deployment owns the complete plugin tree and stdout discipline. -- **stdin EOF cuts off in-flight work** — client disappearance disposes the root immediately; callers that need orderly completion use the protocol-level `shutdown` request. diff --git a/packages/sdk/python-runtime/README.zh.md b/packages/sdk/python-runtime/README.zh.md deleted file mode 100644 index 54001ccfff..0000000000 --- a/packages/sdk/python-runtime/README.zh.md +++ /dev/null @@ -1,34 +0,0 @@ -# @deepseek-ai/dsh-sdk-python-runtime - -[English](README.md) | 中文 - -这是为暂时保持不变的 Python SDK 运行时提供的私有直读配置载体。其 [`jsonrpc`](../server/README.zh.md) 入口通过按换行分隔的 stdio 为 SDK 客户端提供服务,外部 `cordis.yml` 则负责组合主干、后端和服务插件。该 npm 包不公开 bin,也不会发布;Python SDK 既有的 `dsh-jsonrpc-agent-pkg--` [单文件可执行运行时](../../../.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md)从封闭部署树打包 `lib/packaged-bin.js`。裸插件从该树解析,相对插件仍以配置目录为基准。 - -## 配置发现 - -第一个非空通道生效:先 `$DSH_CORDIS_CONFIG`,再位置参数 `argv[2]`。如果二者都没有指向现有文件,打包入口会向 stderr 打印单行用法并以 1 退出;没有工作目录回退或内置回退。[`dsh-app-boot`](../../boot/app-boot/README.zh.md) 会使插件加载失败成为致命错误。此协议不使用 `DSH_SNAPSHOT`。 - -不含 `dsh-sdk-jsonrpc-server` 的配置仍然有效,只是不提供任何服务;该载体不会指定服务器插件。 - -## 退出生命周期 - -stdin EOF 和 `SIGTERM` 会 dispose(释放资源)根上下文,等待完全停稳后以 0 退出;`SIGINT` 完成同样的 dispose 后以 130 退出。EOF 可能按[分发 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md) 所述截断正在处理的轮次。`jsonrpc` 插件拥有先响应再退出的协议关闭流程;两条路径均幂等,即使发生竞态也安全。 - -## stdout 是协议 - -stdout 只承载 JSON-RPC 帧。该载体和启动守卫在 stderr 上输出诊断,配置必须省略 stdout logger。 - -## 模型体验 - -模型体验由外部 `cordis.yml` 加载的插件间接提供;这些插件负责所有面向模型的提示词、schema、消息和结果,该载体不添加任何内容。 - -#### KV Cache 影响 - -不会直接失效;由上述消费方负责请求前缀的任何变更。 - -## 已知限制与暂缓事项 - -- **临时直读配置例外**:为了保持当前 Python 可执行文件与 wheel 包行为不变,该私有载体暂时不经过 `dsh --profile sdk`;后续 Python 运行时迁移会删除它,之后再重命名可执行文件族。 -- **载体无法证明配置提供 JSON-RPC 服务**:不含 `dsh-sdk-jsonrpc-server` 条目的有效配置也能成功启动,但不会提供任何服务。 -- **不存在内置或默认配置**:每次启动都必须提供 `DSH_CORDIS_CONFIG` 或位置路径;部署方负责完整的插件树和 stdout 纪律。 -- **stdin EOF 会截断正在处理的工作**:客户端消失时立即释放根上下文;需要有序完成的调用方应使用协议级 `shutdown` 请求。 diff --git a/packages/sdk/python-runtime/package.json b/packages/sdk/python-runtime/package.json deleted file mode 100644 index cdae65348f..0000000000 --- a/packages/sdk/python-runtime/package.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "@deepseek-ai/dsh-sdk-python-runtime", - "description": "Private direct-config runtime carrier for the temporarily unchanged Python SDK", - "version": "0.1.1-rc.2", - "private": true, - "repository": { - "type": "git", - "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", - "directory": "packages/sdk/python-runtime" - }, - "type": "module", - "main": "lib/index.js", - "types": "lib/types/index.d.ts", - "exports": { - ".": { - "types": "./lib/types/index.d.ts", - "default": "./lib/index.js" - }, - "./invariant": { - "types": "./lib/types/invariant.d.ts", - "default": "./lib/invariant.js" - }, - "./packaged-bin": { - "types": "./lib/types/packaged-bin.d.ts", - "default": "./lib/packaged-bin.js" - }, - "./src/*": "./src/*", - "./package.json": "./package.json" - }, - "files": [ - "lib/index.js", - "lib/invariant.js", - "lib/packaged-bin.js", - "lib/types/**/*.d.ts" - ], - "license": "MIT", - "dependencies": { - "@deepseek-ai/dsh-app-boot": "workspace:^" - }, - "peerDependencies": { - "@deepseek-ai/dsh-invariants": "workspace:^", - "@deepseek-ai/cordis": "workspace:^" - }, - "devDependencies": { - "@deepseek-ai/dsh-invariants": "workspace:^", - "@deepseek-ai/cordis": "workspace:^" - } -} diff --git a/packages/sdk/python-runtime/src/index.ts b/packages/sdk/python-runtime/src/index.ts deleted file mode 100644 index 74dabd87bf..0000000000 --- a/packages/sdk/python-runtime/src/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Private Python SDK runtime carrier: its packaged entry discovers an external - * `cordis.yml` and owns process exit. This module exports no composition plugin; - * the config chooses whether to load the - * {@link @deepseek-ai/dsh-sdk-jsonrpc-server} serving plugin. - * - * @module @deepseek-ai/dsh-sdk-python-runtime - */ - -export {} diff --git a/packages/sdk/python-runtime/src/packaged-bin.ts b/packages/sdk/python-runtime/src/packaged-bin.ts deleted file mode 100644 index 1c5e5c3e92..0000000000 --- a/packages/sdk/python-runtime/src/packaged-bin.ts +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env node -/** - * Closed-runtime JSON-RPC agent bin. Bare plugins resolve from the installed - * runtime closure while relative plugins remain configuration-relative. - * - * @module @deepseek-ai/dsh-sdk-python-runtime/packaged-bin - */ - -import { runPythonSdkRuntime } from './runner.ts' - -/* v8 ignore next -- exercised through the built Python runtime carriers */ -await runPythonSdkRuntime(import.meta.url) diff --git a/packages/sdk/python-runtime/src/runner.ts b/packages/sdk/python-runtime/src/runner.ts deleted file mode 100644 index 9850c64a0b..0000000000 --- a/packages/sdk/python-runtime/src/runner.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Process lifecycle for the Python SDK's closed direct-config runtime. - * - * @module @deepseek-ai/dsh-sdk-python-runtime/runner - */ - -import { existsSync } from 'node:fs' -import { boot, installFailLoud, loadEnv, resolveConfigPath } from '@deepseek-ai/dsh-app-boot' - -/* v8 ignore start -- composition over tested app-boot/jsonrpc and executable acceptance paths */ -const NAME = 'dsh-jsonrpc-agent' - -/** - * Boot the explicitly selected external configuration and own process exit. - * @param bareModuleBaseUrl - installed-runtime base for bare plugins. - * @returns after process handlers are installed; process lifetime then belongs - * to stdin and signal events. - */ -export async function runPythonSdkRuntime(bareModuleBaseUrl: string): Promise { - installFailLoud(NAME) - loadEnv(NAME) - - // Env wins over argv; empty values are absent. External config defines the deployment. - const fromEnv = process.env['DSH_CORDIS_CONFIG'] - const fromArgv = process.argv[2] - const requested = fromEnv !== undefined && fromEnv !== '' - ? fromEnv - : fromArgv !== undefined && fromArgv !== '' ? fromArgv : undefined - const configPath = requested === undefined ? undefined : resolveConfigPath(requested, undefined) - if (configPath === undefined || !existsSync(configPath)) { - process.stderr.write( - `usage: ${NAME} (or set DSH_CORDIS_CONFIG=, which wins); the config is required — there is no built-in fallback\n`, - ) - process.exit(1) - } - - const ctx = await boot(NAME, configPath, undefined, undefined, bareModuleBaseUrl) - let exiting = false - - async function disposeAndExit(code: number): Promise { - if (exiting) return - exiting = true - try { - await ctx.fiber.dispose() - } finally { - process.exit(code) - } - } - - process.stdin.on('end', () => { void disposeAndExit(0) }) - process.on('SIGTERM', () => { void disposeAndExit(0) }) - process.on('SIGINT', () => { void disposeAndExit(130) }) -} -/* v8 ignore stop */ diff --git a/packages/sdk/python-runtime/tsdown.config.ts b/packages/sdk/python-runtime/tsdown.config.ts deleted file mode 100644 index 9b6d5d33a7..0000000000 --- a/packages/sdk/python-runtime/tsdown.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { defineConfig } from 'tsdown' - -/** Builds each published entry as a self-contained file admitted by the package whitelist. */ -export default defineConfig([ - { - entry: ['lib/types/index.js'], outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', - fixedExtension: false, outputOptions: { codeSplitting: false }, dts: false, clean: false, - }, - { - entry: ['lib/types/invariant.js'], outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', - fixedExtension: false, outputOptions: { codeSplitting: false }, dts: false, clean: false, - }, - { - entry: ['lib/types/packaged-bin.js'], outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', - fixedExtension: false, outputOptions: { codeSplitting: false }, dts: false, clean: false, - }, -]) diff --git a/packages/sdk/server/README.i18n.yaml b/packages/sdk/server/README.i18n.yaml index adf84f76c4..3ffdb5d1e4 100644 --- a/packages/sdk/server/README.i18n.yaml +++ b/packages/sdk/server/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/sdk/server/README.md -README.md: e3deaa5288eb94d64a7f5a8425a829e3eed8896e -README.zh.md: a7273e9b4efbfae8e849205ea86b4c0c90588f7c +README.md: 2fac60b9313c66a8eb1653adb02405f2f5fe4b08 +README.zh.md: ed2c18f0a2ee96fdacdf0d998314d349f0b0264b diff --git a/packages/sdk/server/README.md b/packages/sdk/server/README.md index e3deaa5288..2fac60b931 100644 --- a/packages/sdk/server/README.md +++ b/packages/sdk/server/README.md @@ -2,15 +2,15 @@ English | [中文](README.zh.md) -The `jsonrpc` plugin serves newline-delimited JSON-RPC over stdio so out-of-process SDK clients can drive harness agents. [`HarnessSdkJsonRpcServer`](src/server.ts) owns the protocol methods and notifications; the transport and the named wire types live in [`dsh-sdk-protocol`](../protocol/README.md), shared with the client SDKs. The TypeScript client receives this server through `dsh --profile sdk`; the private [Python runtime carrier](../python-runtime/README.md) temporarily supplies a direct-config application around it. +The `jsonrpc` plugin serves newline-delimited JSON-RPC over stdio so out-of-process SDK clients can drive harness agents. [`HarnessSdkJsonRpcServer`](src/server.ts) owns the protocol methods and notifications; the transport and the named wire types live in [`dsh-sdk-protocol`](../protocol/README.md), shared with the client SDKs. TypeScript and Python clients receive this server through `dsh --profile sdk` or another profile that mounts the same row. ## Wiring -`inject: ['agents']`. The server gets or creates one agent per `sessionId`. It forwards subagent completions only when the service-snapshotted lifecycle `local` flag is true; provider names, child ids, and durable lineage never establish locality. A registered adapter wins, an unowned `deepseek-official` route mounts `dsh-llm-deepseek`, and any other unowned provider fails initialization. Other capabilities come from the surrounding `cordis.yml`. +`inject: ['agents']`. The server gets or creates one agent per `sessionId`. It forwards subagent completions only when the service-snapshotted lifecycle `local` flag is true; provider names, child ids, and durable lineage never establish locality. A registered adapter wins, an unowned `deepseek-official` route mounts `dsh-llm-deepseek`, and any other unowned provider fails initialization. Other capabilities come from the surrounding Loader composition. ## Config -`maxTokensAsSuccess` defaults to `false` and affects only the deployment-mapped status on `subagent.finished`; root-session prompts have no prompt-level status. `JsonRpcConfig.input`, `output`, and `exit` are runtime-only transport hooks; production uses process stdio and `process.exit`. +`maxTokensAsSuccess` defaults to `false` and affects only the deployment-mapped status on `subagent.finished`; root-session prompts have no prompt-level status. Optional `toolFilter.allow` and `toolFilter.deny` restrict each SDK-created root agent through `ctx.tools.restrict()`. An allow list excludes later global tool registrations that it does not name, so a fixed SDK deployment cannot silently gain model-facing tools when its base bundle expands. Unknown names and an empty filter fail when the first session is created. `JsonRpcConfig.input`, `output`, and `exit` are runtime-only transport hooks; production uses process stdio and `process.exit`. ## stdout is the protocol @@ -22,7 +22,7 @@ The plugin answers `shutdown`, flushes the response, disposes the root context s ## Wire notes -`initialize` is the runtime-readiness boundary: when the server is mounted by a Loader composition, it waits for the current plugin tree to settle before replying, so async sibling capabilities such as initial MCP tool discovery are visible to the first prompt. Hand-built contexts without Loader remain immediately usable. `initialize.serverInfo.name` is the wire-stable `deepseek-harness-sdk-runtime`. An optional positive `initialize.maxTokens` becomes the request output cap of each SDK-created agent and its in-process descendants; invalid values reject initialization, while omission sends no SDK cap and allows the selected adapter or provider route default to apply. `session/prompt` queues one identified user message and immediately returns `{ messageId }`. The server streams every durable fact as `session.event` and every whole-agent lifecycle transition as `session.status`; it does not assign an assistant message or `turn/end` to that prompt. Independent requests may enqueue more work on the same session. Persistence roots and persona come from `cordis.yml`. +`initialize` is the runtime-readiness boundary: when the server is mounted by a Loader composition, it waits for the current plugin tree to settle before replying, so async sibling capabilities such as initial MCP tool discovery are visible to the first prompt. Hand-built contexts without Loader remain immediately usable. `initialize.serverInfo.name` is the wire-stable `deepseek-harness-sdk-runtime`. An optional positive `initialize.maxTokens` becomes the request output cap of each SDK-created agent and its in-process descendants; invalid values reject initialization, while omission sends no SDK cap and allows the selected adapter or provider route default to apply. `session/prompt` queues one identified user message and immediately returns `{ messageId }`. The server streams every durable fact as `session.event` and every whole-agent lifecycle transition as `session.status`; it does not assign an assistant message or `turn/end` to that prompt. Independent requests may enqueue more work on the same session. Persistence roots and persona come from the surrounding composition. ## Model Experience @@ -30,7 +30,7 @@ The plugin answers `shutdown`, flushes the response, disposes the root context s #### What the model sees -For each accepted `session/prompt`, the conversation model receives the caller-supplied `contentBlocks` verbatim as one user message in that SDK session. This package adds no system-prompt prose or tool schema; those come from the plugins in the surrounding `cordis.yml`. +For each accepted `session/prompt`, the conversation model receives the caller-supplied `contentBlocks` verbatim as one user message in that SDK session. This package adds no system-prompt prose or tool schema; those come from the other plugins in the composition. A configured `toolFilter` projects that composition's global tool registry before the request is assembled and executed. #### Token effect diff --git a/packages/sdk/server/README.zh.md b/packages/sdk/server/README.zh.md index a7273e9b4e..ed2c18f0a2 100644 --- a/packages/sdk/server/README.zh.md +++ b/packages/sdk/server/README.zh.md @@ -2,15 +2,15 @@ [English](README.md) | 中文 -`jsonrpc` 插件通过 stdio 提供以换行符分隔的 JSON-RPC,使进程外 SDK 客户端能够驱动 harness agent(智能体)。[`HarnessSdkJsonRpcServer`](src/server.ts) 负责协议方法和通知;传输与具名协议类型位于 [`dsh-sdk-protocol`](../protocol/README.zh.md),与客户端 SDK 共享。TypeScript 客户端通过 `dsh --profile sdk` 获得该服务器;私有 [Python 运行时载体](../python-runtime/README.zh.md)暂时为其提供直读配置应用。 +`jsonrpc` 插件通过 stdio 提供以换行符分隔的 JSON-RPC,使进程外 SDK 客户端能够驱动 harness agent(智能体)。[`HarnessSdkJsonRpcServer`](src/server.ts) 负责协议方法和通知;传输与具名协议类型位于 [`dsh-sdk-protocol`](../protocol/README.zh.md),与客户端 SDK 共享。TypeScript 与 Python 客户端都通过 `dsh --profile sdk` 或挂载同一配置项的其他 profile 获得该服务器。 ## 组装 -`inject: ['agents']`。服务器按 `sessionId` 获取或创建一个 agent。只有服务对生命周期建立快照时记录的 `local` 标志为 true,服务器才会转发 subagent 完成事件;提供方名称、子级 id 和持久化谱系均不能证明本地性。已注册的适配器优先;尚无适配器负责的 `deepseek-official` 路由会挂载 `dsh-llm-deepseek`,任何其他尚无适配器负责的提供方都会导致初始化失败。其他能力由外围 `cordis.yml` 提供。 +`inject: ['agents']`。服务器按 `sessionId` 获取或创建一个 agent。只有服务对生命周期建立快照时记录的 `local` 标志为 true,服务器才会转发 subagent 完成事件;提供方名称、子级 id 和持久化谱系均不能证明本地性。已注册的适配器优先;尚无适配器负责的 `deepseek-official` 路由会挂载 `dsh-llm-deepseek`,任何其他尚无适配器负责的提供方都会导致初始化失败。其他能力由外围 Loader 组合提供。 ## 配置 -`maxTokensAsSuccess` 默认为 `false`,且只影响 `subagent.finished` 上由部署映射的状态;根会话提示词没有提示词级状态。`JsonRpcConfig.input`、`output` 和 `exit` 是仅供运行时使用的传输钩子;生产环境使用进程 stdio 和 `process.exit`。 +`maxTokensAsSuccess` 默认为 `false`,且只影响 `subagent.finished` 上由部署映射的状态;根会话提示词没有提示词级状态。可选的 `toolFilter.allow` 与 `toolFilter.deny` 通过 `ctx.tools.restrict()` 限制每个由 SDK 创建的根 agent。Allow 列表会排除之后出现但未指名的全局工具,因此固定的 SDK 部署不会在基础 bundle 扩展时静默获得面向模型的新工具。未知名称与空筛选器会在创建首个会话时明确失败。`JsonRpcConfig.input`、`output` 和 `exit` 是仅供运行时使用的传输钩子;生产环境使用进程 stdio 和 `process.exit`。 ## stdout 即协议 @@ -22,7 +22,7 @@ Stdout 只承载 JSON-RPC 帧。部署不得组合 stdout logger;诊断应写 ## 协议说明 -`initialize` 是运行时就绪边界:服务器由 Loader 组合挂载时,会等待当前插件树完成所有加载任务后再响应,因此首次提示词能够看到 MCP 初始工具发现等异步同级能力。没有 Loader 的手工组装上下文仍可立即使用。`initialize.serverInfo.name` 的协议稳定值为 `deepseek-harness-sdk-runtime`。可选的正整数 `initialize.maxTokens` 会成为每个 SDK 创建的 agent 及其进程内后代的请求输出上限;非法值会使初始化失败,省略时则不发送 SDK 上限,并应用所选适配器或提供方路由的默认值。`session/prompt` 将一条带标识的用户消息排入队列,并立即返回 `{ messageId }`。服务器将每个持久事实作为 `session.event` 流式发出,并将整个 agent 生命周期的每次状态转换作为 `session.status` 发出;它不会把某条助手消息或 `turn/end` 归属于该提示词。同一会话上的独立请求可以继续排入更多工作。持久化根目录和 persona 由 `cordis.yml` 提供。 +`initialize` 是运行时就绪边界:服务器由 Loader 组合挂载时,会等待当前插件树完成所有加载任务后再响应,因此首次提示词能够看到 MCP 初始工具发现等异步同级能力。没有 Loader 的手工组装上下文仍可立即使用。`initialize.serverInfo.name` 的协议稳定值为 `deepseek-harness-sdk-runtime`。可选的正整数 `initialize.maxTokens` 会成为每个 SDK 创建的 agent 及其进程内后代的请求输出上限;非法值会使初始化失败,省略时则不发送 SDK 上限,并应用所选适配器或提供方路由的默认值。`session/prompt` 将一条带标识的用户消息排入队列,并立即返回 `{ messageId }`。服务器将每个持久事实作为 `session.event` 流式发出,并将整个 agent 生命周期的每次状态转换作为 `session.status` 发出;它不会把某条助手消息或 `turn/end` 归属于该提示词。同一会话上的独立请求可以继续排入更多工作。持久化根目录和 persona 由外围组合提供。 ## 模型体验 @@ -30,7 +30,7 @@ Stdout 只承载 JSON-RPC 帧。部署不得组合 stdout logger;诊断应写 #### 模型看到的内容 -对于每个已接受的 `session/prompt`,对话模型会将调用方提供的 `contentBlocks` 原样作为该 SDK 会话中的一条用户消息接收。此包不会添加系统提示词文本或工具 schema;这些内容来自外围 `cordis.yml` 中的插件。 +对于每个已接受的 `session/prompt`,对话模型会将调用方提供的 `contentBlocks` 原样作为该 SDK 会话中的一条用户消息接收。此包不会添加系统提示词文本或工具 schema;这些内容来自组合中的其他插件。配置的 `toolFilter` 会在请求组装与执行前投影该组合的全局工具注册表。 #### Token 影响 diff --git a/packages/sdk/server/src/index.ts b/packages/sdk/server/src/index.ts index 963b4fb3bd..17e9d3a89f 100644 --- a/packages/sdk/server/src/index.ts +++ b/packages/sdk/server/src/index.ts @@ -25,6 +25,13 @@ export const inject = ['agents'] export interface JsonRpcConfig { /** Report max-token turn/subagent termination as a successful SDK result. */ maxTokensAsSuccess?: boolean + /** Per-root-agent model-facing tool filter; an allow list excludes later unnamed global tools. */ + toolFilter?: { + /** Global tool names that remain visible. */ + allow?: string[] + /** Global tool names removed from visibility. */ + deny?: string[] + } /** Transport input override; production uses `process.stdin`. */ input?: Readable /** Transport output override; production uses `process.stdout`. */ @@ -35,6 +42,11 @@ export interface JsonRpcConfig { export const Config: Schema = Schema.object({ maxTokensAsSuccess: Schema.boolean().default(false), + // Preserve omission; Schemastery's materialized empty object is not a valid restriction. + toolFilter: Schema.object({ + allow: Schema.array(Schema.string()).default(undefined as unknown as string[]), + deny: Schema.array(Schema.string()).default(undefined as unknown as string[]), + }).default(undefined as unknown as { allow: string[]; deny: string[] }), }) /** @@ -59,6 +71,7 @@ export function apply(ctx: Context, config: JsonRpcConfig): void { const transport = new JsonRpcLineTransport(input, output) const server = new HarnessSdkJsonRpcServer(ctx, transport, { maxTokensAsSuccess: resolvedConfig.maxTokensAsSuccess, + ...resolvedConfig.toolFilter === undefined ? {} : { toolFilter: resolvedConfig.toolFilter }, }) // Share one exit task so racing shutdown requests cannot dispose the root or diff --git a/packages/sdk/server/src/server.ts b/packages/sdk/server/src/server.ts index ccc3b6192d..a2ec97d9f4 100644 --- a/packages/sdk/server/src/server.ts +++ b/packages/sdk/server/src/server.ts @@ -13,6 +13,7 @@ import { carrierKeyOf, type Scoped } from '@deepseek-ai/dsh-scope' import { SessionId } from '@deepseek-ai/dsh-session' import type SubagentRuntime from '@deepseek-ai/dsh-subagent' import type { SubagentRunEndInfo } from '@deepseek-ai/dsh-subagent' +import type { ToolRestriction } from '@deepseek-ai/dsh-tools' import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek' import type { InitializeParams, @@ -38,6 +39,8 @@ function subagentParentOf(carrier: Scoped): Agent { export interface HarnessSdkJsonRpcServerOptions { /** Report max-token termination as an accepted result instead of an infrastructure error. */ maxTokensAsSuccess?: boolean + /** Restrict each SDK-created root agent to an explicit subset of global tools. */ + toolFilter?: ToolRestriction } function successStatus(reason: string, options: HarnessSdkJsonRpcServerOptions): 'ok' | 'error' { @@ -220,6 +223,7 @@ export class HarnessSdkJsonRpcServer { // rows in the host plane, so this agent reads them from the global layer. A // deployment that configures a roster has to join one here first // (@deepseek-ai/dsh-agent-presets README, "Composing a child agent"). + const toolFilter = this.options.toolFilter const handle = await this.ctx.agents.create({ sessionId: SessionId(sessionId), meta: { cwd: this.cwd }, @@ -228,6 +232,9 @@ export class HarnessSdkJsonRpcServer { model: this.model, ...this.maxTokens === undefined ? {} : { maxTokens: this.maxTokens }, }, + ...toolFilter === undefined + ? {} + : { setup: (agentCtx: Context) => { agentCtx.tools.restrict(toolFilter) } }, }) const rec: SessionRecord = { handle } this.sessions.set(sessionId, rec) diff --git a/packages/sdk/server/tests/built-scope-carrier.e2e.ts b/packages/sdk/server/tests/built-scope-carrier.e2e.ts index 0aa7099d0b..ae4ab8ef05 100644 --- a/packages/sdk/server/tests/built-scope-carrier.e2e.ts +++ b/packages/sdk/server/tests/built-scope-carrier.e2e.ts @@ -66,7 +66,7 @@ try { const result = Promise.withResolvers(); const unregister = ctx.subagents.registerProvider({ name: "built-local", - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, start() { return Promise.resolve({ diff --git a/packages/sdk/server/tests/plugin-apply.spec.ts b/packages/sdk/server/tests/plugin-apply.spec.ts index 28c89c19fe..41954007eb 100644 --- a/packages/sdk/server/tests/plugin-apply.spec.ts +++ b/packages/sdk/server/tests/plugin-apply.spec.ts @@ -11,6 +11,7 @@ import * as agentCore from '@deepseek-ai/dsh-agent-spine-demo' import { LlmAdapter } from '@deepseek-ai/dsh-llm' import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' +import { defineTool } from '@deepseek-ai/dsh-tools' import * as jsonrpc from '../src/index.ts' /** @@ -71,6 +72,7 @@ async function mountPlugin( writeDelayMs?: number failFlush?: boolean beforeServer?: (ctx: Context) => Promise | void + toolFilter?: jsonrpc.JsonRpcConfig['toolFilter'] } = {}, ): Promise { const ctx = new Context() @@ -116,7 +118,12 @@ async function mountPlugin( const exit = (code: number): void => { events.push({ kind: 'exit', code }) } ctx.effect(() => () => { events.push({ kind: 'root-disposed' }) }, 'jsonrpc test root-disposal witness') - const fiber = await ctx.plugin(jsonrpc, { input, output, exit }) + const fiber = await ctx.plugin(jsonrpc, { + input, + output, + exit, + ...options.toolFilter === undefined ? {} : { toolFilter: options.toolFilter }, + }) const frames = (): Record[] => events.flatMap(event => event.kind === 'frame' ? [event.frame] : []) @@ -282,6 +289,51 @@ describe('dsh-sdk-jsonrpc-server plugin apply', () => { } }) + it('applies the configured root-agent tool filter through the Loader plugin', async () => { + const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-apply-tool-filter-')) + const llmServer = await mockCompletionServer() + vi.stubEnv('DEEPSEEK_API_KEY', 'test-key') + vi.stubEnv('DEEPSEEK_BASE_URL', llmServer.url) + const harness = await mountPlugin(storageDir, { + toolFilter: { allow: ['kept'] }, + beforeServer: (ctx) => { + for (const name of ['kept', 'excluded']) { + ctx.tools.register(defineTool({ + name, + description: name, + parameters: {}, + output: { + schema: { type: 'string' }, + render: (_args, value) => [{ type: 'text', text: value }], + }, + execute: async () => name, + })) + } + }, + }) + try { + harness.send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { cwd: storageDir, provider: 'deepseek-official', model: 'filtered-model' } }) + await harness.waitForFrame(frame => frame.id === 1, 'initialize response') + harness.send({ + jsonrpc: '2.0', + id: 2, + method: 'session/prompt', + params: { sessionId: 'filtered', contentBlocks: [{ type: 'text', text: 'inspect tools' }] }, + }) + await harness.waitForFrame( + frame => frame.method === 'session.status' + && (frame.params as { status?: string } | undefined)?.status === 'idle', + 'filtered session idle status', + ) + + const request = llmServer.requests[0] as { tools?: Array<{ function?: { name?: string } }> } + expect(request.tools?.map(entry => entry.function?.name)).toEqual(['kept']) + } finally { + await harness.dispose() + await rm(storageDir, { recursive: true, force: true }) + } + }) + it('answers shutdown before exiting 0 exactly once, even against a racing second shutdown', async () => { const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-apply-shutdown-')) const harness = await mountPlugin(storageDir, { writeDelayMs: 10 }) diff --git a/packages/sdk/server/tests/server.spec.ts b/packages/sdk/server/tests/server.spec.ts index 495f1d90f4..257c3e72b0 100644 --- a/packages/sdk/server/tests/server.spec.ts +++ b/packages/sdk/server/tests/server.spec.ts @@ -14,6 +14,7 @@ import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek' import SubagentRuntime, { type SubagentResult, type SubagentRunEndInfo } from '@deepseek-ai/dsh-subagent' import type { JsonRpcTransportPeer } from '@deepseek-ai/dsh-sdk-protocol' +import { defineTool } from '@deepseek-ai/dsh-tools' import { HarnessSdkJsonRpcServer } from '../src/index.ts' class FakeTransport implements JsonRpcTransportPeer { @@ -78,7 +79,7 @@ async function settleSubagent( const result = Promise.withResolvers() const disposeProvider = ctx.subagents.registerProvider({ name: info.provider, - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, async start() { return { @@ -171,6 +172,46 @@ describe('HarnessSdkJsonRpcServer', () => { } }) + it('allowlists each root session against current and later global tools', { timeout: 15_000 }, async () => { + const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-tool-filter-')) + const llmServer = await mockCompletionServer() + vi.stubEnv('DEEPSEEK_API_KEY', 'test-key') + vi.stubEnv('DEEPSEEK_BASE_URL', llmServer.url) + const ctx = await makeHarness(storageDir) + const tool = (name: string) => defineTool({ + name, + description: name, + parameters: {}, + output: { + schema: { type: 'string' as const }, + render: (_args, value) => [{ type: 'text' as const, text: value }], + }, + execute: async () => name, + }) + ctx.tools.register(tool('kept')) + ctx.tools.register(tool('excluded')) + const server = new HarnessSdkJsonRpcServer(ctx, new FakeTransport(), { + toolFilter: { allow: ['kept'] }, + }) + try { + await server.initialize({ cwd: storageDir, provider: 'deepseek-official', model: 'filtered-model' }) + await server.prompt({ sessionId: 'first', contentBlocks: [{ type: 'text', text: 'first' }] }) + await vi.waitFor(() => { expect(llmServer.requests).toHaveLength(1) }) + ctx.tools.register(tool('future')) + await server.prompt({ sessionId: 'second', contentBlocks: [{ type: 'text', text: 'second' }] }) + await vi.waitFor(() => { expect(llmServer.requests).toHaveLength(2) }) + + expect(llmServer.requests.map((request) => { + const tools = (request as { tools?: Array<{ function?: { name?: string } }> }).tools ?? [] + return tools.map(entry => entry.function?.name) + })).toEqual([['kept'], ['kept']]) + await server.shutdown() + } finally { + await ctx.fiber.dispose() + await rm(storageDir, { recursive: true, force: true }) + } + }) + it('queues overlapping prompts for one session without blocking other sessions', async () => { const mainFollowup = vi.fn() const mainAgent = ({ @@ -497,7 +538,7 @@ describe('HarnessSdkJsonRpcServer', () => { let currentLocalAgent = oldChild.agent const disposeProvider = ctx.subagents.registerProvider({ name: 'reused', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, start() { const result = results[starts] @@ -592,7 +633,7 @@ describe('HarnessSdkJsonRpcServer', () => { const remoteResult = Promise.withResolvers() const unregisterLocal = ctx.subagents.registerProvider({ name: 'reused-provider', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, start: () => Promise.resolve({ id: SessionId('provider-reuse-child'), @@ -610,7 +651,7 @@ describe('HarnessSdkJsonRpcServer', () => { const unregisterRemote = ctx.subagents.registerProvider({ name: 'reused-provider', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, start: () => Promise.resolve({ id: SessionId('provider-reuse-child'), @@ -690,7 +731,7 @@ describe('HarnessSdkJsonRpcServer', () => { const missedStartResult = Promise.withResolvers() const disposeMissedStartProvider = ctx.subagents.registerProvider({ name: 'fork', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: true, start: () => Promise.resolve({ id: SessionId('fallback-child-session'), diff --git a/packages/subagent/subagent-acp/README.i18n.yaml b/packages/subagent/subagent-acp/README.i18n.yaml index fa56c715a5..73f55ccfbf 100644 --- a/packages/subagent/subagent-acp/README.i18n.yaml +++ b/packages/subagent/subagent-acp/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/subagent/subagent-acp/README.md -README.md: 0201f9bacca5c59031d2fc2eec7fe7210489dd09 -README.zh.md: c987e6251e035f0cf94ac41b570f3be10f0118f5 +README.md: cc4deb5b97152f106caabf747b8c7ccb2f5ddf8e +README.zh.md: e28fb556801d4567bcc606a777e3b090e0551e03 diff --git a/packages/subagent/subagent-acp/README.md b/packages/subagent/subagent-acp/README.md index 0201f9bacc..cc4deb5b97 100644 --- a/packages/subagent/subagent-acp/README.md +++ b/packages/subagent/subagent-acp/README.md @@ -18,7 +18,7 @@ After publication, the provider sends the prompt and collects streamed `agent_me ## Capabilities and context -ACP advertises no start-time capabilities because this process cannot enforce the remote child's depth, tool filter, persona, or structured-output runtime. It also reports `inheritsParentContext: false`: the remote session starts fresh, and the only parent-derived input is the workspace cwd described above — no conversation context crosses the process boundary. +ACP advertises no start-time capabilities because this process cannot apply `request.agentOptions` or enforce the remote child's depth, tool filter, persona, or structured-output runtime. It also reports `inheritsParentContext: false`: the remote session starts fresh, and the only parent-derived input is the workspace cwd described above — no conversation context crosses the process boundary. ## Configuration @@ -70,7 +70,7 @@ The package has no default export. Cordis loader unwrapping would otherwise hide #### What the model sees -The remote child receives the standalone task content through ACP plus its own process's configured system prompt, tools, and fresh session. It receives no parent conversation. This provider advertises no optional start-time capabilities, so the local service rejects requests for persona, tool filtering, depth enforcement, or structured output instead of silently omitting them. +The remote child receives the standalone task content through ACP plus its own process's configured system prompt, tools, and fresh session. It receives no parent conversation. This provider advertises no optional start-time capabilities, so the local service rejects requests for `agentOptions`, persona, tool filtering, depth enforcement, or structured output instead of silently omitting them. #### Token effect @@ -98,6 +98,6 @@ Append-only; newly visible content follows the reusable request prefix and does - **A fresh process per run** — persistent-process pooling is a future optimization ([the seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)). - **Local workspaces only** — the resolved cwd is a local path handed to a child on the same machine; workspace mapping for a remote ACP agent would need its own backend capability and is not designed here. -- **No optional start-time capabilities** — this provider cannot apply the local harness's `outputSchema`, depth cap, tool filter, or persona inside the remote process, so it advertises none and the service rejects requests that require them. +- **No optional start-time capabilities** — this provider cannot apply the local harness's `agentOptions`, `outputSchema`, depth cap, tool filter, or persona inside the remote process, so it advertises none and the service rejects requests that require them. - **Only committed `agent_message_chunk` text is collected** — the automation server keeps reasoning, tool activity, plans, and other trace data in the child session log rather than emitting them on ACP. - **Permission prompts are auto-answered** (`permission: allow | reject`) — no human is surfaced a child's `session/request_permission`. diff --git a/packages/subagent/subagent-acp/README.zh.md b/packages/subagent/subagent-acp/README.zh.md index c987e6251e..e28fb55680 100644 --- a/packages/subagent/subagent-acp/README.zh.md +++ b/packages/subagent/subagent-acp/README.zh.md @@ -18,7 +18,7 @@ ACP(Agent Client Protocol)提供方会在全新的子进程中运行每个 s ## 能力与上下文 -ACP 不声明任何启动时能力,因为当前进程无法强制执行远程子 agent 的深度、工具过滤、persona 或结构化输出运行时。它也报告 `inheritsParentContext: false`:远程会话从全新状态开始,唯一源自父级的输入是上述工作区 cwd;对话上下文不会跨越进程边界。 +ACP 不声明任何启动时能力,因为当前进程无法应用 `request.agentOptions`,也无法强制执行远程子 agent 的深度、工具过滤、persona 或结构化输出运行时。它也报告 `inheritsParentContext: false`:远程会话从全新状态开始,唯一源自父级的输入是上述工作区 cwd;对话上下文不会跨越进程边界。 ## 配置 @@ -70,7 +70,7 @@ DeepSeek Harness 子进程使用产品启动器和一个显式的绝对路径 `D #### 模型看到的内容 -远程子 agent 通过 ACP 接收独立任务内容,并使用其自身进程配置的系统提示词、工具和全新会话。它不接收父级对话。该提供方不声明任何可选启动时能力,因此本地服务会拒绝要求 persona、工具过滤、深度强制或结构化输出的请求,而不是静默省略这些要求。 +远程子 agent 通过 ACP 接收独立任务内容,并使用其自身进程配置的系统提示词、工具和全新会话。它不接收父级对话。该提供方不声明任何可选启动时能力,因此本地服务会拒绝要求 `agentOptions`、persona、工具过滤、深度强制或结构化输出的请求,而不是静默省略这些要求。 #### Token 影响 @@ -98,6 +98,6 @@ DeepSeek Harness 子进程使用产品启动器和一个显式的绝对路径 `D - **每次运行使用全新进程**:持久进程池属于后续优化(见 [seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.zh.md))。 - **仅支持本地工作区**:解析后的 cwd 是交给同一台机器上子进程的本地路径;远程 ACP agent 的工作区映射需要独立的后端能力,此处尚未设计这种能力。 -- **不支持可选启动时能力**:该提供方无法在远程进程内应用本地 harness 的 `outputSchema`、深度上限、工具过滤器或 persona,因此不会声明这些能力;服务会拒绝需要它们的请求。 +- **不支持可选启动时能力**:该提供方无法在远程进程内应用本地 harness 的 `agentOptions`、`outputSchema`、深度上限、工具过滤器或 persona,因此不会声明这些能力;服务会拒绝需要它们的请求。 - **只收集已提交的 `agent_message_chunk` 文本**:自动化服务器把推理(reasoning)、工具活动、计划和其他 trace 数据保留在子 agent 会话日志中,不通过 ACP 发出。 - **权限提示自动回答**(`permission: allow | reject`):不会把子 agent 的 `session/request_permission` 呈现给人。 diff --git a/packages/subagent/subagent-acp/src/index.ts b/packages/subagent/subagent-acp/src/index.ts index 4b526279ba..4c610ef2f9 100644 --- a/packages/subagent/subagent-acp/src/index.ts +++ b/packages/subagent/subagent-acp/src/index.ts @@ -140,11 +140,17 @@ function resolveCwd(configured: string | undefined, request: SubagentStartReques /** * The ACP provider. Advertises NO start-time capabilities: an out-of-process - * child cannot honor `outputSchema`/`maxDepth`/`toolFilter` (the service rejects - * a request needing any of them before `start` runs). + * child cannot honor `agentOptions`/`outputSchema`/`maxDepth`/`toolFilter`/ + * `persona` (the service rejects a request needing any before `start` runs). */ class AcpProvider implements SubagentProvider { - readonly capabilities: SubagentCapabilities = { outputSchema: false, depthLimit: false, toolFilter: false, persona: false } + readonly capabilities: SubagentCapabilities = { + agentOptions: false, + outputSchema: false, + depthLimit: false, + toolFilter: false, + persona: false, + } // Context contract: an out-of-process ACP child starts fresh — no parent conversation crosses the process boundary. readonly inheritsParentContext = false diff --git a/packages/subagent/subagent-acp/tests/subagent-acp.spec.ts b/packages/subagent/subagent-acp/tests/subagent-acp.spec.ts index c534b8e949..d4553d8f86 100644 --- a/packages/subagent/subagent-acp/tests/subagent-acp.spec.ts +++ b/packages/subagent/subagent-acp/tests/subagent-acp.spec.ts @@ -863,7 +863,13 @@ describe('dsh-subagent-acp', () => { it('advertises no start-time capabilities (out-of-process child)', async () => { const ctx = await setup() const provider = ctx.subagents.getProvider('acp')! - expect(provider.capabilities).toEqual({ outputSchema: false, depthLimit: false, toolFilter: false, persona: false }) + expect(provider.capabilities).toEqual({ + agentOptions: false, + outputSchema: false, + depthLimit: false, + toolFilter: false, + persona: false, + }) }) it('unregisters the provider when its fiber is disposed (HMR safety)', async () => { diff --git a/packages/subagent/subagent-claude-code/README.i18n.yaml b/packages/subagent/subagent-claude-code/README.i18n.yaml index ff129c60e2..f894fb15ab 100644 --- a/packages/subagent/subagent-claude-code/README.i18n.yaml +++ b/packages/subagent/subagent-claude-code/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/subagent/subagent-claude-code/README.md -README.md: 0260d9c82dee82541e5b60ff7fe2c323cf9b7331 -README.zh.md: 484241f1ca23f2b8ee843b6f412d0f779832d400 +README.md: 30b61347dfda110fe95a9153aeea35c8f760012f +README.zh.md: 17ddac7d4beebfbf0f374f606a68d77b13ec2b77 diff --git a/packages/subagent/subagent-claude-code/README.md b/packages/subagent/subagent-claude-code/README.md index 0260d9c82d..30b61347df 100644 --- a/packages/subagent/subagent-claude-code/README.md +++ b/packages/subagent/subagent-claude-code/README.md @@ -20,7 +20,7 @@ Each query sets `persistSession: false` and disables `AskUserQuestion`. Except i ## Capabilities and context -The provider advertises no optional start-time capabilities and reports `inheritsParentContext: false`. Claude Code receives the standalone text task and the parent Session cwd, but not the parent conversation, persona, tool filter, depth policy, or structured-output contract. Every run has an independent SDK query, cancellation controller, CLI process, and non-persisted product session. +The provider advertises no optional start-time capabilities and reports `inheritsParentContext: false`. The shared service rejects `request.agentOptions` for this provider. Claude Code receives the standalone text task and the parent Session cwd, but not the parent conversation, persona, tool filter, depth policy, or structured-output contract. Every run has an independent SDK query, cancellation controller, CLI process, and non-persisted product session. ## Configuration @@ -145,5 +145,5 @@ Append-only: foreground adds one result after the reusable parent prefix, while - **The SDK platform payload is required at delegation time** — installs that omit optional dependencies, unsupported platforms, and missing or damaged payloads fail at the first query; there is no host-CLI fallback. - **No human interaction path** — `AskUserQuestion` is disabled, permission prompts are denied, MCP elicitation is declined, and blocking dialogs fail closed instead of suspending. - **Assistant payload is final text only** — a failed run may additionally expose the separate safe diagnostic; reasoning, intermediate messages, tool traffic, usage, stderr, and workspace diffs remain product-local, while generic Job ids, notices, and status come from the shared job runtime. -- **No optional shared capabilities** — output schemas, child personas, tool filtering, and harness depth enforcement are rejected by the shared service for this provider. +- **No optional shared capabilities** — `agentOptions`, output schemas, child personas, tool filtering, and harness depth enforcement are rejected by the shared service for this provider. - **No wall-clock timeout or side-effect rollback** — the caller cancels long work, and files or external systems changed before cancellation are not restored. diff --git a/packages/subagent/subagent-claude-code/README.zh.md b/packages/subagent/subagent-claude-code/README.zh.md index 484241f1ca..17ddac7d4b 100644 --- a/packages/subagent/subagent-claude-code/README.zh.md +++ b/packages/subagent/subagent-claude-code/README.zh.md @@ -20,7 +20,7 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK ## 能力与上下文 -本提供方不声明任何可选的启动时能力,并报告 `inheritsParentContext: false`。Claude Code 会接收独立文本任务和父会话 cwd,但不会接收父会话的对话、角色设定、工具筛选器、深度策略或结构化输出约定。每次运行都拥有独立的 SDK query、取消控制器、CLI 进程和不持久化的产品会话。 +本提供方不声明任何可选的启动时能力,并报告 `inheritsParentContext: false`。共享服务会拒绝本提供方的 `request.agentOptions`。Claude Code 会接收独立文本任务和父会话 cwd,但不会接收父会话的对话、角色设定、工具筛选器、深度策略或结构化输出约定。每次运行都拥有独立的 SDK query、取消控制器、CLI 进程和不持久化的产品会话。 ## 配置 @@ -145,5 +145,5 @@ Claude Code 子级会在一个全新的 SDK query 中接收独立文本任务。 - **委派时必须存在 SDK 平台载荷**:省略 optional dependencies 的安装、不受支持的平台以及缺失或损坏的载荷都会在第一次 query 时失败;不会回退到宿主 CLI。 - **没有人工交互路径**:`AskUserQuestion` 被禁用,权限提示会被拒绝,MCP elicitation 会被拒绝,阻塞对话会快速失败而不会挂起。 - **assistant 载荷仅包含最终文本**:失败运行可以额外公开独立的安全诊断;推理、中间消息、工具通信、用量信息、stderr 和工作区差异仍只保留在产品内部,通用 Job id、通知与状态来自共享作业运行时。 -- **没有可选的共享能力**:对于本提供方,共享服务会拒绝输出 schema、子任务角色设定、工具筛选和 harness 深度强制约束。 +- **没有可选的共享能力**:对于本提供方,共享服务会拒绝 `agentOptions`、输出 schema、子任务角色设定、工具筛选和 harness 深度强制约束。 - **没有按实际经过时间触发的超时或副作用回滚**:长时间运行的工作由调用方取消,且取消前已更改的文件或外部系统不会恢复原状。 diff --git a/packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts b/packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts index 5f339ce22e..6054370615 100644 --- a/packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts +++ b/packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts @@ -45,6 +45,7 @@ describe('product-provider public Loader composition', () => { { name: 'codex', capabilities: { + agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, @@ -55,6 +56,7 @@ describe('product-provider public Loader composition', () => { { name: 'claude-code', capabilities: { + agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, @@ -65,6 +67,7 @@ describe('product-provider public Loader composition', () => { { name: 'claude-primary', capabilities: { + agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, @@ -75,6 +78,7 @@ describe('product-provider public Loader composition', () => { { name: 'claude-secondary', capabilities: { + agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, diff --git a/packages/subagent/subagent-codex/README.i18n.yaml b/packages/subagent/subagent-codex/README.i18n.yaml index 0f8ed31eab..fd0cccc02d 100644 --- a/packages/subagent/subagent-codex/README.i18n.yaml +++ b/packages/subagent/subagent-codex/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/subagent/subagent-codex/README.md -README.md: 975f353b9f1bc6fab61a4c0eb40ebaf50c436623 -README.zh.md: 2ea256afb3bb8fdfe57fd555b6db10522a709a56 +README.md: 5016d6b9aa4b57c9d1e83701a0ccfc4b04616f6d +README.zh.md: adabd942c362a28a4eeabe7a4d1b27b138aee4c6 diff --git a/packages/subagent/subagent-codex/README.md b/packages/subagent/subagent-codex/README.md index 975f353b9f..5016d6b9aa 100644 --- a/packages/subagent/subagent-codex/README.md +++ b/packages/subagent/subagent-codex/README.md @@ -18,7 +18,7 @@ Local cancellation wins the result race and maps to `aborted`. For failed turns, ## Capabilities and context -The provider advertises no optional start-time capabilities and reports `inheritsParentContext: false`. Codex receives the standalone text task and the parent Session cwd, but not the parent conversation, persona, tool filter, depth policy, or structured-output contract. The ephemeral Codex thread id and turn id stay private to this run and are never persisted in the parent Session. +The provider advertises no optional start-time capabilities and reports `inheritsParentContext: false`. The shared service rejects `request.agentOptions` for this provider. Codex receives the standalone text task and the parent Session cwd, but not the parent conversation, persona, tool filter, depth policy, or structured-output contract. The ephemeral Codex thread id and turn id stay private to this run and are never persisted in the parent Session. ## Configuration @@ -139,5 +139,5 @@ Append-only: foreground adds one result after the reusable parent prefix, while - **Compatibility is pinned by development evidence** — upgrading from the verified 0.147.0 protocol baseline requires regenerating upstream schema evidence and rerunning handshake, answer-selection, approval, cancellation, keyless real-product, and credentialed DeepSeek nonce tests. - **No human approval path** — known unattended approval requests are denied and unknown server requests fail closed; the three Profile modes never create a DSH interaction channel or per-call allow policy. - **Assistant payload is final text only** — a failed run may additionally expose the separate safe diagnostic; reasoning, commentary, intermediate messages, tool traffic, usage, raw stderr, and workspace diffs remain outside the parent Session, while generic Job ids, notices, and status come from the shared job runtime. -- **No optional shared capabilities** — output schemas, child personas, tool filtering, and harness depth enforcement are rejected by the shared service for this provider. +- **No optional shared capabilities** — `agentOptions`, output schemas, child personas, tool filtering, and harness depth enforcement are rejected by the shared service for this provider. - **No wall-clock timeout or side-effect rollback** — the caller cancels long work, and files or external systems changed before cancellation are not restored. diff --git a/packages/subagent/subagent-codex/README.zh.md b/packages/subagent/subagent-codex/README.zh.md index 2ea256afb3..adabd942c3 100644 --- a/packages/subagent/subagent-codex/README.zh.md +++ b/packages/subagent/subagent-codex/README.zh.md @@ -18,7 +18,7 @@ ## 能力与上下文 -本提供方不声明任何可选的启动时能力,并报告 `inheritsParentContext: false`。Codex 会接收独立文本任务和父会话 cwd,但不会接收父会话的对话、角色设定、工具筛选器、深度策略或结构化输出约定。临时 Codex 线程 ID 与轮次 ID 仅在此次运行内部可见,绝不会持久化到父会话。 +本提供方不声明任何可选的启动时能力,并报告 `inheritsParentContext: false`。共享服务会拒绝本提供方的 `request.agentOptions`。Codex 会接收独立文本任务和父会话 cwd,但不会接收父会话的对话、角色设定、工具筛选器、深度策略或结构化输出约定。临时 Codex 线程 ID 与轮次 ID 仅在此次运行内部可见,绝不会持久化到父会话。 ## 配置 @@ -139,5 +139,5 @@ Codex 子级会在一个全新的临时线程中,以单个轮次接收这些 - **兼容性由开发证据锁定**:若要从已验证的 0.147.0 协议基线升级,必须重新生成上游 schema 证据,并重新运行握手、答案选择、审批、取消、无密钥真实产品以及带密钥的 DeepSeek 随机数测试。 - **没有人工审批路径**:已知的无人值守审批请求会被拒绝,未知服务器请求会以默认拒绝方式使运行失败;三种 Profile 模式都不会创建 DSH 交互通道或逐次调用 allow 策略。 - **assistant 载荷仅包含最终文本**:失败运行可以额外公开独立的安全诊断;推理、过程说明、中间消息、工具通信、用量信息、原始 stderr 和工作区差异不会进入父会话,通用 Job id、通知与状态来自共享作业运行时。 -- **没有可选的共享能力**:对于本提供方,共享服务会拒绝输出 schema、子任务角色设定、工具筛选和 harness 深度强制约束。 +- **没有可选的共享能力**:对于本提供方,共享服务会拒绝 `agentOptions`、输出 schema、子任务角色设定、工具筛选和 harness 深度强制约束。 - **没有按实际经过时间触发的超时或副作用回滚**:长时间运行的工作由调用方取消,且取消前已更改的文件或外部系统不会恢复原状。 diff --git a/packages/subagent/subagent-codex/tests/loader-composition.e2e.ts b/packages/subagent/subagent-codex/tests/loader-composition.e2e.ts index b55b819989..c411ae2dbf 100644 --- a/packages/subagent/subagent-codex/tests/loader-composition.e2e.ts +++ b/packages/subagent/subagent-codex/tests/loader-composition.e2e.ts @@ -45,6 +45,7 @@ describe('Codex provider public Loader composition', () => { { name: 'codex', capabilities: { + agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, @@ -55,6 +56,7 @@ describe('Codex provider public Loader composition', () => { { name: 'codex-primary', capabilities: { + agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, @@ -65,6 +67,7 @@ describe('Codex provider public Loader composition', () => { { name: 'codex-secondary', capabilities: { + agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, diff --git a/packages/subagent/subagent-dsh-sdk/README.i18n.yaml b/packages/subagent/subagent-dsh-sdk/README.i18n.yaml index 8946de4236..a05ec421e3 100644 --- a/packages/subagent/subagent-dsh-sdk/README.i18n.yaml +++ b/packages/subagent/subagent-dsh-sdk/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/subagent/subagent-dsh-sdk/README.md -README.md: 10f437f1618d006f58ad37701e6fde7944f5c1fb -README.zh.md: d3237d9a0b36ce63c9eacf85bfe7cff947acd9e0 +README.md: 302baa05afed2b78c2041f57b1ef900713e350cd +README.zh.md: e4e1460274170b0c990d9e454f1cbf54546676e9 diff --git a/packages/subagent/subagent-dsh-sdk/README.md b/packages/subagent/subagent-dsh-sdk/README.md index 10f437f161..302baa05af 100644 --- a/packages/subagent/subagent-dsh-sdk/README.md +++ b/packages/subagent/subagent-dsh-sdk/README.md @@ -20,7 +20,7 @@ The SDK client returns an owned child activity rather than a prompt result. The ## Capabilities and context -The provider advertises no start-time capabilities (`outputSchema`/`depthLimit`/`toolFilter`/`persona` all false) and `inheritsParentContext: false`: the child is a fresh runtime in another process, and the only parent-derived input is the workspace cwd. `dsh-tool-subagent` deployments over this provider set `maxDepth: 'provider-managed'` — the child harness owns its own recursion budget. +The provider advertises no start-time capabilities (`agentOptions`/`outputSchema`/`depthLimit`/`toolFilter`/`persona` all false) and `inheritsParentContext: false`: the child is a fresh runtime in another process, and the only parent-derived input is the workspace cwd. `dsh-tool-subagent` deployments over this provider set `maxDepth: 'provider-managed'` — the child harness owns its own recursion budget. ## Configuration @@ -68,7 +68,7 @@ The package has no default export. Cordis loader unwrapping would otherwise hide #### What the model sees -The child runtime's model receives the standalone task as its user message plus that runtime's own configured system prompt, tools, and fresh session. It receives no parent conversation. This provider advertises no optional start-time capabilities, so the local service rejects requests for persona, tool filtering, depth enforcement, or structured output instead of silently omitting them. +The child runtime's model receives the standalone task as its user message plus that runtime's own configured system prompt, tools, and fresh session. It receives no parent conversation. This provider advertises no optional start-time capabilities, so the local service rejects requests for `agentOptions`, persona, tool filtering, depth enforcement, or structured output instead of silently omitting them. #### Token effect @@ -95,6 +95,6 @@ Append-only; newly visible content follows the reusable request prefix and does ## Known Limitations and Deferred Work - **A fresh runtime process per run** — no pooling; a harness runtime boots a full plugin tree, so per-run spawn cost is higher than the ACP backend's typical child. -- **No optional start-time capabilities** — the parent cannot enforce `outputSchema`, depth, tool filters, or persona inside the child process; configure the selected child profile and its ordered patches instead. +- **No optional start-time capabilities** — the parent cannot apply `agentOptions` or enforce `outputSchema`, depth, tool filters, or persona inside the child process; configure the selected child profile and its ordered patches instead. - **The child's transcript stays in the child's own session root** — the parent log records only the delegation tool call/result (the seam's child-isolation rule); the streamed `session.event` channel is consumed for output extraction, not bridged into the parent log. - **Local child processes only** — the resolved cwd is a local path; a remote runtime would need its own backend. diff --git a/packages/subagent/subagent-dsh-sdk/README.zh.md b/packages/subagent/subagent-dsh-sdk/README.zh.md index d3237d9a0b..e4e1460274 100644 --- a/packages/subagent/subagent-dsh-sdk/README.zh.md +++ b/packages/subagent/subagent-dsh-sdk/README.zh.md @@ -20,7 +20,7 @@ SDK 客户端返回自有子活动,而不是提示词结果。提供方读取 ## 能力与上下文 -Provider 不宣告任何启动期能力(`outputSchema`/`depthLimit`/`toolFilter`/`persona` 全为 false),且 `inheritsParentContext: false`:子进程是另一进程里的全新运行时,唯一来自父方的输入是工作区 cwd。基于本 provider 的 `dsh-tool-subagent` 部署应设置 `maxDepth: 'provider-managed'`——子 harness 拥有自己的递归预算。 +Provider 不宣告任何启动期能力(`agentOptions`/`outputSchema`/`depthLimit`/`toolFilter`/`persona` 全为 false),且 `inheritsParentContext: false`:子进程是另一进程里的全新运行时,唯一来自父方的输入是工作区 cwd。基于本 provider 的 `dsh-tool-subagent` 部署应设置 `maxDepth: 'provider-managed'`——子 harness 拥有自己的递归预算。 ## 配置 @@ -68,7 +68,7 @@ Provider 不宣告任何启动期能力(`outputSchema`/`depthLimit`/`toolFilte #### 模型看到的内容 -子运行时的模型会收到作为用户消息的独立任务,以及该运行时自身配置的系统提示词、工具和全新会话。它不会收到父级对话。本提供方不声明可选的启动时能力,因此本地服务会拒绝要求 persona、工具过滤、深度强制或结构化输出的请求,而不是静默省略这些要求。 +子运行时的模型会收到作为用户消息的独立任务,以及该运行时自身配置的系统提示词、工具和全新会话。它不会收到父级对话。本提供方不声明可选的启动时能力,因此本地服务会拒绝要求 `agentOptions`、persona、工具过滤、深度强制或结构化输出的请求,而不是静默省略这些要求。 #### Token 影响 @@ -95,6 +95,6 @@ Provider 不宣告任何启动期能力(`outputSchema`/`depthLimit`/`toolFilte ## 已知限制与暂缓事项 - **每次运行都使用全新的运行时进程**:不使用进程池;harness 运行时需要启动完整的插件树,因此每次运行的 spawn 成本高于 ACP 后端通常使用的子进程。 -- **不支持可选的启动时能力**:父级无法在子进程内强制执行 `outputSchema`、深度限制、工具过滤或 persona;应改为配置所选子 profile 及其有序 patch。 +- **不支持可选的启动时能力**:父级无法在子进程内应用 `agentOptions`,也无法强制执行 `outputSchema`、深度限制、工具过滤或 persona;应改为配置所选子 profile 及其有序 patch。 - **子进程的 transcript(文本记录)保留在其自身的会话根目录中**:父级日志只记录委派工具调用/结果(seam 的子级隔离规则);流式 `session.event` 通道只用于提取输出,不会桥接到父级日志中。 - **仅支持本地子进程**:解析出的 cwd 是本地路径;远程运行时需要独立的后端。 diff --git a/packages/subagent/subagent-dsh-sdk/src/index.ts b/packages/subagent/subagent-dsh-sdk/src/index.ts index 7530e104d2..277add74f8 100644 --- a/packages/subagent/subagent-dsh-sdk/src/index.ts +++ b/packages/subagent/subagent-dsh-sdk/src/index.ts @@ -105,7 +105,7 @@ function resolveConfiguredFile(field: string, value: string): string { /** * The SDK provider. Advertises NO start-time capabilities: an out-of-process - * child cannot honor `outputSchema`/`maxDepth`/`toolFilter`/`persona` (the + * child cannot honor `agentOptions`/`outputSchema`/`maxDepth`/`toolFilter`/`persona` (the * service rejects a request needing any of them before `start` runs). */ class SdkSubagentProvider implements SubagentProvider { diff --git a/packages/subagent/subagent-dsh-sdk/tests/subagent-dsh-sdk.spec.ts b/packages/subagent/subagent-dsh-sdk/tests/subagent-dsh-sdk.spec.ts index 2e67c76c24..27511670cf 100644 --- a/packages/subagent/subagent-dsh-sdk/tests/subagent-dsh-sdk.spec.ts +++ b/packages/subagent/subagent-dsh-sdk/tests/subagent-dsh-sdk.spec.ts @@ -433,6 +433,7 @@ describe('dsh-subagent-dsh-sdk provider', () => { expect(ctx.subagents.getProvider('sdk-hmr')?.name).toBe('sdk-hmr') expect(ctx.subagents.getProvider('sdk-hmr')?.inheritsParentContext).toBe(false) expect(ctx.subagents.getProvider('sdk-hmr')?.capabilities).toEqual({ + agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, diff --git a/packages/subagent/subagent-fork-in-process/README.i18n.yaml b/packages/subagent/subagent-fork-in-process/README.i18n.yaml index 9cd2361d52..19af9e598a 100644 --- a/packages/subagent/subagent-fork-in-process/README.i18n.yaml +++ b/packages/subagent/subagent-fork-in-process/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/subagent/subagent-fork-in-process/README.md -README.md: 74c27ff10c76aa711ed3e954e806c00a27aacfa5 -README.zh.md: 43e7ef489b33d52b674420d08f7edf8c89fb0e42 +README.md: c2dcda39c03b8c059839bef1573436e485a51911 +README.zh.md: 3eb84053a3e74abe7394c0985dcd88bd6146e449 diff --git a/packages/subagent/subagent-fork-in-process/README.md b/packages/subagent/subagent-fork-in-process/README.md index 74c27ff10c..c2dcda39c0 100644 --- a/packages/subagent/subagent-fork-in-process/README.md +++ b/packages/subagent/subagent-fork-in-process/README.md @@ -16,7 +16,7 @@ The seed transfers conversation history only. The child still receives a fresh f `start(request)` passes the completed-turn seed to [`startInProcessRun`](../subagent-in-process-driver/README.md) and awaits child publication. The shared driver owns cancellation, depth, customization, result reading, and disposal. -Fork advertises `{ outputSchema: true, depthLimit: true, toolFilter: true, persona: true }`, identical to spawn. +Fork advertises `{ agentOptions: true, outputSchema: true, depthLimit: true, toolFilter: true, persona: true }`, identical to spawn. ## Config @@ -39,7 +39,7 @@ Forking duplicates retained completed history into separate child requests; the #### KV Cache effect -The child may reuse the inherited byte-identical prefix under the same provider and model. Persona, tool-filter, generated-SDK, or route changes may invalidate reuse before inherited history; later child history is append-only. Shipped compositions therefore bind this provider to `backgroundMode: one-shot`, because a continuable child additionally carries the child-scoped `report` tool and its prompt section — deltas that precede the inherited history and so invalidate all of it ([the fork-one-shot Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md)). +The child may reuse the inherited byte-identical prefix under the same provider and model. Persona, tool-filter, generated-SDK, or route changes may invalidate reuse before inherited history; later child history is append-only. The base bundle and ACP/headless examples bind this provider to `backgroundMode: one-shot`, because a continuable child additionally carries the child-scoped `report` tool and its prompt section — deltas that precede the inherited history and so invalidate all of it. The CLI presets retain `continuable` fork and therefore accept that prefix loss ([the cache-preserving fork Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md)). ### Parent tool result, indirectly @@ -58,4 +58,5 @@ Append-only; newly visible content follows the reusable request prefix and does ## Known Limitations and Deferred Work - **The seed is a one-time snapshot** — the child sees the parent's completed turns as of the fork and nothing the parent logs afterwards; there is no live context sharing. -- **No shipped composition creates a continuable fork child** — `prepareContinuable` remains implemented and the seam accepts it, but every shipped `cordis.yml` sets `backgroundMode: one-shot` on the fork delegation tool, so the provider's continuable path has no production caller. Reopening it requires the child's system prompt and tool schemas to match the parent's byte for byte, which the [`report` return channel](../tool-subagent-report/README.md) currently prevents. Rationale and the reintroduction condition: [the fork-one-shot Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md). +- **Fork lifecycle policy differs by composition** — the base bundle and ACP/headless examples use one-shot fork to preserve prefix reuse, while the CLI presets use continuable fork and accept the child-scoped [`report` return channel](../tool-subagent-report/README.md) invalidating that prefix. Making continuable fork cache-preserving requires the child system prompt and tool schemas to match the parent's byte for byte. Rationale and the reintroduction condition: [the cache-preserving fork Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md). +- **Shipped fork tools do not expose child LLM route selection** — they inherit the parent's provider and model so the copied history remains eligible for KV Cache reuse. Route selection stays disabled until a change can preserve reuse or expose a bounded recomputation cost; the [model-selected route Agent Note](../../../.agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.md) owns that separate restriction. diff --git a/packages/subagent/subagent-fork-in-process/README.zh.md b/packages/subagent/subagent-fork-in-process/README.zh.md index 43e7ef489b..3eb84053a3 100644 --- a/packages/subagent/subagent-fork-in-process/README.zh.md +++ b/packages/subagent/subagent-fork-in-process/README.zh.md @@ -16,7 +16,7 @@ subagent 启动时,父 agent 当前的工具调用轮次仍未结束:其日 `start(request)` 将已完成轮次的初始内容传给 [`startInProcessRun`](../subagent-in-process-driver/README.zh.md),并等待子 agent 发布。共享驱动器负责取消、深度、定制、结果读取和 dispose(资源释放)。 -fork 声明 `{ outputSchema: true, depthLimit: true, toolFilter: true, persona: true }`,与 spawn 相同。 +fork 声明 `{ agentOptions: true, outputSchema: true, depthLimit: true, toolFilter: true, persona: true }`,与 spawn 相同。 ## 配置 @@ -39,7 +39,7 @@ fork 会把保留的已完成历史复制到独立的子 agent 请求中;随 #### KV Cache 影响 -在提供方和模型相同的前提下,子 agent 可以复用继承的逐字节相同前缀。persona、工具过滤、生成 SDK 或路由变化可能在继承历史之前使复用失效;后续子 agent 历史仅追加。因此随附组合把本提供方绑定为 `backgroundMode: one-shot`:可继续子 agent 还会额外携带作用域局部的 `report` 工具及其提示词 section,而这些增量位于继承历史之前,会使继承历史整体失效(见 [fork 保持 one-shot 的 Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md))。 +在提供方和模型相同的前提下,子 agent 可以复用继承的逐字节相同前缀。persona、工具过滤、生成 SDK 或路由变化可能在继承历史之前使复用失效;后续子 agent 历史仅追加。base 组合包与 ACP/headless 示例把本提供方绑定为 `backgroundMode: one-shot`:可继续子 agent 还会额外携带作用域局部的 `report` 工具及其提示词 section,而这些增量位于继承历史之前,会使继承历史整体失效。CLI preset 保留可继续 fork,因此接受这项前缀损失(见[保留缓存的 fork Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md))。 ### 父 agent 工具结果(间接) @@ -58,4 +58,5 @@ fork 会把保留的已完成历史复制到独立的子 agent 请求中;随 ## 已知限制与暂缓事项 - **初始内容是一次性快照**:子 agent 只能看到 fork 时父 agent 已完成的轮次,看不到父 agent 此后记录的任何内容;不会实时共享上下文。 -- **没有任何随附组合会创建可继续的 fork 子 agent**:`prepareContinuable` 仍然实现完好,seam 也接受它,但每份随附的 `cordis.yml` 都在 fork 委派工具上设置 `backgroundMode: one-shot`,因此该提供方的可继续路径没有生产调用方。重新开放它需要子 agent 的系统提示词与工具 schema 与父 agent 逐字节一致,而这一点目前被 [`report` 返回通道](../tool-subagent-report/README.zh.md)阻止。理由与重新开放条件见 [fork 保持 one-shot 的 Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md)。 +- **fork 生命周期策略因组合而异**:base 组合包与 ACP/headless 示例使用一次性 fork 以保留前缀复用,CLI preset 则使用可继续 fork,并接受子级作用域的 [`report` 返回通道](../tool-subagent-report/README.zh.md)使该前缀失效。要让可继续 fork 保留缓存,子 agent 的系统提示词与工具 schema 必须与父级逐字节一致。理由与重新开放条件见[保留缓存的 fork Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md)。 +- **随附 fork 工具不公开子级 LLM 路由选择**:它们会继承父级的提供方与模型,使复制的历史仍可供 KV Cache 复用。只有在路由变化仍能保留复用,或接口能公开一项有界的重算成本时,才启用路由选择;该独立限制由[模型选择路由 Agent Note](../../../.agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.zh.md)负责。 diff --git a/packages/subagent/subagent-fork-in-process/src/index.ts b/packages/subagent/subagent-fork-in-process/src/index.ts index 1f8e48b8c8..9786f585fa 100644 --- a/packages/subagent/subagent-fork-in-process/src/index.ts +++ b/packages/subagent/subagent-fork-in-process/src/index.ts @@ -55,11 +55,18 @@ function completedTurnPrefix(parent: Agent): SessionEvent[] { /** * The fork provider. Supports `depthLimit` and `outputSchema` (via the shared - * in-process structured runtime), plus `toolFilter`/`persona` (scoped - * restrict() and a scoped shadowing persona section). + * in-process structured runtime), `agentOptions` (merged over the parent + * route), and `toolFilter`/`persona` (scoped restrict() and a scoped shadowing + * persona section). */ class ForkInProcessProvider implements SubagentProvider { - readonly capabilities: SubagentCapabilities = { outputSchema: true, depthLimit: true, toolFilter: true, persona: true } + readonly capabilities: SubagentCapabilities = { + agentOptions: true, + outputSchema: true, + depthLimit: true, + toolFilter: true, + persona: true, + } // Context contract: a forked child IS seeded with the parent's completed-turn prefix. readonly inheritsParentContext = true @@ -74,11 +81,11 @@ class ForkInProcessProvider implements SubagentProvider { }) } - // TODO(fork-continuable-prefix-reuse): no shipped composition calls this — - // they bind fork to `backgroundMode: one-shot` because a continuable child's - // `report` tool and prompt section precede the inherited history, defeating - // the prefix reuse a fork exists for. Reopening needs a byte-identical child - // system prompt and tool schemas; see issue #2124 and + // TODO(fork-continuable-prefix-reuse): CLI presets call this and accept that + // a continuable child's `report` tool and prompt section precede the inherited + // history, defeating the prefix reuse a fork exists for. Cache-preserving + // continuable fork needs byte-identical child system prompt and tool schemas; + // see issue #2124 and // .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md. prepareContinuable(request: ContinuableCreateRequest): Promise { // The fork prefix is captured ONCE, at creation: it becomes part of the diff --git a/packages/subagent/subagent-fork-in-process/tests/subagent-fork-in-process.spec.ts b/packages/subagent/subagent-fork-in-process/tests/subagent-fork-in-process.spec.ts index 8b69711233..292fba32cc 100644 --- a/packages/subagent/subagent-fork-in-process/tests/subagent-fork-in-process.spec.ts +++ b/packages/subagent/subagent-fork-in-process/tests/subagent-fork-in-process.spec.ts @@ -193,9 +193,15 @@ describe('dsh-subagent-fork-in-process', () => { await run.dispose() }) - it('advertises every start-time capability (depthLimit, outputSchema, toolFilter, persona)', async () => { + it('advertises every start-time capability', async () => { const { ctx } = await setup([]) - expect(ctx.subagents.getProvider('fork')!.capabilities).toEqual({ outputSchema: true, depthLimit: true, toolFilter: true, persona: true }) + expect(ctx.subagents.getProvider('fork')!.capabilities).toEqual({ + agentOptions: true, + outputSchema: true, + depthLimit: true, + toolFilter: true, + persona: true, + }) }) it('unregisters the provider when its fiber is disposed (HMR safety)', async () => { diff --git a/packages/subagent/subagent-in-process-driver/README.i18n.yaml b/packages/subagent/subagent-in-process-driver/README.i18n.yaml index 505c480cfe..16d07d8211 100644 --- a/packages/subagent/subagent-in-process-driver/README.i18n.yaml +++ b/packages/subagent/subagent-in-process-driver/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/subagent/subagent-in-process-driver/README.md -README.md: 47a5c09fc1c80c5dc3062be82e7355b874a627d3 -README.zh.md: b96399795a0fbac05ef1795888aa93c620687f30 +README.md: ed2568fcff3fe1f0f3968d1cef43ebd914a8911b +README.zh.md: f9958e5c2b819d51bfdf8fc1e14d1f8c7c19be91 diff --git a/packages/subagent/subagent-in-process-driver/README.md b/packages/subagent/subagent-in-process-driver/README.md index 47a5c09fc1..ed2568fcff 100644 --- a/packages/subagent/subagent-in-process-driver/README.md +++ b/packages/subagent/subagent-in-process-driver/README.md @@ -16,7 +16,7 @@ The driver follows this sequence: 4. Publish the child, retain the returned `AgentHandle`, and drive one task with `child.followup(prompt)` followed by `child.whenIdle()`. 5. Read the child's own output — its last non-empty assistant message (an empty-content message that records usage is skipped), or its accumulated assistant text when no such message exists — and the final durable turn reason from the complete owned child run, excluding any fork seed. -The child gets the parent's working-directory/session lineage and inherits the parent provider, model, and output-token cap unless `request.agentOptions` overrides them. It gets a fresh flat registration scope: parent ownership does not import parent tool restrictions or establish an authority subset. +The child gets the parent's working-directory/session lineage and inherits the parent provider, model, reasoning effort, and output-token cap unless `request.agentOptions` overrides them. It gets a fresh flat registration scope: parent ownership does not import parent tool restrictions or establish an authority subset. This result boundary is valid because the provider owns an isolated child lifecycle from publication through quiescence. Steering submitted during that lifecycle belongs to the child run; the provider does not pretend the initial follow-up alone owns its output. diff --git a/packages/subagent/subagent-in-process-driver/README.zh.md b/packages/subagent/subagent-in-process-driver/README.zh.md index b96399795a..f9958e5c2b 100644 --- a/packages/subagent/subagent-in-process-driver/README.zh.md +++ b/packages/subagent/subagent-in-process-driver/README.zh.md @@ -16,7 +16,7 @@ 4. 发布子 agent,保留返回的 `AgentHandle`,并通过先调用 `child.followup(prompt)`、再调用 `child.whenIdle()` 来驱动一项任务。 5. 从完整的自有子运行中读取子 agent 自身的输出——最后一条非空 assistant 消息(记录 usage 的空内容消息会被跳过),若没有这类消息则取其累积的 assistant 文本——以及最终持久化的轮次原因,并排除任何 fork 初始内容。 -子 agent 会获得父 agent 的工作目录/会话谱系;除非 `request.agentOptions` 覆盖,否则还会继承父 agent 的提供方、模型和输出 token 上限。它获得全新的扁平注册作用域:父级所有权不会导入父 agent 的工具限制,也不会建立权限子集。 +子 agent 会获得父 agent 的工作目录/会话谱系;除非 `request.agentOptions` 覆盖,否则还会继承父 agent 的提供方、模型、推理强度与输出 token 上限。它获得全新的扁平注册作用域:父级所有权不会导入父 agent 的工具限制,也不会建立权限子集。 该结果边界成立,是因为提供方拥有从发布到完全停稳的隔离子 agent 生命周期。在该生命周期内提交的 steering(中途引导)属于子运行;提供方不会声称输出只归初始 follow-up 所有。 diff --git a/packages/subagent/subagent-in-process-driver/tests/structured.spec.ts b/packages/subagent/subagent-in-process-driver/tests/structured.spec.ts index 8840e8b101..6bb18f5f28 100644 --- a/packages/subagent/subagent-in-process-driver/tests/structured.spec.ts +++ b/packages/subagent/subagent-in-process-driver/tests/structured.spec.ts @@ -70,7 +70,7 @@ async function setup(script: Script, options: SetupOptions = {}) { await ctx.plugin(SubagentRuntime) const disposeProvider = ctx.subagents.registerProvider({ name: 'spawn', - capabilities: { outputSchema: true, depthLimit: true, toolFilter: false, persona: false }, + capabilities: { agentOptions: true, outputSchema: true, depthLimit: true, toolFilter: false, persona: false }, inheritsParentContext: false, start: (request: ResolvedSubagentStartRequest) => startInProcessRun(request, {}), }) diff --git a/packages/subagent/subagent-spawn-in-process/README.i18n.yaml b/packages/subagent/subagent-spawn-in-process/README.i18n.yaml index 1246456cc0..022dbfa58d 100644 --- a/packages/subagent/subagent-spawn-in-process/README.i18n.yaml +++ b/packages/subagent/subagent-spawn-in-process/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/subagent/subagent-spawn-in-process/README.md -README.md: f1fb96f2230359cb3ff55c630f29fd34345dbed7 -README.zh.md: 95a3b5cdb7084eb75666f8d62001221c57ac676c +README.md: ebe2b069dc56dc1a3359f8880860a5796ef3ef4c +README.zh.md: 66ecbec2c00865d16a99f1e6bf4f0c32cb6e5538 diff --git a/packages/subagent/subagent-spawn-in-process/README.md b/packages/subagent/subagent-spawn-in-process/README.md index f1fb96f223..ebe2b069dc 100644 --- a/packages/subagent/subagent-spawn-in-process/README.md +++ b/packages/subagent/subagent-spawn-in-process/README.md @@ -6,13 +6,13 @@ The spawn provider creates a fresh child `Agent` in the current process. The chi ## Behavior -`start(request)` delegates to [`startInProcessRun`](../subagent-in-process-driver/README.md) with no seed and awaits publication before returning. The child receives parent working-directory/session lineage and inherits the parent model unless overridden, but starts with an empty conversation. +`start(request)` delegates to [`startInProcessRun`](../subagent-in-process-driver/README.md) with no seed and awaits publication before returning. The child receives parent working-directory/session lineage and inherits the parent provider, model, reasoning effort, and output-token limit unless `request.agentOptions` overrides them, but starts with an empty conversation. The shared driver owns depth checking, persona and tool-filter setup, structured output, required-signal cancellation, one-shot execution, result reading, and quiescent disposal. A startup rejection leaves no published child; provider unload after fulfillment does not revoke the holder-owned run. ## Capabilities -Spawn advertises `{ outputSchema: true, depthLimit: true, toolFilter: true, persona: true }` because it controls the child's creation window and can enforce all four features. +Spawn advertises `{ agentOptions: true, outputSchema: true, depthLimit: true, toolFilter: true, persona: true }` because it controls the child's creation window and can enforce all five features. ## Config @@ -26,7 +26,7 @@ Spawn advertises `{ outputSchema: true, depthLimit: true, toolFilter: true, pers #### What the model sees -The fresh child receives the standalone task content verbatim, inherits the parent model and workspace by default, and sees the global prompt with any configured child-scoped persona shadow. A tool filter removes global wire schemas, executable lookup, and Code Mode SDK bindings for that child but leaves independently registered guidance. It receives zero parent conversation messages; the filter is visibility/composition, not an authority grant inherited from the parent. +The fresh child receives the standalone task content verbatim, inherits the parent provider, model, reasoning effort, output-token limit, and workspace by default, and sees the global prompt with any configured child-scoped persona shadow. A tool filter removes global wire schemas, executable lookup, and Code Mode SDK bindings for that child but leaves independently registered guidance. It receives zero parent conversation messages; the filter is visibility/composition, not an authority grant inherited from the parent. #### Token effect @@ -52,4 +52,4 @@ Append-only; newly visible content follows the reusable request prefix and does ## Known Limitations and Deferred Work -- **Fresh means no parent transcript** — the child inherits cwd, lineage, model, and explicitly configured persona/tool restrictions, but none of the parent's conversation; use the fork provider when completed-turn context is required. +- **Fresh means no parent transcript** — the child inherits cwd, lineage, provider, model, reasoning effort, output-token limit, and explicitly configured persona/tool restrictions, but none of the parent's conversation; use the fork provider when completed-turn context is required. diff --git a/packages/subagent/subagent-spawn-in-process/README.zh.md b/packages/subagent/subagent-spawn-in-process/README.zh.md index 95a3b5cdb7..66ecbec2c0 100644 --- a/packages/subagent/subagent-spawn-in-process/README.zh.md +++ b/packages/subagent/subagent-spawn-in-process/README.zh.md @@ -6,13 +6,13 @@ spawn 提供方会在当前进程中创建一个全新的子 `Agent`。子 agent ## 行为 -`start(request)` 不传入 seed,直接委托给 [`startInProcessRun`](../subagent-in-process-driver/README.zh.md),并在子 agent 发布后才返回。子 agent 获得父 agent 的工作目录/会话谱系,并默认继承父 agent 模型(除非覆盖),但以空对话开始运行。 +`start(request)` 不传入 seed,直接委托给 [`startInProcessRun`](../subagent-in-process-driver/README.zh.md),并在子 agent 发布后才返回。子 agent 获得父 agent 的工作目录/会话谱系;除非 `request.agentOptions` 覆盖,否则还会继承父 agent 的提供方、模型、推理强度与输出 token 上限,但以空对话开始运行。 共享驱动器负责深度检查、persona 与工具过滤器设置、结构化输出、通过必需的信号执行取消、单次执行、结果读取和完全停稳后的 dispose(资源释放)。启动遭拒不会留下已发布的子 agent;启动调用兑现后卸载提供方,也不会撤销由持有方拥有的运行。 ## 能力 -spawn 声明 `{ outputSchema: true, depthLimit: true, toolFilter: true, persona: true }`,因为它控制子 agent 的创建窗口,能够强制执行全部四项功能。 +spawn 声明 `{ agentOptions: true, outputSchema: true, depthLimit: true, toolFilter: true, persona: true }`,因为它控制子 agent 的创建窗口,能够强制执行全部五项功能。 ## 配置 @@ -26,7 +26,7 @@ spawn 声明 `{ outputSchema: true, depthLimit: true, toolFilter: true, persona: #### 模型看到的内容 -全新的子 agent 逐字接收独立任务内容,默认继承父 agent 的模型和工作区,并看到带有已配置子 agent 作用域 persona 遮蔽的全局提示词。工具过滤器会为该子 agent 移除全局协议 schema、可执行工具查找和 Code Mode SDK 绑定,但保留独立注册的指导内容。它不接收任何父 agent 对话消息;过滤控制的是可见性与组合,并非从父 agent 继承的权限授予。 +全新的子 agent 逐字接收独立任务内容,默认继承父 agent 的提供方、模型、推理强度、输出 token 上限与工作区,并看到带有已配置子 agent 作用域 persona 遮蔽的全局提示词。工具过滤器会为该子 agent 移除全局协议 schema、可执行工具查找和 Code Mode SDK 绑定,但保留独立注册的指导内容。它不接收任何父 agent 对话消息;过滤控制的是可见性与组合,并非从父 agent 继承的权限授予。 #### Token 影响 @@ -52,4 +52,4 @@ spawn 声明 `{ outputSchema: true, depthLimit: true, toolFilter: true, persona: ## 已知限制与暂缓事项 -- **全新表示不含父 agent transcript(文本记录)**:子 agent 会继承 cwd、谱系、模型及显式配置的 persona/工具限制,但不继承父 agent 的任何对话;需要已完成轮次上下文时,请使用 fork 提供方。 +- **全新表示不含父 agent transcript(文本记录)**:子 agent 会继承 cwd、谱系、提供方、模型、推理强度、输出 token 上限及显式配置的 persona/工具限制,但不继承父 agent 的任何对话;需要已完成轮次上下文时,请使用 fork 提供方。 diff --git a/packages/subagent/subagent-spawn-in-process/src/index.ts b/packages/subagent/subagent-spawn-in-process/src/index.ts index dcd036e4ad..73811155c1 100644 --- a/packages/subagent/subagent-spawn-in-process/src/index.ts +++ b/packages/subagent/subagent-spawn-in-process/src/index.ts @@ -34,12 +34,18 @@ export const Config: z = z.object({ /** * The spawn provider. Supports every start-time capability: `depthLimit` (it * constructs the child, so it can enforce a recursion cap), `outputSchema` - * (the scoped structured runtime), and `toolFilter`/`persona` (scoped - * `restrict()` and a scoped shadowing persona section, applied in the child's - * creation window). + * (the scoped structured runtime), `agentOptions` (merged over the parent + * route), and `toolFilter`/`persona` (scoped `restrict()` and a scoped + * shadowing persona section, applied in the child's creation window). */ class SpawnInProcessProvider implements SubagentProvider { - readonly capabilities: SubagentCapabilities = { outputSchema: true, depthLimit: true, toolFilter: true, persona: true } + readonly capabilities: SubagentCapabilities = { + agentOptions: true, + outputSchema: true, + depthLimit: true, + toolFilter: true, + persona: true, + } // Context contract: a spawned child starts fresh — it never sees the parent conversation. readonly inheritsParentContext = false diff --git a/packages/subagent/subagent-spawn-in-process/tests/subagent-spawn-in-process.spec.ts b/packages/subagent/subagent-spawn-in-process/tests/subagent-spawn-in-process.spec.ts index ae60480c02..7a798a4818 100644 --- a/packages/subagent/subagent-spawn-in-process/tests/subagent-spawn-in-process.spec.ts +++ b/packages/subagent/subagent-spawn-in-process/tests/subagent-spawn-in-process.spec.ts @@ -282,10 +282,16 @@ describe('dsh-subagent-spawn-in-process', () => { await parentHandle.dispose() }) - it('advertises every start-time capability (depthLimit, outputSchema, toolFilter, persona)', async () => { + it('advertises every start-time capability', async () => { const { ctx } = await setup([]) const provider = ctx.subagents.getProvider('spawn')! - expect(provider.capabilities).toEqual({ outputSchema: true, depthLimit: true, toolFilter: true, persona: true }) + expect(provider.capabilities).toEqual({ + agentOptions: true, + outputSchema: true, + depthLimit: true, + toolFilter: true, + persona: true, + }) }) it('unregisters the provider when its fiber is disposed (HMR safety)', async () => { diff --git a/packages/subagent/subagent/README.i18n.yaml b/packages/subagent/subagent/README.i18n.yaml index a645351bd8..a55a61ee77 100644 --- a/packages/subagent/subagent/README.i18n.yaml +++ b/packages/subagent/subagent/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/subagent/subagent/README.md -README.md: e84a6b486253e81ccf7e7df12c4149e6df4ed9f2 -README.zh.md: e289863531c1686cedeccadfa76e2661dfa9bfc8 +README.md: 68ddc49197bcbd3f8eb5f362de60da33cb08c147 +README.zh.md: cf434152cd6366e371eef86f0edcb08d18978c66 diff --git a/packages/subagent/subagent/README.md b/packages/subagent/subagent/README.md index e84a6b4862..68ddc49197 100644 --- a/packages/subagent/subagent/README.md +++ b/packages/subagent/subagent/README.md @@ -26,7 +26,7 @@ The [subagent family overview](../README.md) maps implementations and model-faci | `listChildren(parentSessionId, signal?)` | List direct session-backed subagents with their `one-shot`/`continuable` mode, `running`/`inactive` activity, origin-classified one-level `hasChildren` hint, and per-child diagnostics, ordered by `createdAt` then id, without loading or resuming them. Reads the live session store and optional session persistence directly (live-only enumeration when persistence is absent) and requires the mounted `sessionProjections` registry; it does not require `ctx.agents`, the continuation manager, or any query service. | | `listDescendants(rootSessionId, signal?)` | Flatten the root's complete session tree in stable pre-order from the same live-preferred corpus, adding each subagent entry's durable `parentId` and root-relative `depth`. Ordinary sessions and one-shot children remain traversal nodes so continuable descendants below them are discovered. Identity, diagnostics, dependencies, and cancellation follow `listChildren()`. | -`SubagentStartRequest.label` is an optional short durable display label for a session-backed one-shot child. Model-facing delegation supplies its existing `description`; lower-level callers need not invent presentation metadata. Continuable starts always carry their own required label. `signal` is required and is the canonical cancellation channel for a one-shot `start`. An abort before publication makes `start()` reject after rollback; an abort after publication cancels the returned run's remaining turn work without hiding its id. The request may also select a model, require structured output, cap delegation depth, restrict child tools, or set a child persona. For a continuable start or follow-up, the caller signal owns lookup, materialization, and admission only until inbox acceptance; afterward the manager owns the Activation independently, so later caller cancellation neither cancels the accepted turn nor disposes the child. +`SubagentStartRequest.label` is an optional short durable display label for a session-backed one-shot child. Model-facing delegation supplies its existing `description`; lower-level callers need not invent presentation metadata. Continuable starts always carry their own required label. `signal` is required and is the canonical cancellation channel for a one-shot `start`. An abort before publication makes `start()` reject after rollback; an abort after publication cancels the returned run's remaining turn work without hiding its id. The request may also override the host Agent's provider, model, reasoning effort, and token limit, require structured output, cap delegation depth, restrict child tools, or set a child persona. Every requested optional feature requires its matching provider capability. For a continuable start or follow-up, the caller signal owns lookup, materialization, and admission only until inbox acceptance; afterward the manager owns the Activation independently, so later caller cancellation neither cancels the accepted turn nor disposes the child. Follow-up authority comes from the exact live direct parent recorded in the child's durable header. Cold resume checks that authority before reconstruction and again in the final no-await inbox-admission span, so a parent unregistered or replaced during materialization cannot authorize delivery. The `source` on a follow-up records who supplied the delivered message and grants no authority. @@ -36,11 +36,14 @@ Same-process requests, descriptors, results, and event payloads are trusted type Start-time features are advertised in `provider.capabilities` because the service must reject an unsupported one-shot request before child creation: +- `agentOptions` — apply host-Agent provider, model, reasoning-effort, and output-token overrides. - `outputSchema` — enforce a structured final result. - `depthLimit` — enforce `maxDepth`. - `toolFilter` — apply the requested child tool restriction. - `persona` — apply a per-child persona. +Both in-process providers advertise `agentOptions`: child creation merges requested fields over the provider, model, and reasoning effort in the parent's latest logged request, falling back to its creation options before the first request and retaining its configured token limit. A route change without an explicit effort clears the inherited route-owned effort so the selected model resolves its default. Current out-of-process providers advertise it as unsupported, so configured or model-selected overrides fail before their child transport starts instead of being silently ignored. + Every in-process child is composed by one call, `applyChildComposition(childCtx, parent, composition)`, which joins the parent's agent-preset composition before applying the child's own persona and tool filter. The join is what gives the child its capabilities: with every model-facing row on the agent plane, a child that joined nothing would reach the model with an empty tool registry ([`dsh-agent-presets`](../../preset/agent-presets/README.md)). Taking the parent as a parameter is deliberate — it makes composing a child WITHOUT that join unrepresentable at the call sites, which is the defect the one call exists to prevent. A deployment composing no preset roster joins nothing and needs nothing: its model-facing rows sit in the host composition, where the child already resolves them through the tool registry's global layer. `childSessionMeta()` records the joined preset id on the child's durable header for the same reason a top-level session records its own: the preset decides the tool schemas and prompt sections the model saw, so a cold read of the child's history has to rebuild that composition rather than the deployment default. It is read from the parent's live scope chain, not from the parent header, because a parent that switched preset while blank runs on the newer composition while its header still names the older one. @@ -49,7 +52,7 @@ Continuable creation is the optional `SubagentProvider.prepareContinuable?()` me ## The durable descriptor -The Service Definition owns the versioned `subagent/descriptor` session event vocabulary (`src/descriptor.ts`): `snapshotSubagentDescriptor()` validates and detaches the record before provider work, and `foldSubagentDescriptor()` validates the complete current-version payload before recovering it from a loaded child log. Every local session-backed start appends one descriptor with the provider name and lifecycle `mode`. A `one-shot` descriptor optionally carries the caller-owned durable display `label`; a `continuable` descriptor requires its durable creation label and additionally records resolved child `agentOptions.provider`/`model` and optional `persona`/`toolFilter` for cold resume. These are explicit fields, never the merge-extensible `AgentOptions` object, so an unrelated extension value cannot break continuation. The descriptor omits `subagentDepth` (the persisted header's `delegationDepth` is the monotone floor) and `outputSchema` (an Activation's result contract). The event is log-only: no `surfaceOp`, absent from model history, and retained by the append-only log across compaction. Malformed current-version payloads are corrupt; unsupported versions cannot be classified by this runtime. +The Service Definition owns the versioned `subagent/descriptor` session event vocabulary (`src/descriptor.ts`): `snapshotSubagentDescriptor()` validates and detaches the record before provider work, and `foldSubagentDescriptor()` validates the complete current-version payload before recovering it from a loaded child log. Every local session-backed start appends one descriptor with the provider name and lifecycle `mode`. A `one-shot` descriptor optionally carries the caller-owned durable display `label`; a `continuable` descriptor requires its durable creation label and additionally records resolved child `agentOptions.provider`/`model`/`reasoningEffort` and optional `persona`/`toolFilter` for cold resume. These are explicit fields, never the merge-extensible `AgentOptions` object, so an unrelated extension value cannot break continuation. The descriptor omits `subagentDepth` (the persisted header's `delegationDepth` is the monotone floor) and `outputSchema` (an Activation's result contract). The event is log-only: no `surfaceOp`, absent from model history, and retained by the append-only log across compaction. Malformed current-version payloads are corrupt; unsupported versions cannot be classified by this runtime. ## Delegation depth diff --git a/packages/subagent/subagent/README.zh.md b/packages/subagent/subagent/README.zh.md index e289863531..cf434152cd 100644 --- a/packages/subagent/subagent/README.zh.md +++ b/packages/subagent/subagent/README.zh.md @@ -26,7 +26,7 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委 | `listChildren(parentSessionId, signal?)` | 按 `createdAt`、再按 id 的顺序列出由会话支撑的直接 subagent,包括其 `one-shot`/`continuable` 模式、`running`/`inactive` 活动状态、根据 origin 分类得出的一层 `hasChildren` 提示,以及每个子级的诊断信息,且不会加载或恢复它们。该操作直接读取在线会话存储和可选的会话持久化(没有持久化时只枚举在线子级),并要求已挂载 `sessionProjections` 注册表;不要求 `ctx.agents`、继续执行管理器或任何查询服务。 | | `listDescendants(rootSessionId, signal?)` | 从同一份在线优先语料按稳定 pre-order 展平根的完整会话树,并为每个 subagent 条目附加持久 `parentId` 与相对根的 `depth`。普通会话与一次性 child 仍作为遍历节点,因此其下的可继续后代仍可发现。身份、diagnostic、依赖与取消约定均沿用 `listChildren()`。 | -`SubagentStartRequest.label` 是由会话支撑的一次性 child 所使用的可选简短持久化显示标签。面向模型的委派会提供其已有的 `description`;底层调用方无需凭空构造展示元数据。可继续启动始终携带自身的必填标签。`signal` 是必填项,也是一次性 `start` 的规范取消通道。发布前中止会使 `start()` 在回滚后拒绝;发布后中止会取消已返回 run 的剩余轮次工作,但不会隐藏其 id。请求还可以选择模型、要求结构化输出、限制委派深度、约束子 agent 工具或设置子 agent persona。对于可继续启动或后续操作,调用方信号只负责 inbox 接受前的查找、物化和准入;此后,Activation 由管理器独立拥有,因此调用方取消既不会取消已接受的轮次,也不会 dispose(资源释放)子 agent。 +`SubagentStartRequest.label` 是由会话支撑的一次性 child 所使用的可选简短持久化显示标签。面向模型的委派会提供其已有的 `description`;底层调用方无需凭空构造展示元数据。可继续启动始终携带自身的必填标签。`signal` 是必填项,也是一次性 `start` 的规范取消通道。发布前中止会使 `start()` 在回滚后拒绝;发布后中止会取消已返回 run 的剩余轮次工作,但不会隐藏其 id。请求还可以覆盖宿主 Agent 的提供方、模型、推理强度与 token 上限、要求结构化输出、限制委派深度、约束子 agent 工具或设置子 agent persona。每个被请求的可选特性都要求匹配的提供方能力。对于可继续启动或后续操作,调用方信号只负责 inbox 接受前的查找、物化和准入;此后,Activation 由管理器独立拥有,因此调用方取消既不会取消已接受的轮次,也不会 dispose(资源释放)子 agent。 后续操作的权限来自子 agent 持久化 header 中记录的确切在线直接父级。冷恢复会在重建前检查该权限,并在最终无 await 的 inbox 准入区间再次检查,因此在物化期间被注销或替换的 parent 无法授权投递。后续操作上的 `source` 记录谁提供了所投递的消息,不授予任何权限。 @@ -36,11 +36,14 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委 启动时功能通过 `provider.capabilities` 声明,因为服务必须在创建子 agent 前拒绝不受支持的一次性请求: +- `agentOptions`:应用宿主 Agent 提供方、模型、推理强度与输出 token 上限覆盖; - `outputSchema`:强制执行结构化最终结果; - `depthLimit`:强制执行 `maxDepth`; - `toolFilter`:应用请求的子 agent 工具限制; - `persona`:应用每个子 agent 独立的 persona。 +两个进程内提供方都会声明 `agentOptions`:创建子 agent 时,请求字段会覆盖父级最新记录请求中的提供方、模型与推理强度;首个请求之前回退到其创建选项,并保留其中配置的 token 上限。更换路由但没有显式指定强度时,会清除继承的路由所属强度,使所选模型解析自己的默认值。当前进程外提供方会声明不支持,因此配置或模型选择的覆盖会在启动子传输前失败,而不会被静默忽略。 + 每个进程内子 agent 都通过一次 `applyChildComposition(childCtx, parent, composition)` 调用完成组装:先加入父级的 agent-preset 组合,再应用子 agent 自己的 persona 和工具限制。加入父级组合正是子 agent 获得能力的途径:所有面向模型的行都位于 agent 平面,完全没有加入任何组合的子 agent 抵达模型时会看到空的工具注册表(见 [`dsh-agent-presets`](../../preset/agent-presets/README.zh.md))。将父级作为参数是刻意设计:这让“组装子 agent 却不做该加入”在各调用点无法表达,而这正是这一次调用所要杜绝的缺陷。未组装 preset roster 的部署不加入任何组合、也不需要加入;其面向模型的行位于宿主组合中,子 agent 已能通过工具注册表的全局层解析到它们。 `childSessionMeta()` 把所加入的 preset id 记在子 agent 的持久化 header 上,理由与顶层会话记录自己的那一个相同:preset 决定了模型所见的工具 schema 与提示段,因此冷读子 agent 的历史时必须重建那份组装,而不是部署默认值。该值从父方**活着的** scope 链读取,而不是从父方 header 读取,因为在空白期切换过 preset 的父方运行在更新的那份组装上,而它的 header 仍写着旧的那个。 @@ -49,7 +52,7 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委 ## 持久化描述符 -该 Service Definition 拥有版本化的 `subagent/descriptor` 会话事件词汇(`src/descriptor.ts`):`snapshotSubagentDescriptor()` 会在提供方工作之前校验并分离记录,`foldSubagentDescriptor()` 则会在从已加载子 agent 日志中恢复描述符之前,校验当前版本的完整 payload。每次由本地会话支撑的启动都会追加一个带有提供方名称与生命周期 `mode` 的描述符。`one-shot` 描述符可以携带调用方拥有的可选持久化显示 `label`;`continuable` 描述符要求其持久化创建标签,并另外记录已解析的子 agent `agentOptions.provider`/`model`,以及用于从持久化存储恢复的可选 `persona`/`toolFilter`。这些是显式字段,绝不是可通过合并扩展的 `AgentOptions` 对象,因此无关的扩展值不会破坏继续执行。描述符省略 `subagentDepth`(持久化 header 的 `delegationDepth` 是单调下界)和 `outputSchema`(单次 Activation 的结果约定)。该事件只进入日志:不含 `surfaceOp`,不进入模型历史,并由仅追加日志跨压缩(compaction)保留。格式错误的当前版本 payload 属于损坏;本运行时无法对不受支持的版本进行分类。 +该 Service Definition 拥有版本化的 `subagent/descriptor` 会话事件词汇(`src/descriptor.ts`):`snapshotSubagentDescriptor()` 会在提供方工作之前校验并分离记录,`foldSubagentDescriptor()` 则会在从已加载子 agent 日志中恢复描述符之前,校验当前版本的完整 payload。每次由本地会话支撑的启动都会追加一个带有提供方名称与生命周期 `mode` 的描述符。`one-shot` 描述符可以携带调用方拥有的可选持久化显示 `label`;`continuable` 描述符要求其持久化创建标签,并另外记录已解析的子 agent `agentOptions.provider`/`model`/`reasoningEffort`,以及用于从持久化存储恢复的可选 `persona`/`toolFilter`。这些是显式字段,绝不是可通过合并扩展的 `AgentOptions` 对象,因此无关的扩展值不会破坏继续执行。描述符省略 `subagentDepth`(持久化 header 的 `delegationDepth` 是单调下界)和 `outputSchema`(单次 Activation 的结果约定)。该事件只进入日志:不含 `surfaceOp`,不进入模型历史,并由仅追加日志跨压缩(compaction)保留。格式错误的当前版本 payload 属于损坏;本运行时无法对不受支持的版本进行分类。 ## 委派深度 diff --git a/packages/subagent/subagent/src/child-agent.ts b/packages/subagent/subagent/src/child-agent.ts index 7582338858..22c9e77bf5 100644 --- a/packages/subagent/subagent/src/child-agent.ts +++ b/packages/subagent/subagent/src/child-agent.ts @@ -57,9 +57,38 @@ export function resolveChildDepth(parent: Agent, maxDepth: number | undefined): } /** - * Resolve the child's `AgentOptions`: the parent's provider/model/maxTokens - * route unless the request overrides it, stamped with the child's own - * delegation depth. + * Resolve the parent values inherited by a child. The latest request header + * owns provider, model, and reasoning effort after request-time selection; + * creation options remain the fallback before the first request and retain + * the configured output-token limit. + * @param parent - delegating parent Agent. + * @returns detached Agent options for child-option merging. + */ +export function parentAgentOptionsForDelegation(parent: Agent): AgentOptions { + const requestConfig = parent.session.requestHeader()?.config + if (requestConfig === undefined) return { ...parent.options } + const { + provider: _createdProvider, + model: _createdModel, + reasoningEffort: _createdReasoningEffort, + ...createdOptions + } = parent.options + return { + ...createdOptions, + provider: requestConfig.provider, + model: requestConfig.model, + ...requestConfig.reasoningEffort === undefined + ? {} + : { reasoningEffort: requestConfig.reasoningEffort }, + } +} + +/** + * Resolve the child's `AgentOptions`: the parent's provider/model, + * reasoning-effort, and maxTokens values unless the request overrides them, + * stamped with the child's own delegation depth. Changing the route without + * naming an effort clears the parent's route-owned effort so the selected + * model resolves its own default. * @param parent - the delegating parent whose route the child inherits. * @param requested - per-child overrides, if any. * @param childDepth - the resolved delegation depth to stamp. @@ -70,16 +99,22 @@ export function resolveChildAgentOptions( requested: AgentOptions | undefined, childDepth: number, ): AgentOptions { - const parentProvider = parent.options.provider - const parentModel = parent.options.model - const parentMaxTokens = parent.options.maxTokens - return { + const parentOptions = parentAgentOptionsForDelegation(parent) + const parentProvider = parentOptions.provider + const parentModel = parentOptions.model + const parentReasoningEffort = parentOptions.reasoningEffort + const parentMaxTokens = parentOptions.maxTokens + const resolved: AgentOptions = { ...parentProvider !== undefined ? { provider: parentProvider } : {}, ...parentModel !== undefined ? { model: parentModel } : {}, + ...parentReasoningEffort !== undefined ? { reasoningEffort: parentReasoningEffort } : {}, ...parentMaxTokens !== undefined ? { maxTokens: parentMaxTokens } : {}, ...requested, subagentDepth: childDepth, } + const routeChanged = resolved.provider !== parentProvider || resolved.model !== parentModel + if (routeChanged && requested?.reasoningEffort === undefined) delete resolved.reasoningEffort + return resolved } /** diff --git a/packages/subagent/subagent/src/continuation.ts b/packages/subagent/subagent/src/continuation.ts index 652a3ba6c8..2588c1a699 100644 --- a/packages/subagent/subagent/src/continuation.ts +++ b/packages/subagent/subagent/src/continuation.ts @@ -30,7 +30,7 @@ import type { AgentSetupCommit, CreateAgentOptions, } from '@deepseek-ai/dsh-agent' -import { boundContextSummary, createUserMessage, errorChain } from '@deepseek-ai/dsh-llm' +import { ReasoningEffortId, boundContextSummary, createUserMessage, errorChain } from '@deepseek-ai/dsh-llm' import type { ContentBlock, MessageId, MessageSource } from '@deepseek-ai/dsh-llm' import { SessionId } from '@deepseek-ai/dsh-session' import type { SessionEvent } from '@deepseek-ai/dsh-session' @@ -417,14 +417,17 @@ export class SubagentContinuationManager { const childDepth = resolveChildDepth(parent, request.maxDepth) // Snapshot before any await: invalid descriptor JSON rejects the call // before a child exists, and the detached value is what reaches the log. - const agentProvider = request.agentOptions?.provider ?? parent.options.provider - const agentModel = request.agentOptions?.model ?? parent.options.model + const agentOptions = resolveChildAgentOptions(parent, request.agentOptions, childDepth) + const agentProvider = agentOptions.provider + const agentModel = agentOptions.model + const agentReasoningEffort = agentOptions.reasoningEffort const descriptor = snapshotSubagentDescriptor({ mode: 'continuable', provider: spec.provider, label: spec.label, ...agentProvider !== undefined ? { agentProvider } : {}, ...agentModel !== undefined ? { agentModel } : {}, + ...agentReasoningEffort !== undefined ? { agentReasoningEffort } : {}, ...request.persona !== undefined ? { persona: request.persona } : {}, ...request.toolFilter !== undefined ? { toolFilter: request.toolFilter } : {}, }) @@ -460,7 +463,7 @@ export class SubagentContinuationManager { provider: spec.provider, parent, create: { seed, meta: childSessionMeta(parent, childDepth, lineageSeedLength), delegatedPolicies }, - agentOptions: resolveChildAgentOptions(parent, request.agentOptions, childDepth), + agentOptions, composition: { persona: request.persona, toolFilter: request.toolFilter }, signal: spec.signal, }) @@ -981,6 +984,9 @@ export class SubagentContinuationManager { agentOptions: { ...descriptor.agentProvider !== undefined ? { provider: descriptor.agentProvider } : {}, ...descriptor.agentModel !== undefined ? { model: descriptor.agentModel } : {}, + ...descriptor.agentReasoningEffort !== undefined + ? { reasoningEffort: ReasoningEffortId(descriptor.agentReasoningEffort) } + : {}, }, composition: { persona: descriptor.persona, toolFilter: descriptor.toolFilter }, signal: options.signal, diff --git a/packages/subagent/subagent/src/descriptor.ts b/packages/subagent/subagent/src/descriptor.ts index 6d9dedee75..9a25c382b1 100644 --- a/packages/subagent/subagent/src/descriptor.ts +++ b/packages/subagent/subagent/src/descriptor.ts @@ -23,6 +23,7 @@ import { snapshotJsonValue } from '@deepseek-ai/dsh-session' import type { SessionEvent } from '@deepseek-ai/dsh-session' +import type { ReasoningEffortId } from '@deepseek-ai/dsh-llm' import type { ToolRestriction } from '@deepseek-ai/dsh-tools' declare module '@deepseek-ai/dsh-session/types' { @@ -44,7 +45,7 @@ declare module '@deepseek-ai/dsh-session/types' { * Supporting another composition input is a deliberate version change, never * an implicit extra field. */ -export const SUBAGENT_DESCRIPTOR_VERSION = 2 +export const SUBAGENT_DESCRIPTOR_VERSION = 3 /** Fields shared by every supported `subagent/descriptor` payload. */ interface SubagentDescriptorBase { @@ -76,6 +77,8 @@ export interface ContinuableSubagentDescriptorData extends SubagentDescriptorBas readonly agentProvider?: string /** Resolved child `agentOptions.model`, when one was declared. */ readonly agentModel?: string + /** Resolved child `agentOptions.reasoningEffort`, when one was declared. */ + readonly agentReasoningEffort?: ReasoningEffortId /** Per-child persona that shadows the deployment persona on resume. */ readonly persona?: string /** Child tool scoping reapplied on resume. */ @@ -111,6 +114,8 @@ export interface ContinuableSubagentDescriptorInput extends SubagentDescriptorIn readonly agentProvider?: string /** Requested child `agentOptions.model`. */ readonly agentModel?: string + /** Requested child `agentOptions.reasoningEffort`. */ + readonly agentReasoningEffort?: ReasoningEffortId /** Requested per-child persona. */ readonly persona?: string /** Requested child tool scoping. */ @@ -133,6 +138,7 @@ const CONTINUABLE_DESCRIPTOR_KEYS = new Set([ ...DESCRIPTOR_BASE_KEYS, 'agentProvider', 'agentModel', + 'agentReasoningEffort', 'persona', 'toolFilter', ]) @@ -231,6 +237,7 @@ function parseSubagentDescriptor(value: unknown): SubagentDescriptorData | undef } const agentProvider = optionalString(value, 'agentProvider') const agentModel = optionalString(value, 'agentModel') + const agentReasoningEffort = optionalString(value, 'agentReasoningEffort') as ReasoningEffortId | undefined const persona = optionalString(value, 'persona') const toolFilter = Object.hasOwn(value, 'toolFilter') ? parseToolFilter(value['toolFilter']) @@ -242,6 +249,7 @@ function parseSubagentDescriptor(value: unknown): SubagentDescriptorData | undef label, ...agentProvider !== undefined ? { agentProvider } : {}, ...agentModel !== undefined ? { agentModel } : {}, + ...agentReasoningEffort !== undefined ? { agentReasoningEffort } : {}, ...persona !== undefined ? { persona } : {}, ...toolFilter !== undefined ? { toolFilter } : {}, } @@ -283,6 +291,7 @@ export function snapshotSubagentDescriptor(input: SubagentDescriptorInput): Suba label: input.label, ...input.agentProvider !== undefined ? { agentProvider: input.agentProvider } : {}, ...input.agentModel !== undefined ? { agentModel: input.agentModel } : {}, + ...input.agentReasoningEffort !== undefined ? { agentReasoningEffort: input.agentReasoningEffort } : {}, ...input.persona !== undefined ? { persona: input.persona } : {}, ...input.toolFilter !== undefined ? { toolFilter: input.toolFilter } : {}, } diff --git a/packages/subagent/subagent/src/index.ts b/packages/subagent/subagent/src/index.ts index 2f29e32010..42dca84908 100644 --- a/packages/subagent/subagent/src/index.ts +++ b/packages/subagent/subagent/src/index.ts @@ -103,6 +103,7 @@ export { applyChildComposition, captureDelegatedPolicyOverrides, childSessionMeta, + parentAgentOptionsForDelegation, resolveChildAgentOptions, resolveChildDepth, SubagentDepthError, @@ -494,6 +495,7 @@ export class SubagentRuntime extends Service { /** Reject the first requested capability that the provider lacks. */ private assertCapabilities(provider: SubagentProvider, request: SubagentStartRequest): void { const needs: { when: boolean; cap: keyof SubagentCapabilities }[] = [ + { when: request.agentOptions !== undefined, cap: 'agentOptions' }, { when: request.outputSchema !== undefined, cap: 'outputSchema' }, { when: request.maxDepth !== undefined, cap: 'depthLimit' }, { when: request.toolFilter !== undefined, cap: 'toolFilter' }, diff --git a/packages/subagent/subagent/src/out-of-process.ts b/packages/subagent/subagent/src/out-of-process.ts index abb6dd50e7..2667884af4 100644 --- a/packages/subagent/subagent/src/out-of-process.ts +++ b/packages/subagent/subagent/src/out-of-process.ts @@ -44,10 +44,11 @@ function limitSubagentDiagnostic(diagnostic: string): string { /** * The capability advertisement of an out-of-process backend: NONE. A child in * another process cannot honor parent-enforced start features - * (`outputSchema`/`maxDepth`/`toolFilter`/`persona`), so the service rejects a + * (`agentOptions`/`outputSchema`/`maxDepth`/`toolFilter`/`persona`), so the service rejects a * request needing any of them before `start` runs — never accepted-then-ignored. */ export const NO_START_CAPABILITIES: SubagentCapabilities = Object.freeze({ + agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, diff --git a/packages/subagent/subagent/src/types.ts b/packages/subagent/subagent/src/types.ts index 17978550ab..415379ca75 100644 --- a/packages/subagent/subagent/src/types.ts +++ b/packages/subagent/subagent/src/types.ts @@ -84,6 +84,7 @@ export interface SubagentRunEndInfo { * to `maxDepth`; the other names match. */ export interface SubagentCapabilities { + readonly agentOptions: boolean readonly outputSchema: boolean readonly depthLimit: boolean readonly toolFilter: boolean @@ -116,6 +117,12 @@ export interface SubagentStartRequest { * remaining turn work when it fires afterward. */ readonly signal: AbortSignal + /** + * Optional host-Agent provider, model, reasoning-effort, and output-token + * overrides. Requires {@link SubagentCapabilities.agentOptions}; in-process + * providers merge them over the parent Agent's options when they create the + * child. + */ readonly agentOptions?: AgentOptions /** * Object-rooted JSON Schema within `assertObjectJsonSchema`'s enforced subset. Start rejects diff --git a/packages/subagent/subagent/tests/child-agent.spec.ts b/packages/subagent/subagent/tests/child-agent.spec.ts new file mode 100644 index 0000000000..92302cfca4 --- /dev/null +++ b/packages/subagent/subagent/tests/child-agent.spec.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from 'vitest' +import type { Agent } from '@deepseek-ai/dsh-agent' +import { ReasoningEffortId } from '@deepseek-ai/dsh-llm' +import { Session, SessionId } from '@deepseek-ai/dsh-session' +import { resolveChildAgentOptions } from '../src/child-agent.ts' + +function parentAgent(): Agent { + const id = SessionId('parent') + return { + id, + options: { + provider: 'parent-provider', + model: 'parent-model', + reasoningEffort: ReasoningEffortId('high'), + maxTokens: 512, + }, + session: Session.create(id), + } as Agent +} + +describe('child Agent options', () => { + it('inherits the parent effort while the exact route is unchanged', () => { + expect(resolveChildAgentOptions(parentAgent(), undefined, 1)).toEqual({ + provider: 'parent-provider', + model: 'parent-model', + reasoningEffort: 'high', + maxTokens: 512, + subagentDepth: 1, + }) + }) + + it('clears an inherited effort when the child route changes', () => { + expect(resolveChildAgentOptions(parentAgent(), { model: 'child-model' }, 1)).toEqual({ + provider: 'parent-provider', + model: 'child-model', + maxTokens: 512, + subagentDepth: 1, + }) + }) + + it('keeps an explicit child effort when the child route changes', () => { + expect(resolveChildAgentOptions(parentAgent(), { + provider: 'child-provider', + model: 'child-model', + reasoningEffort: ReasoningEffortId('max'), + }, 1)).toEqual({ + provider: 'child-provider', + model: 'child-model', + reasoningEffort: 'max', + maxTokens: 512, + subagentDepth: 1, + }) + }) + + it('inherits the latest logged request selection over creation-time values', () => { + const parent = parentAgent() + parent.session.append('request/header', { + header: { + config: { + provider: 'current-provider', + model: 'current-model', + reasoningEffort: ReasoningEffortId('low'), + }, + }, + reason: 'initial', + }) + + expect(resolveChildAgentOptions(parent, undefined, 1)).toEqual({ + provider: 'current-provider', + model: 'current-model', + reasoningEffort: 'low', + maxTokens: 512, + subagentDepth: 1, + }) + }) +}) diff --git a/packages/subagent/subagent/tests/continuation.spec.ts b/packages/subagent/subagent/tests/continuation.spec.ts index 6cf1aea5a8..d1d40e0ff4 100644 --- a/packages/subagent/subagent/tests/continuation.spec.ts +++ b/packages/subagent/subagent/tests/continuation.spec.ts @@ -12,7 +12,7 @@ import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process' import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork-in-process' import type { GenerateOptions, MessageId, StreamChunk } from '@deepseek-ai/dsh-llm' -import { CallId, createUserMessage, LlmAdapter } from '@deepseek-ai/dsh-llm' +import { CallId, createUserMessage, LlmAdapter, ReasoningEffortId } from '@deepseek-ai/dsh-llm' import { defineTool } from '@deepseek-ai/dsh-tools' import InvariantRegistry from '@deepseek-ai/dsh-invariants' import { MockAdapter, maxTokensResponse, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts' @@ -239,7 +239,7 @@ describe('SubagentRuntime.startContinuable', () => { const start = vi.fn(async () => { throw new Error('must not dispatch') }) ctx.subagents.registerProvider({ name: 'one-shot', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, start, }) @@ -283,6 +283,40 @@ describe('SubagentRuntime.startContinuable', () => { expect(loaded.meta.origin).toBe('subagent') }) + it('persists a selected reasoning effort and reapplies it on cold resume', async () => { + const effort = ReasoningEffortId('max') + const adapter = new MockAdapter([ + textResponse('first answer'), + textResponse('resumed answer'), + ], { + efforts: [{ id: effort, name: 'Max' }], + defaultEffort: effort, + }) + const { ctx, parent } = await setupWith(adapter) + parkParent(ctx, parent) + const childEfforts: Array = [] + ctx.on('agent/created', ({ agent }) => { + if (agent !== parent) childEfforts.push(agent.options.reasoningEffort) + }) + + const started = await ctx.subagents.startContinuable({ + ...startSpec(parent), + request: { + prompt: message('selected reasoning'), + parent, + agentOptions: { reasoningEffort: effort }, + }, + }) + await waitNoActivation(ctx, started.childId) + const loaded = await ctx.sessionPersistence.load(started.childId) + expect(loaded.events.find(event => event.type === 'subagent/descriptor')?.data) + .toMatchObject({ agentReasoningEffort: 'max' }) + + await followup(ctx, parent, started.childId, message('resume selected reasoning')) + await waitNoActivation(ctx, started.childId) + expect(childEfforts).toEqual(['max', 'max']) + }) + it('rolls the child back completely when the caller signal aborts before acceptance', async () => { const { ctx, parent } = await setup([textResponse('unused')]) const controller = new AbortController() @@ -531,7 +565,7 @@ describe('SubagentRuntime.followup residency routing', () => { await ctx.plugin(SubagentInvariant) const disposeProvider = ctx.subagents.registerProvider({ name: 'retired', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, start: async () => { throw new Error('one-shot start is not used') }, prepareContinuable: () => Promise.resolve({}), @@ -2418,27 +2452,43 @@ describe('continuable errors', () => { hold.resolve(undefined) }) - it('reapplies the descriptor model route on cold resume', async () => { - const { ctx, parent } = await setup([textResponse('first'), textResponse('resumed')]) + it('reapplies the descriptor model route and reasoning effort on cold resume', async () => { + const effort = ReasoningEffortId('high') + const adapter = new MockAdapter([textResponse('first'), textResponse('resumed')], { + efforts: [{ id: effort, name: 'High' }], + defaultEffort: effort, + }) + const { ctx, parent } = await setupWith(adapter) const started = await ctx.subagents.startContinuable({ ...startSpec(parent), request: { prompt: message('routed work'), parent, - agentOptions: { provider: 'mock', model: 'child-model' }, + agentOptions: { provider: 'mock', model: 'child-model', reasoningEffort: effort }, }, }) await waitNoActivation(ctx, started.childId) const loaded = await ctx.sessionPersistence.load(started.childId) expect(loaded.events.find(event => event.type === 'subagent/descriptor')?.data) - .toMatchObject({ agentProvider: 'mock', agentModel: 'child-model' }) + .toMatchObject({ + agentProvider: 'mock', + agentModel: 'child-model', + agentReasoningEffort: 'high', + }) // The resumed Activation runs on the declared route, not the parent's. await followup(ctx, parent, started.childId, message('again')) await vi.waitFor(() => { - expect(ctx.agents.get(started.childId)?.options.model).toBe('child-model') + expect(ctx.agents.get(started.childId)?.options).toMatchObject({ + model: 'child-model', + reasoningEffort: 'high', + }) }) await waitNoActivation(ctx, started.childId) + const resumed = await ctx.sessionPersistence.load(started.childId) + expect(resumed.events.flatMap(event => event.type === 'request/header' + ? [event.data.header.config.reasoningEffort] + : [])).toEqual([effort, effort]) }) it('unloading the manager drains its live activations', async () => { diff --git a/packages/subagent/subagent/tests/invariant.spec.ts b/packages/subagent/subagent/tests/invariant.spec.ts index 91200abf8d..08792c2348 100644 --- a/packages/subagent/subagent/tests/invariant.spec.ts +++ b/packages/subagent/subagent/tests/invariant.spec.ts @@ -21,7 +21,7 @@ async function setup(): Promise { const provider = (name: string): SubagentProvider => ({ name, - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, start: async () => { throw new Error('not used') }, }) diff --git a/packages/subagent/subagent/tests/out-of-process.spec.ts b/packages/subagent/subagent/tests/out-of-process.spec.ts index 0d3307ca51..98b670f36f 100644 --- a/packages/subagent/subagent/tests/out-of-process.spec.ts +++ b/packages/subagent/subagent/tests/out-of-process.spec.ts @@ -21,7 +21,13 @@ import { describe('NO_START_CAPABILITIES', () => { it('advertises nothing and is frozen (shared by every out-of-process backend)', () => { - expect(NO_START_CAPABILITIES).toEqual({ outputSchema: false, depthLimit: false, toolFilter: false, persona: false }) + expect(NO_START_CAPABILITIES).toEqual({ + agentOptions: false, + outputSchema: false, + depthLimit: false, + toolFilter: false, + persona: false, + }) expect(Object.isFrozen(NO_START_CAPABILITIES)).toBe(true) }) }) diff --git a/packages/subagent/subagent/tests/service.spec.ts b/packages/subagent/subagent/tests/service.spec.ts index 05e9785611..6a93f2fdef 100644 --- a/packages/subagent/subagent/tests/service.spec.ts +++ b/packages/subagent/subagent/tests/service.spec.ts @@ -2,7 +2,7 @@ import { describe, expect, expectTypeOf, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import { type Agent } from '@deepseek-ai/dsh-agent' -import { HarnessError } from '@deepseek-ai/dsh-llm' +import { HarnessError, ReasoningEffortId } from '@deepseek-ai/dsh-llm' import { carrierKeyOf } from '@deepseek-ai/dsh-scope' import SubagentRuntime, { foldSubagentDescriptor, @@ -24,8 +24,8 @@ function fakeParent(id = 'parent-1'): Agent { return { id: SessionId(id) } as unknown as Agent } -const ALL_CAPS: SubagentCapabilities = { outputSchema: true, depthLimit: true, toolFilter: true, persona: true } -const NO_CAPS: SubagentCapabilities = { outputSchema: false, depthLimit: false, toolFilter: false, persona: false } +const ALL_CAPS: SubagentCapabilities = { agentOptions: true, outputSchema: true, depthLimit: true, toolFilter: true, persona: true } +const NO_CAPS: SubagentCapabilities = { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false } function baseRequest(overrides: Partial = {}): SubagentStartRequest { return { @@ -162,6 +162,7 @@ describe('SubagentRuntime', () => { }) it.each([ + ['agentOptions', { agentOptions: { model: 'child-model' } }], ['outputSchema', { outputSchema: { type: 'object', properties: {} } }], ['depthLimit', { maxDepth: 1 }], ['toolFilter', { toolFilter: { deny: ['bash'] } }], @@ -348,6 +349,7 @@ describe('subagent descriptors', () => { label: 'complete child', agentProvider: 'deepseek', agentModel: 'chat', + agentReasoningEffort: ReasoningEffortId('high'), persona: 'reviewer', toolFilter: { allow: ['read'], deny: ['bash'] }, } @@ -357,6 +359,7 @@ describe('subagent descriptors', () => { label: complete.label, agentProvider: complete.agentProvider, agentModel: complete.agentModel, + agentReasoningEffort: complete.agentReasoningEffort, persona: complete.persona, toolFilter: complete.toolFilter, })).toEqual(complete) @@ -448,6 +451,13 @@ describe('subagent descriptors', () => { label: 'l', agentModel: [], }, 'agentModel must be a string'], + ['invalid agent reasoning effort', { + version: SUBAGENT_DESCRIPTOR_VERSION, + mode: 'continuable', + provider: 'spawn', + label: 'l', + agentReasoningEffort: 7, + }, 'agentReasoningEffort must be a string'], ['invalid persona', { version: SUBAGENT_DESCRIPTOR_VERSION, mode: 'continuable', diff --git a/packages/subagent/tool-subagent/README.i18n.yaml b/packages/subagent/tool-subagent/README.i18n.yaml index 29d65cf9da..f6caeb1941 100644 --- a/packages/subagent/tool-subagent/README.i18n.yaml +++ b/packages/subagent/tool-subagent/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/subagent/tool-subagent/README.md -README.md: 28e6213b903ffffa7934e244b2a74ada519b32b2 -README.zh.md: 74e8896a152c787abd0aebf055d6e13f6158bcd3 +README.md: e643442de7fa45f15a5c2bf818e2c25feb44b6c5 +README.zh.md: aa6dec73c66ce6b4db525d09cd166e671dbec9dc diff --git a/packages/subagent/tool-subagent/README.md b/packages/subagent/tool-subagent/README.md index 28e6213b90..e643442de7 100644 --- a/packages/subagent/tool-subagent/README.md +++ b/packages/subagent/tool-subagent/README.md @@ -6,7 +6,11 @@ The model-facing delegation tool over one configured `ctx.subagents` provider. C ## Provider selection and lifecycle -Each plugin instance binds one `provider` to one `toolName`; the model receives no provider selector. Load another distinctly named instance to expose another transport. The tool registers only while its provider exists, avoiding sibling load-order and provider-reload dependencies. Its description follows `provider.inheritsParentContext`: fresh children require standalone prompts, while forked children already see completed parent turns. +Each plugin instance binds one subagent transport `provider` to one `toolName`; the model cannot change that transport. Load another distinctly named instance to expose another transport. `enableModelSelection: true`, or an enabled Host preference when `modelSelectionSettings: true`, requires that provider's child `agentOptions` capability and exposes optional child LLM `provider`, `model`, and `reasoning_effort` fields without additional route configuration. A call may supply a complete provider/model pair, or only an effort when configured or parent values supply the effective route. The live adapter resolves explicit or configured routes before child creation. A call that omits every selection field uses `agentOptions` and then inherits compatible missing values from the parent's latest logged request selection, falling back to its creation options before the first request and retaining its configured `maxTokens`. Changing provider or model without naming an effort clears the lower layer's route-owned effort so the selected model resolves its default. + +The delegation tool registers only while its subagent provider exists, avoiding sibling load-order and provider-reload dependencies. When model selection is enabled, its optional fields remain visible without `ctx.llm`; a call that selects a route rejects if the service is unavailable. When disabled, the schema omits those fields and execution rejects a forced selection. Configured `agentOptions` remain deployment-owned child defaults independently of this model-facing switch. Adapter catalog and topology changes do not rewrite or re-register the tool. Its description follows `provider.inheritsParentContext`: fresh children require standalone prompts, while forked children already see completed parent turns. + +An enabled definition registers `list_subagent_models`, which lists registered providers, one provider's advertised models, or one exact model's reasoning efforts at call time. At most one instance in a tool scope may enable selection because this discovery tool has a global name; duplicate owners fail registration. Shipped product compositions default the primary `subagent` (`spawn`) instance off and sample the Host `subagent-model-selection.enabled` preference when each new top-level session is composed. The enabled decision is logged as `subagent/model-selection-enabled`, inherited by child sessions, and retained on resume; later settings edits do not change a running session. Shipped compositions deliberately keep `subagent_fork` disabled so the fork inherits the parent's provider and model: changing that route would forfeit provider-side KV Cache reuse of the inherited conversation prefix and can make prefix recomputation dominate the delegated task's cost. This restriction remains even if discovery ownership is separated. Catalog membership remains advisory: an enabled delegation tool accepts an unlisted model id when its adapter does. The [model-selected route Agent Note](../../../.agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.md) owns the rationale and reintroduction condition. A foreground call passes the execution signal through startup and execution, awaits `run.result`, and always awaits `run.dispose()` before returning. Only `completed` returns the canonical `{ kind: 'foreground', runId, output: JsonValue[] }`, rendered as the same final text. Abort, refusal, token limit, and other failures become errored tool results whose message contains the stop-reason headline, an optional provider-authored `SubagentResult.diagnostic`, and then any preserved partial assistant text. The diagnostic remains separate from `SubagentResult.output`, so a truncated answer is never reported as success or confused with infrastructure detail. If result collection and disposal both reject, the errored result preserves both failures. @@ -20,9 +24,11 @@ A foreground call passes the execution signal through startup and execution, awa |---|---| | `provider` (required) | Provider name (`spawn`, `fork`, `acp`, ...). | | `toolName` | Model-facing name, default `subagent`; distinct for every loaded instance. | +| `enableModelSelection` | Exposes and accepts model-facing child LLM selection fields and registers the shared `list_subagent_models` tool, default `false`. It requires the subagent provider's `agentOptions` capability. At most one instance in a tool scope may enable it; the discovery schema remains registered without `ctx.llm`, while discovery and selected-route calls reject until that optional service is available. Configured `agentOptions` remain available when this switch is disabled. | +| `modelSelectionSettings` | Samples the Host `subagent-model-selection` preference while composing an Agent, records an enabled decision in its Session, and inherits that decision in child Sessions. Default `false`; mutually exclusive with `enableModelSelection` and valid only in an Agent-scoped composition. The preference defaults off and changes only subsequently composed top-level Sessions. | | `enableRunInBackground` | Exposes background mode, default `true`; disabling also rejects forced background calls. | | `backgroundMode` | Background lifecycle policy, default `one-shot`. `one-shot` defaults calls to foreground; `continuable` defaults them to background, requires the provider's `prepareContinuable` capability, and returns a durable child id without requiring the follow-up tool. | -| `agentOptions` | Provider-specific child `provider`, `model`, and positive `maxTokens`; the in-process provider treats explicit values as overrides of inherited parent options. | +| `agentOptions` | Configured child LLM `provider`, `model`, adapter-owned `reasoningEffort`, and positive `maxTokens`; requires the subagent provider's `agentOptions` capability. In-process providers merge explicit values over the parent's latest logged request selection, or its creation options before the first request. An inherited effort survives only while the effective provider/model route is unchanged; changing the route without an explicit effort lets the selected model supply its default. A configured provider, model, or effort is checked through the optional `ctx.llm` service before child creation even when the call omits model-selection fields; a missing service or invalid value rejects the call. | | `persona` | Per-child persona; requires provider `persona` capability. | | `toolFilter` | Per-child global-tool restriction; requires `toolFilter` capability. | | `maxDepth` | Absolute delegation-depth cap, default `3` (`0` forbids delegation); a numeric cap requires the `depthLimit` capability and fails the mount without it. `'provider-managed'` sends no cap for an out-of-process provider whose budget belongs to the child harness. The tool stays visible at the cap; each attempted start checks the calling agent's current depth and returns an errored tool result when rejected. | @@ -37,15 +43,29 @@ Foreground and background calls are concurrency-safe: sibling delegations in one #### What the model sees -The generated default [`subagent` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent) under this instance's configured name while its provider exists. Provider context inheritance changes the tool and prompt descriptions. Enabled background mode adds `run_in_background`: continuable mode documents its `true` default, runtime settlement notice, and explicit foreground override, while one-shot mode documents its `false` default and the job id collected with `job_output` or stopped with `job_kill`. While the tool is visible in an assembly's scope, a `tool:` system-prompt section tells the model to start independent continuable delegations together, keep working while they run, and choose foreground only when its next action depends on the result; a tool restriction removes both its schema and this guidance. +The generated default [`subagent` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent) under this instance's configured name while its provider exists. `enableModelSelection` adds `provider`, `model`, and `reasoning_effort` plus inheritance and selection guidance; the provider must support `agentOptions`. Provider context inheritance changes the tool and prompt descriptions. Enabled background mode adds `run_in_background`: continuable mode documents its `true` default, runtime settlement notice, and explicit foreground override, while one-shot mode documents its `false` default and the job id collected with `job_output` or stopped with `job_kill`. While the tool is visible in an assembly's scope, a `tool:` system-prompt section tells the model to start independent continuable delegations together, keep working while they run, and choose foreground only when its next action depends on the result; a tool restriction removes both its schema and this guidance. #### Token effect -Fixed schema cost per parent request; each provider instance adds one schema, and each continuable instance adds one short system-prompt section. +Fixed schema cost per parent request; enabling model selection adds three parameters. Each subagent provider instance adds one schema, and each continuable instance adds one short system-prompt section. #### KV Cache effect -Prefix-stable while provider instances, names, descriptions, and schemas are unchanged. Provider registration lifecycle may invalidate parent reuse from the first changed tool definition. +Prefix-stable while subagent provider instances and their configuration are unchanged. Adapter catalog changes do not alter the definition. A route override on an inheritance-capable instance may prevent the child from reusing the inherited parent prefix. + +### Model selection and discovery + +#### What the model sees + +An instance with static `enableModelSelection: true`, or a settings-controlled instance whose Session decision is enabled, exposes the child LLM selection fields and `list_subagent_models`. Calls reject while the optional `ctx.llm` service is unavailable. With no arguments the discovery tool returns registered provider ids and names; with `provider` it returns that adapter's advertised models; with `provider` and `model` it resolves the exact model and returns its advertised reasoning efforts and default. The result is read-only runtime metadata, not an authorization list. + +#### Token effect + +One fixed tool schema is present in shipped compositions. Directory contents enter the transcript only when the model calls the tool. + +#### KV Cache effect + +The schema is prefix-stable across adapter registration and catalog changes. Each result is appended after the reusable prefix. ### Foreground result @@ -79,4 +99,5 @@ Append-only; newly visible content follows the reusable request prefix and does - **Background runs expose no result through this tool** — a one-shot task's final output is collected through the generic task surface, and a continuable child's output stays in its own session, read by its subagent id. The settlement notice states how that child ended and carries any final assistant message, but it is not this call's return value and cannot be awaited here. - **Duplicate names across waiting one-shot instances are detected late** (`TODO(subagent-dup-toolname)`) — continuable instances reserve their prompt-section name during plugin application, but preventing provider-registration rollback for waiting one-shot instances requires a registry of intended names. -- **Child policy is fixed per instance** — another model, persona, tool filter, or depth cap requires another distinctly named tool. +- **Shipped fork tools cannot select a child LLM route** — they inherit the parent's provider and model to keep the copied conversation prefix eligible for KV Cache reuse. Re-enable the fields only when route changes preserve reuse or expose a bounded recomputation cost. +- **Non-routing child policy is fixed per instance** — another persona, tool filter, or depth cap requires another distinctly named tool. LLM provider/model/reasoning-effort selection requires static enablement or an enabled per-Session preference and a subagent provider that advertises `agentOptions`; out-of-process providers currently reject enabling it rather than ignore it. diff --git a/packages/subagent/tool-subagent/README.zh.md b/packages/subagent/tool-subagent/README.zh.md index 74e8896a15..aa6dec73c6 100644 --- a/packages/subagent/tool-subagent/README.zh.md +++ b/packages/subagent/tool-subagent/README.zh.md @@ -6,7 +6,11 @@ ## 提供方选择与生命周期 -每个插件实例把一个 `provider` 绑定到一个 `toolName`;模型不会收到提供方选择器。如需公开另一种传输,请加载另一个名称不同的实例。工具只在其提供方存在时注册,从而避免对同级加载顺序和提供方重新加载的依赖。工具描述遵循 `provider.inheritsParentContext`:新建子 agent(智能体)需要独立提示词,而 fork 子 agent 已能看到父级已完成轮次。 +每个插件实例把一个 subagent 传输 `provider` 绑定到一个 `toolName`;模型不能改变该传输。如需公开另一种传输,请加载另一个名称不同的实例。`enableModelSelection: true`,或 `modelSelectionSettings: true` 时已启用的 Host 偏好,都要求该提供方具备子级 `agentOptions` 能力,并且无需额外路由配置即可公开可选的子 agent LLM `provider`、`model` 与 `reasoning_effort` 字段。调用可以提供完整的提供方/模型对;当配置值或父 Agent 值能够提供生效路由时,也可以只提供推理强度。实时 adapter 会在创建子 agent 前解析显式或配置的路由。完全省略选择字段的调用使用 `agentOptions`,再从父 Agent 最新记录的请求选择中继承兼容的缺失值;首个请求之前回退到其创建选项,并保留其中配置的 `maxTokens`。如果更换提供方或模型但没有指定强度,则清除下层路由所属的强度,使所选模型解析自己的默认值。 + +委派工具只在其 subagent 提供方存在时注册,从而避免对同级加载顺序和提供方重新加载的依赖。启用模型选择时,即使没有 `ctx.llm`,可选字段仍然可见;选择路由的调用会在该服务缺失时失败。禁用时,schema 会省略这些字段,执行阶段也会拒绝强制传入的选择。配置的 `agentOptions` 仍是部署方所有的子级默认值,不受这个面向模型的开关影响。adapter 目录和拓扑变化不会改写或重新注册工具。工具描述遵循 `provider.inheritsParentContext`:新建子 agent(智能体)需要独立提示词,而 fork 子 agent 已能看到父级已完成轮次。 + +启用的定义会注册 `list_subagent_models`,它会在调用时列出已注册提供方、某个提供方公布的模型,或某个精确模型的推理强度。因为发现工具使用全局名称,一个工具作用域最多只能由一个实例启用选择;多个持有方会使注册失败。随附产品组合默认关闭主 `subagent`(`spawn`)实例,并在每个新的顶层会话完成组合时读取 Host 的 `subagent-model-selection.enabled` 偏好。启用决定记录为 `subagent/model-selection-enabled`,由子会话继承并在恢复时保留;之后修改设置不会改变运行中的会话。组合会刻意在 `subagent_fork` 上保持禁用,使 fork 继承父级的提供方与模型:更改该路由会失去继承对话前缀的提供方侧 KV Cache 复用,重新计算前缀的成本可能超过委派任务本身。即使分离发现工具的持有权,该限制也仍然成立。目录条目仍只提供建议:如果适配器接受未列出的模型 ID,启用选择的委派工具也会接受。理由与重新开放条件由[模型选择路由 Agent Note](../../../.agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.zh.md)负责。 前台调用会让执行信号贯穿启动和执行,等待 `run.result`,并且在返回前总会等待 `run.dispose()`。只有 `completed` 会返回规范值 `{ kind: 'foreground', runId, output: JsonValue[] }`,并渲染为相同的最终文本。中止、拒绝、token 上限和其他失败都会变成出错的工具结果,其消息依次包含终止原因标题、可选的提供方 `SubagentResult.diagnostic`,以及子 agent 保留下来的部分 assistant 文本。诊断与 `SubagentResult.output` 保持分离,因此被截断的回答不会被报告为成功,也不会与基础设施说明混淆。如果结果收集与 dispose(资源释放)都 reject,出错结果会保留两项失败。 @@ -20,9 +24,11 @@ |---|---| | `provider`(必填) | 提供方名称(`spawn`、`fork`、`acp` 等)。 | | `toolName` | 面向模型的名称,默认 `subagent`;每个已加载实例必须不同。 | +| `enableModelSelection` | 公开并接受面向模型的子级 LLM 选择字段,同时注册共享的 `list_subagent_models` 工具;默认为 `false`。它要求 subagent 提供方具备 `agentOptions` 能力。一个工具作用域最多只能由一个实例启用;即使没有 `ctx.llm`,发现 schema 仍保持注册,而发现调用和所选路由调用会在该可选服务可用前失败。禁用此开关时仍可配置 `agentOptions`。 | +| `modelSelectionSettings` | 组合 Agent 时读取 Host 的 `subagent-model-selection` 偏好,把启用决定记录进其 Session,并让子 Session 继承该决定。默认为 `false`;与 `enableModelSelection` 互斥,且只能用于 Agent 作用域组合。该偏好默认关闭,只影响之后组合的新顶层 Session。 | | `enableRunInBackground` | 公开后台模式,默认 `true`;禁用时也会拒绝强制后台调用。 | | `backgroundMode` | 后台生命周期策略,默认 `one-shot`。`one-shot` 默认前台调用;`continuable` 默认后台调用,要求提供方具备 `prepareContinuable` 能力,并返回持久化子 agent ID,且不要求加载后续消息工具。 | -| `agentOptions` | 传给具体提供方的子 agent `provider`、`model` 和正整数 `maxTokens`;进程内提供方会用显式值覆盖继承的父级选项。 | +| `agentOptions` | 配置的子 agent LLM `provider`、`model`、adapter 自有 `reasoningEffort` 与正整数 `maxTokens`;要求 subagent 提供方具备 `agentOptions` 能力。进程内提供方把显式值合并到父 Agent 最新记录的请求选择之上;首个请求之前则合并到其创建选项之上。只有生效提供方/模型路由不变时才会保留继承的推理强度;改变路由但不显式提供强度时,由所选模型提供默认值。即使调用省略模型选择字段,配置的提供方、模型或强度也会在创建子 agent 前通过可选 `ctx.llm` 服务进行校验;服务缺失或值无效都会拒绝调用。 | | `persona` | 每个子 agent 独立的 persona;要求提供方具备 `persona` 能力。 | | `toolFilter` | 每个子 agent 独立的全局工具限制;要求提供方具备 `toolFilter` 能力。 | | `maxDepth` | 绝对委派深度上限,默认 `3`(`0` 禁止委派);数值上限要求 `depthLimit` 能力,缺失时挂载失败。对于预算由子 harness 拥有的进程外提供方,`'provider-managed'` 不发送上限。工具在达到上限时仍然可见;每次尝试启动都会检查调用 agent 的当前深度,被拒绝时返回出错的工具结果。 | @@ -37,15 +43,29 @@ #### 模型看到的内容 -当提供方存在时,以当前实例配置的名称公开已生成的默认 [`subagent` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-subagent)。提供方是否继承上下文会改变工具描述和提示词描述。启用后台模式会添加 `run_in_background`:可继续模式会记录其默认值为 `true`、运行时结算通知与显式前台覆盖;一次性模式会记录其默认值为 `false`,以及用 `job_output` 收集或用 `job_kill` 停止的 job id。当工具在本次组装的作用域中可见时,一个 `tool:` 系统提示词 section 会指示模型同时启动相互独立的可继续委派、在它们运行时继续工作,并且仅当下一步动作依赖结果时选择前台;工具限制会同时移除其 schema 和这段指引。 +当提供方存在时,以当前实例配置的名称公开已生成的默认 [`subagent` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-subagent)。`enableModelSelection` 会添加 `provider`、`model` 与 `reasoning_effort`,以及继承和选择指引;提供方必须支持 `agentOptions`。提供方是否继承上下文会改变工具描述和提示词描述。启用后台模式会添加 `run_in_background`:可继续模式会记录其默认值为 `true`、运行时结算通知与显式前台覆盖;一次性模式会记录其默认值为 `false`,以及用 `job_output` 收集或用 `job_kill` 停止的 job id。当工具在本次组装的作用域中可见时,一个 `tool:` 系统提示词 section 会指示模型同时启动相互独立的可继续委派、在它们运行时继续工作,并且仅当下一步动作依赖结果时选择前台;工具限制会同时移除其 schema 和这段指引。 #### Token 影响 -每个父级请求都会产生固定的 schema token 开销;每个提供方实例增加一个 schema,每个可继续实例还会增加一个简短的系统提示词 section。 +每个父级请求都会产生固定的 schema token 开销;启用模型选择会增加三个参数。每个 subagent 提供方实例增加一个 schema,每个可继续实例还会增加一个简短的系统提示词 section。 #### KV Cache 影响 -只要提供方实例、名称、描述和 schema 不变,前缀就保持稳定。提供方注册生命周期可能从首个变化的工具定义开始,使父级复用失效。 +只要 subagent 提供方实例及其配置不变,前缀就保持稳定。adapter 目录变化不会改变定义。具备继承能力的实例如果覆盖路由,可能阻止子 agent 复用继承的父级前缀。 + +### 模型选择与发现 + +#### 模型看到的内容 + +静态配置 `enableModelSelection: true` 的实例,或 Session 决定为启用的 settings 控制实例,会公开子级 LLM 选择字段与 `list_subagent_models`。可选 `ctx.llm` 服务不可用时,调用会失败。无参数调用发现工具会返回已注册提供方的 ID 和名称;提供 `provider` 时返回该适配器公布的模型;同时提供 `provider` 和 `model` 时解析精确模型,并返回其公布的推理强度和默认值。结果是只读的运行时元数据,不是授权列表。 + +#### Token 影响 + +随附组合会包含一个固定工具 schema。只有模型调用该工具时,目录内容才会进入 transcript。 + +#### KV Cache 影响 + +adapter 注册和目录变化不会改变 schema 的前缀稳定性。每次结果都追加在可复用前缀之后。 ### 前台结果 @@ -79,4 +99,5 @@ - **后台运行不通过本工具公开结果**:一次性任务的最终输出通过通用 Task 接口收集,可继续子 agent 的输出留在其自身会话中,按其 subagent id 读取。结算通知会说明该子 agent 如何结束,并携带可能存在的最终 assistant 消息,但它不是本次调用的返回值,也无法在此等待。 - **等待中的一次性实例较晚才发现重复名称**(`TODO(subagent-dup-toolname)`):可继续实例会在插件应用期间预留提示词 section 名称,但若要阻止等待中的一次性实例回滚提供方注册,仍需要一份预期名称注册表。 -- **每个实例的子 agent 策略固定**:其他模型、persona、工具过滤器或深度上限都需要另一个名称不同的工具。 +- **随附 fork 工具无法选择子级 LLM 路由**:它们会继承父级的提供方与模型,使复制的对话前缀仍可供 KV Cache 复用。只有在路由变化仍能保留复用,或接口能公开一项有界的重算成本时,才重新启用这些字段。 +- **每个实例的非路由子 agent 策略固定**:其他 persona、工具过滤器或深度上限都需要另一个名称不同的工具。LLM 提供方/模型/推理强度选择要求静态启用或每 Session 偏好已启用,并要求 subagent 提供方声明 `agentOptions`;进程外提供方目前会拒绝启用它,而不是忽略它。 diff --git a/packages/subagent/tool-subagent/package.json b/packages/subagent/tool-subagent/package.json index 9ddab59996..708ca2335b 100644 --- a/packages/subagent/tool-subagent/package.json +++ b/packages/subagent/tool-subagent/package.json @@ -18,6 +18,10 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./model-selection-settings": { + "types": "./lib/types/model-selection-settings.d.ts", + "default": "./lib/model-selection-settings.js" + }, "./invariant": { "types": "./lib/types/invariant.d.ts", "default": "./lib/invariant.js" @@ -28,6 +32,7 @@ "files": [ "lib/index.js", "lib/invariant.js", + "lib/model-selection-settings.js", "lib/types/**/*.d.ts" ], "license": "MIT", @@ -35,6 +40,9 @@ "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", + "@deepseek-ai/dsh-session": "workspace:^", + "@deepseek-ai/dsh-scope": "workspace:^", + "@deepseek-ai/dsh-settings": "workspace:^", "@deepseek-ai/dsh-subagent": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-jobs": "workspace:^", @@ -50,8 +58,10 @@ "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", + "@deepseek-ai/dsh-scope": "workspace:^", "@deepseek-ai/dsh-session-persistence": "workspace:^", "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^", + "@deepseek-ai/dsh-settings": "workspace:^", "@deepseek-ai/dsh-subagent": "workspace:^", "@deepseek-ai/dsh-subagent-spawn-in-process": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", diff --git a/packages/subagent/tool-subagent/src/index.ts b/packages/subagent/tool-subagent/src/index.ts index ef0e9941eb..ceb04cced8 100644 --- a/packages/subagent/tool-subagent/src/index.ts +++ b/packages/subagent/tool-subagent/src/index.ts @@ -10,14 +10,33 @@ import type { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' +import { scopeChainOf, scopeOf } from '@deepseek-ai/dsh-scope' import { defineTool } from '@deepseek-ai/dsh-tools' -import type { AgentOptions } from '@deepseek-ai/dsh-agent' +import type { Agent, AgentOptions } from '@deepseek-ai/dsh-agent' +import { ReasoningEffortId } from '@deepseek-ai/dsh-llm' import type { ContentBlock } from '@deepseek-ai/dsh-llm' import type { JsonValue } from '@deepseek-ai/dsh-session' -import { assertSubagentMaxDepth, settleRun } from '@deepseek-ai/dsh-subagent' +import { + assertSubagentMaxDepth, + parentAgentOptionsForDelegation, + settleRun, +} from '@deepseek-ai/dsh-subagent' import type { SubagentProvider, SubagentResult, SubagentRun } from '@deepseek-ai/dsh-subagent' import type { JobOutcome } from '@deepseek-ai/dsh-jobs' import type {} from '@deepseek-ai/dsh-system-prompt' +import { + hasConfiguredLlmSelection, + hasDelegationModelRequest, + preflightChildLlmRoute, + requestedAgentOptions, +} from './model-selection.ts' +import type { DelegationModelRequest } from './model-selection.ts' +import { registerListSubagentModels } from './list-models.ts' +import type {} from './model-selection-settings.ts' +import { + hasSubagentModelSelection, + recordSubagentModelSelection, +} from './model-selection-state.ts' export const name = 'tool-subagent' export const inject = ['tools', 'subagents', 'systemPrompt'] @@ -34,6 +53,14 @@ export interface Config { * a distinct name. */ toolName?: string + /** Let the model discover and select the child LLM route (default false). */ + enableModelSelection?: boolean + /** + * Sample the Host `subagent-model-selection` user setting for each new + * top-level session and inherit that decision in its child sessions. Mutually + * exclusive with `enableModelSelection`. + */ + modelSelectionSettings?: boolean /** * Expose `run_in_background` (default true). Disabled instances omit the * parameter and reject forced background calls. @@ -81,14 +108,22 @@ export interface Config { export const Config: z = z.object({ provider: z.string().required(), toolName: z.string().default('subagent'), + enableModelSelection: z.boolean().default(false), + modelSelectionSettings: z.boolean().default(false), enableRunInBackground: z.boolean().default(true), backgroundMode: z.union(['one-shot', 'continuable'] as const).default('one-shot'), // Prevent Schemastery from materializing omitted agentOptions as `{}`. agentOptions: z.object({ provider: z.string(), model: z.string(), + reasoningEffort: z.string().min(1) as z>, maxTokens: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER), - }).default(undefined as unknown as { provider: string; model: string; maxTokens: number }), + }).default(undefined as unknown as { + provider: string + model: string + reasoningEffort: ReturnType + maxTokens: number + }), persona: z.string(), // Preserve omission; Schemastery's `{ allow: [] }` default would deny every tool. toolFilter: z.object({ @@ -281,196 +316,348 @@ export function apply(ctx: Context, config: Config): void { if (config.toolFilter !== undefined && config.toolFilter.allow === undefined && config.toolFilter.deny === undefined) { throw new Error('tool-subagent: `toolFilter` is configured but names neither `allow` nor `deny` — remove the key or fill the filter') } + if (config.enableModelSelection === true && config.modelSelectionSettings === true) { + throw new Error('tool-subagent: `enableModelSelection` and `modelSelectionSettings` are mutually exclusive') + } const backgroundEnabled = config.enableRunInBackground !== false const continuable = (config.backgroundMode ?? 'one-shot') === 'continuable' const toolName = config.toolName ?? 'subagent' - // Load order and HMR replacement can change provider availability while - // this fiber remains active. - let disposeTool: (() => void) | undefined - const mount = (provider: SubagentProvider): void => { - // A numeric cap the provider cannot enforce is a misconfiguration — fail at - // mount (the earliest point the provider's capabilities are known), not on - // the first delegation. - if (typeof config.maxDepth === 'number' && !provider.capabilities.depthLimit) { + + const modelSelectionCapable = config.enableModelSelection === true || config.modelSelectionSettings === true + + const assertSubagentProviderConfiguration = (subagentProvider: SubagentProvider): void => { + if (typeof config.maxDepth === 'number' && !subagentProvider.capabilities.depthLimit) { throw new Error( - `tool-subagent: provider "${provider.name}" cannot enforce maxDepth (no depthLimit capability) — ` + `tool-subagent: provider "${subagentProvider.name}" cannot enforce maxDepth (no depthLimit capability) — ` + 'set maxDepth: \'provider-managed\' to leave the recursion budget to the provider', ) } - const wording = providerWording(provider.inheritsParentContext) - if (continuable && provider.prepareContinuable === undefined) { + if (config.agentOptions !== undefined && !subagentProvider.capabilities.agentOptions) { throw new Error( - `tool-subagent: provider "${provider.name}" does not support \`backgroundMode: continuable\``, + `tool-subagent: provider "${subagentProvider.name}" does not support child agentOptions`, + ) + } + if (modelSelectionCapable && !subagentProvider.capabilities.agentOptions) { + throw new Error( + `tool-subagent: provider "${subagentProvider.name}" does not support child model selection`, + ) + } + if (continuable && subagentProvider.prepareContinuable === undefined) { + throw new Error( + `tool-subagent: provider "${subagentProvider.name}" does not support \`backgroundMode: continuable\``, ) } - disposeTool = ctx.tools.register(defineTool({ - name: toolName, - description: wording.description + (backgroundEnabled - // The completion notice is the continuation service's own behavior, not - // a separately installed capability, so this promise holds whenever the - // continuable background path is reachable at all. - ? continuable - ? ' This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.' - : ' This call waits for the result by default. Set `run_in_background: true` to return a job id; collect with `job_output` and stop with `job_kill`.' - : ' This call waits for the subagent and returns its result.'), - parameters: { - description: { - type: 'string', - required: true, - description: 'A short (3-5 word) description of the delegated task, for display.', - }, - prompt: { - type: 'string', - required: true, - description: wording.promptDescription, - }, - ...backgroundEnabled ? { - run_in_background: { - type: 'boolean' as const, - description: continuable - ? 'Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it.' - : 'Whether to run as a background job and return its id. Defaults to false; collect with job_output or stop with job_kill.', - }, - } : {}, - }, - output: { - schema: { - oneOf: [ - { - type: 'object', - additionalProperties: false, - properties: { - kind: { type: 'string', required: true, const: 'background' }, - jobId: { type: 'string', required: true }, - }, - }, - { - type: 'object', - additionalProperties: false, - properties: { - kind: { type: 'string', required: true, const: 'continuable' }, - subagentId: { type: 'string', required: true }, - }, - }, - { - type: 'object', - additionalProperties: false, - properties: { - kind: { type: 'string', required: true, const: 'foreground' }, - runId: { type: 'string', required: true }, - output: { type: 'array', required: true, items: { type: 'json' } }, - }, - }, - ], - }, - render: (_args, value) => [{ - type: 'text', - text: value.kind === 'background' - ? `started background subagent job ${value.jobId}` - : value.kind === 'continuable' - ? `started subagent ${value.subagentId}` - : outputValueText(value.output), - }], - }, - // Children never mutate the parent session; the one parent-owned write - // (tasks.start) is a synchronous commutative insertion. - isConcurrencySafe: () => true, - async execute(args, exec) { - const parent = exec.agent - if (!parent) { - // Non-agent callers provide no parent for delegation ownership. - throw new Error('subagent tool requires a calling agent (exec.agent was undefined)') - } - - const maxDepth = typeof config.maxDepth === 'number' ? config.maxDepth : undefined - const request = { - label: args.description, - prompt: [{ type: 'text', text: args.prompt }] as ContentBlock[], - parent, - ...config.agentOptions !== undefined ? { agentOptions: config.agentOptions } : {}, - ...config.persona !== undefined ? { persona: config.persona } : {}, - ...config.toolFilter !== undefined ? { toolFilter: config.toolFilter } : {}, - ...maxDepth !== undefined ? { maxDepth } : {}, - } - - const runSpec = resolveDelegationRun(args, { backgroundEnabled, continuable }) - if (runSpec.runInBackground) { - if (continuable) { - // Resolves at inbox acceptance: the child owns its own turns from - // there, so this call neither waits for nor collects a result. - const started = await ctx.subagents.startContinuable({ - provider: config.provider, - label: args.description, - request, - signal: exec.signal, - }) - return { kind: 'continuable' as const, subagentId: started.childId } - } - const jobs = ctx.get('jobs') - if (jobs === undefined) { - throw new Error('background jobs unavailable: load @deepseek-ai/dsh-jobs and @deepseek-ai/dsh-tool-jobs') - } - // One-shot background child: job preflight finishes before the - // starter can spawn, and the task-owned signal covers startup. - const id = jobs.start({ - kind: 'subagent', - label: args.description, - owner: parent, - run: () => { - const controller = new AbortController() - const start = ctx.subagents.start(config.provider, { ...request, signal: controller.signal }) - return { - cancel: (reason?: string) => { - controller.abort(reason ?? 'background subagent task killed') - }, - done: settleStart(start, controller.signal), - // No readOutput: the child session owns intermediate detail. - } - }, - }) - return { kind: 'background' as const, jobId: id } - } - - const run: SubagentRun = await ctx.subagents.start(config.provider, { - ...request, - signal: exec.signal, - }) - return settleForegroundRun(run) - }, - })) } - // Register listeners before checking presence so no synchronous change is missed. - // TODO(subagent-dup-toolname): two waiting one-shot fibers configured with the - // same toolName collide when their provider appears, and the duplicate-name - // throw rolls back the provider registration. Continuable instances reserve - // their prompt-section name during apply() and fail earlier. Add an intent - // registry if the late one-shot collision occurs in a shipped composition. - ctx.on('subagent/provider-added', (provider) => { - if (provider.name === config.provider && disposeTool === undefined) mount(provider) + // Validate provider-owned config outside the optional LLM binding so an + // invalid provider always rejects its registration or this plugin's load. + ctx.on('subagent/provider-added', (subagentProvider) => { + if (subagentProvider.name === config.provider) assertSubagentProviderConfiguration(subagentProvider) }) - ctx.on('subagent/provider-removed', (name) => { - if (name !== config.provider || disposeTool === undefined) return - disposeTool() - disposeTool = undefined - }) - const present = ctx.subagents.getProvider(config.provider) - if (present !== undefined) { - mount(present) - } else { - // A backend fiber may activate later; a misspelled provider remains visible in this log. - ctx.logger.info(`subagent provider "${config.provider}" not registered yet; the "${config.toolName ?? 'subagent'}" tool will register when it appears`) - } - if (backgroundEnabled && continuable) { - // The section follows provider availability without its own manual - // lifecycle: empty text is omitted from rendered prompts while the tool is - // absent, and the registration itself stays owned by this plugin fiber. - ctx.systemPrompt.section({ - name: `tool:${toolName}`, - order: SUBAGENT_SECTION_ORDER, - text: context => disposeTool === undefined || ctx.tools.get(toolName, context.scope) === undefined + const initialProvider = ctx.subagents.getProvider(config.provider) + if (initialProvider !== undefined) assertSubagentProviderConfiguration(initialProvider) + + const install = (runtimeCtx: Context, modelSelectionEnabled: boolean): void => { + if (modelSelectionEnabled) registerListSubagentModels(runtimeCtx) + // Load order and HMR replacement can change provider availability while + // this fiber remains active. + let mounted: { subagentProvider: SubagentProvider; disposeTool: () => void } | undefined + const mount = (subagentProvider: SubagentProvider): void => { + assertSubagentProviderConfiguration(subagentProvider) + const wording = providerWording(subagentProvider.inheritsParentContext) + const choiceDescription = !modelSelectionEnabled ? '' - : `Use ${toolName} in the background by default. Start independent delegations together in one assistant message and continue useful work while they run. Set \`run_in_background: false\` only when your next action depends on that subagent's result. When a background run settles, the runtime sends you a notice containing its outcome and any final assistant message.`, + : ' Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model\'s default effort.' + + (subagentProvider.inheritsParentContext + ? ' Changing the route can prevent provider-side reuse of the inherited conversation prefix.' + : '') + const disposeTool = runtimeCtx.tools.register(defineTool({ + name: toolName, + description: wording.description + (backgroundEnabled + // The completion notice is the continuation service's own behavior, not + // a separately installed capability, so this promise holds whenever the + // continuable background path is reachable at all. + ? continuable + ? ' This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.' + : ' This call waits for the result by default. Set `run_in_background: true` to return a job id; collect with `job_output` and stop with `job_kill`.' + : ' This call waits for the subagent and returns its result.') + choiceDescription, + parameters: { + description: { + type: 'string', + required: true, + description: 'A short (3-5 word) description of the delegated task, for display.', + }, + prompt: { + type: 'string', + required: true, + description: wording.promptDescription, + }, + ...modelSelectionEnabled ? { + provider: { + type: 'string' as const, + description: 'LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route.', + }, + model: { + type: 'string' as const, + description: 'Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route.', + }, + reasoning_effort: { + type: 'string' as const, + description: 'Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model\'s default.', + }, + } : {}, + ...backgroundEnabled ? { + run_in_background: { + type: 'boolean' as const, + description: continuable + ? 'Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it.' + : 'Whether to run as a background job and return its id. Defaults to false; collect with job_output or stop with job_kill.', + }, + } : {}, + }, + output: { + schema: { + oneOf: [ + { + type: 'object', + additionalProperties: false, + properties: { + kind: { type: 'string', required: true, const: 'background' }, + jobId: { type: 'string', required: true }, + }, + }, + { + type: 'object', + additionalProperties: false, + properties: { + kind: { type: 'string', required: true, const: 'continuable' }, + subagentId: { type: 'string', required: true }, + }, + }, + { + type: 'object', + additionalProperties: false, + properties: { + kind: { type: 'string', required: true, const: 'foreground' }, + runId: { type: 'string', required: true }, + output: { type: 'array', required: true, items: { type: 'json' } }, + }, + }, + ], + }, + render: (_args, value) => [{ + type: 'text', + text: value.kind === 'background' + ? `started background subagent job ${value.jobId}` + : value.kind === 'continuable' + ? `started subagent ${value.subagentId}` + : outputValueText(value.output), + }], + }, + // Children never mutate the parent session; the one parent-owned write + // (tasks.start) is a synchronous commutative insertion. + isConcurrencySafe: () => true, + async execute(args, exec) { + const parent = exec.agent + if (!parent) { + // Non-agent callers provide no parent for delegation ownership. + throw new Error('subagent tool requires a calling agent (exec.agent was undefined)') + } + + const modelRequest = args as DelegationModelRequest + const parentOptions = parentAgentOptionsForDelegation(parent) + const childAgentOptions = requestedAgentOptions( + parentOptions, + config.agentOptions, + modelRequest, + modelSelectionEnabled, + ) + if (hasDelegationModelRequest(modelRequest) || hasConfiguredLlmSelection(config.agentOptions)) { + const llm = runtimeCtx.get('llm') + if (llm === undefined) { + throw new Error('cannot resolve the selected child LLM route because the `llm` service is unavailable') + } + await preflightChildLlmRoute(llm, parentOptions, childAgentOptions, exec.signal) + } + exec.signal.throwIfAborted() + const maxDepth = typeof config.maxDepth === 'number' ? config.maxDepth : undefined + const request = { + label: args.description, + prompt: [{ type: 'text', text: args.prompt }] as ContentBlock[], + parent, + ...childAgentOptions !== undefined ? { agentOptions: childAgentOptions } : {}, + ...config.persona !== undefined ? { persona: config.persona } : {}, + ...config.toolFilter !== undefined ? { toolFilter: config.toolFilter } : {}, + ...maxDepth !== undefined ? { maxDepth } : {}, + } + + const runSpec = resolveDelegationRun(args, { backgroundEnabled, continuable }) + if (runSpec.runInBackground) { + if (continuable) { + // Resolves at inbox acceptance: the child owns its own turns from + // there, so this call neither waits for nor collects a result. + const started = await runtimeCtx.subagents.startContinuable({ + provider: config.provider, + label: args.description, + request, + signal: exec.signal, + }) + return { kind: 'continuable' as const, subagentId: started.childId } + } + const jobs = runtimeCtx.get('jobs') + if (jobs === undefined) { + throw new Error('background jobs unavailable: load @deepseek-ai/dsh-jobs and @deepseek-ai/dsh-tool-jobs') + } + // One-shot background child: job preflight finishes before the + // starter can spawn, and the task-owned signal covers startup. + const id = jobs.start({ + kind: 'subagent', + label: args.description, + owner: parent, + run: () => { + const controller = new AbortController() + const start = runtimeCtx.subagents.start(config.provider, { ...request, signal: controller.signal }) + return { + cancel: (reason?: string) => { + controller.abort(reason ?? 'background subagent task killed') + }, + done: settleStart(start, controller.signal), + // No readOutput: the child session owns intermediate detail. + } + }, + }) + return { kind: 'background' as const, jobId: id } + } + + const run: SubagentRun = await runtimeCtx.subagents.start(config.provider, { + ...request, + signal: exec.signal, + }) + return settleForegroundRun(run) + }, + })) + mounted = { subagentProvider, disposeTool } + } + + // Register listeners before checking presence so no synchronous change is missed. + // TODO(subagent-dup-toolname): two waiting one-shot fibers configured with the + // same toolName collide when their provider appears, and the duplicate-name + // throw rolls back the provider registration. Continuable instances reserve + // their prompt-section name during apply() and fail earlier. Add an intent + // registry if the late one-shot collision occurs in a shipped composition. + runtimeCtx.on('subagent/provider-added', (subagentProvider) => { + if (subagentProvider.name === config.provider && mounted === undefined) mount(subagentProvider) + }) + runtimeCtx.on('subagent/provider-removed', (name) => { + if (name !== config.provider || mounted === undefined) return + mounted.disposeTool() + mounted = undefined + }) + const present = runtimeCtx.subagents.getProvider(config.provider) + if (present !== undefined) { + mount(present) + } else { + // A backend fiber may activate later; a misspelled provider remains visible in this log. + runtimeCtx.logger.info(`subagent provider "${config.provider}" not registered yet; the "${config.toolName ?? 'subagent'}" tool will register when it appears`) + } + if (backgroundEnabled && continuable) { + // The section follows provider availability without its own manual + // lifecycle: empty text is omitted from rendered prompts while the tool is + // absent, and the registration itself stays owned by this plugin fiber. + runtimeCtx.systemPrompt.section({ + name: `tool:${toolName}`, + order: SUBAGENT_SECTION_ORDER, + text: context => mounted === undefined || runtimeCtx.tools.get(toolName, context.scope) === undefined + ? '' + : `Use ${toolName} in the background by default. Start independent delegations together in one assistant message and continue useful work while they run. Set \`run_in_background: false\` only when your next action depends on that subagent's result. When a background run settles, the runtime sends you a notice containing its outcome and any final assistant message.`, + }) + } + } + + if (config.modelSelectionSettings !== true) { + install(ctx, config.enableModelSelection === true) + return + } + + const settings = ctx.get('subagentModelSelection') + if (settings === undefined) { + throw new Error( + 'tool-subagent: `modelSelectionSettings` requires ' + + '@deepseek-ai/dsh-tool-subagent/model-selection-settings in the Host scope', + ) + } + const compositionScope = scopeOf(ctx) + if (compositionScope === undefined) { + throw new Error('tool-subagent: `modelSelectionSettings` requires an Agent or preset scope') + } + + const selectForAgent = (agent: NonNullable): boolean => { + let enabled = hasSubagentModelSelection(agent.session) + if (!enabled) { + const parentId = agent.session.header.origin === 'subagent' + ? agent.session.header.parentSession + : undefined + if (parentId !== undefined) { + const parent = ctx.get('agents')?.get(parentId) + enabled = parent !== undefined && hasSubagentModelSelection(parent.session) + } else if (agent.session.firstLiveSeq === 0) { + enabled = settings.currentEnabled() + } + } + if (enabled) recordSubagentModelSelection(agent.session) + return enabled + } + + const agent = ctx.agent + if (agent !== undefined) { + install(ctx, selectForAgent(agent)) + return + } + const agents = ctx.get('agents') + /* v8 ignore next -- Agent and preset scopes are minted only by the Agent registry. */ + if (agents === undefined) throw new Error('tool-subagent: scoped model-selection settings require the Agent registry') + const scopedInstalls = new WeakMap>() + const installing = new WeakSet() + const belongsToComposition = (candidate: Agent): boolean => + scopeChainOf(scopeOf(candidate.ctx)).includes(compositionScope) + const installScoped = (candidate: Agent): void => { + if (scopedInstalls.has(candidate) || installing.has(candidate)) return + // Reserve before the injected fiber runs: tool registration emits + // `tools/change` synchronously, which re-enters the reconciliation below. + installing.add(candidate) + const enabled = selectForAgent(candidate) + const fiber = candidate.ctx.inject(['tools', 'subagents', 'systemPrompt'], (runtimeCtx) => { + install(runtimeCtx, enabled) + }) + installing.delete(candidate) + scopedInstalls.set(candidate, fiber) + } + const removeScoped = (candidate: Agent): void => { + const fiber = scopedInstalls.get(candidate) + if (fiber === undefined) return + scopedInstalls.delete(candidate) + /* v8 ignore next 3 -- Cordis Fiber disposal contains registration cleanup failures; this is the final diagnostic sink. */ + void fiber.dispose().catch((error: unknown) => { + ctx.logger.warn(`tool-subagent: failed to remove recomposed Agent "${candidate.id}" definitions: ${String(error)}`) }) } + const reconcileComposedAgents = (): void => { + // Every Agent and preset scope is minted by the Agent registry; the scope + // check above makes this same-process typed relationship authoritative. + for (const candidate of agents.list()) { + if (belongsToComposition(candidate)) installScoped(candidate) + else removeScoped(candidate) + } + } + // A shipped preset is mounted once in a standing scope. Its listener admits + // only descendant Agents and installs the sampled tool definition in each + // Agent's own scope, so a later settings change cannot mutate a live session. + ctx.on('agent/created', ({ agent: created }) => { + installScoped(created) + }) + ctx.on('agent/disposed', ({ agent: disposed }) => { removeScoped(disposed) }) + // Reparenting an Agent between standing presets changes its inherited tool + // set and emits `tools/change`; reconcile the Agent-owned override with the + // new ancestry. Other registry changes are idempotent no-ops here. + ctx.on('tools/change', reconcileComposedAgents) } diff --git a/packages/subagent/tool-subagent/src/invariant.ts b/packages/subagent/tool-subagent/src/invariant.ts index 5b8facc900..84bd209caa 100644 --- a/packages/subagent/tool-subagent/src/invariant.ts +++ b/packages/subagent/tool-subagent/src/invariant.ts @@ -5,7 +5,8 @@ /* jscpd:ignore-start */ import type { Context } from '@deepseek-ai/cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { hasSubagentModelSelection } from './model-selection-state.ts' const PACKAGE_NAME = '@deepseek-ai/dsh-tool-subagent' @@ -14,11 +15,24 @@ export const name = 'tool-subagent-invariant' /** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** - * No runtime invariant: this model-facing adapter has no independent lifecycle stream; execution - * relations are owned by the capability seam it calls. - */ -const install: InvariantInstaller = () => {} +/** Assert that a durable opt-in is represented by both model-facing definitions. */ +const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => { + ctx.on('agent/pre-step', async ({ agent }, next) => { + if (hasSubagentModelSelection(agent.session)) { + const schemas = ctx.tools.schemas(agent) + const selectable = schemas.some((schema) => { + const properties = (schema.parameters as { properties?: Record }).properties + return properties?.['provider'] !== undefined + && properties['model'] !== undefined + && properties['reasoning_effort'] !== undefined + }) + if (!selectable || !schemas.some(schema => schema.name === 'list_subagent_models')) { + fail('a subagent/model-selection-enabled session must expose route fields and list_subagent_models') + } + } + return next() + }, { global: true }) +}, { inject: ['tools'] }) /** * Register this package's invariant companion. diff --git a/packages/subagent/tool-subagent/src/list-models.ts b/packages/subagent/tool-subagent/src/list-models.ts new file mode 100644 index 0000000000..9e1ff5c24e --- /dev/null +++ b/packages/subagent/tool-subagent/src/list-models.ts @@ -0,0 +1,94 @@ +/** Model-facing discovery of LLM routes available to child Agents. */ + +import type { Context } from '@deepseek-ai/cordis' +import type LlmRuntime from '@deepseek-ai/dsh-llm' +import type { LlmProviderInfo } from '@deepseek-ai/dsh-llm' +import { defineTool } from '@deepseek-ai/dsh-tools' + +interface ListSubagentModelsRequest { + readonly provider?: string + readonly model?: string +} + +/** Resolve one registered provider with a model-correctable diagnostic. */ +function registeredProvider(llm: LlmRuntime, providerId: string): LlmProviderInfo { + const providers = llm.listProviders() + const provider = providers.find(candidate => candidate.id === providerId) + if (provider !== undefined) return provider + const available = providers.map(candidate => candidate.id).join(', ') || '(none)' + throw new Error(`LLM provider "${providerId}" is not registered; available providers: ${available}`) +} + +/** Render one advertised or resolved model. */ +function modelLine(provider: string, model: { id: string; name: string; description?: string }): string { + return `${provider}/${model.id} — ${model.name}${model.description === undefined ? '' : `: ${model.description}`}` +} + +/** Read the requested provider, advertised models, or exact-model efforts. */ +async function listSubagentModels( + ctx: Context, + request: ListSubagentModelsRequest, + signal: AbortSignal, +): Promise { + const llm = ctx.get('llm') + if (llm === undefined) { + throw new Error('cannot discover child LLM routes because the `llm` service is unavailable') + } + if (request.model !== undefined && request.provider === undefined) { + throw new Error('`model` requires `provider`') + } + if (request.provider === undefined) { + const providers = llm.listProviders() + return providers.length === 0 + ? '(no LLM providers)' + : providers.map(provider => `${provider.id} — ${provider.name}`).join('\n') + } + if (request.provider.length === 0) throw new Error('`provider` must be non-empty') + const provider = registeredProvider(llm, request.provider) + if (request.model === undefined) { + const models = await llm.listModels(provider.id) + return models.length === 0 + ? `(no advertised models for ${provider.id})` + : models.map(model => modelLine(provider.id, model)).join('\n') + } + if (request.model.length === 0) throw new Error('`model` must be non-empty') + const model = await llm.resolveModelInfo(provider.id, request.model, signal) + const efforts = model.reasoning?.efforts.map(effort => ( + `${effort.id}${model.reasoning?.defaultEffort === effort.id ? ' (default)' : ''} — ${effort.name}` + + (effort.description === undefined ? '' : `: ${effort.description}`) + )).join('\n') || '(no advertised reasoning efforts)' + return `${modelLine(provider.id, model)}\nReasoning efforts:\n${efforts}` +} + +/** + * Register `list_subagent_models` for one owning delegation-tool instance. + * @param ctx - Context whose tool registry owns the fixed discovery definition. + */ +export function registerListSubagentModels(ctx: Context): void { + ctx.tools.register(defineTool({ + name: 'list_subagent_models', + description: + 'Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list ' + + 'registered providers, with `provider` to list its advertised models, or with `provider` and `model` ' + + 'to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may ' + + 'accept an unlisted model id. Use the returned ids with a delegation tool\'s `provider`, `model`, and ' + + '`reasoning_effort` fields.', + parameters: { + provider: { + type: 'string', + description: 'Registered LLM provider id. Omit to list providers.', + }, + model: { + type: 'string', + description: 'Exact model id to inspect. Requires provider; omit to list that provider\'s advertised models.', + }, + }, + output: { + schema: { type: 'string' }, + render: (_args, result) => [{ type: 'text', text: result }], + }, + execute(args, exec) { + return listSubagentModels(ctx, args, exec.signal) + }, + })) +} diff --git a/packages/subagent/tool-subagent/src/model-selection-settings.ts b/packages/subagent/tool-subagent/src/model-selection-settings.ts new file mode 100644 index 0000000000..113cd4c6f8 --- /dev/null +++ b/packages/subagent/tool-subagent/src/model-selection-settings.ts @@ -0,0 +1,70 @@ +/** Host-owned opt-in setting for model-selectable subagent delegation. */ + +import { Context, Service } from '@deepseek-ai/cordis' +import z from '@deepseek-ai/schemastery' +import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings' + +declare module '@deepseek-ai/cordis' { + interface Context { + /** User preference sampled when a new Agent receives its delegation tools. */ + subagentModelSelection: SubagentModelSelectionConfig + } +} + +/** User-settings section for model-selectable subagent delegation. */ +export const SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE = settingsNamespace('subagent-model-selection') + +/** Stored user preference; the shipped composition defaults it off. */ +export interface SubagentModelSelectionSettings { + /** Whether new Agents may expose child LLM route selection to the model. */ + enabled: boolean +} + +/** Schema served to settings clients for the opt-in preference. */ +export const SUBAGENT_MODEL_SELECTION_SETTINGS_SCHEMA: z = z.object({ + enabled: z.boolean().default(false), +}) + +/** Optional deployment base for the preference. */ +export interface Config { + /** Initial value inherited when the user document does not override it. */ + enabled?: boolean +} + +/** Singleton settings owner read by delegation tools when an Agent is published. */ +export class SubagentModelSelectionConfig extends Service { + static Config: z = z.object({ + enabled: z.boolean().default(false), + }) + + private source: () => SubagentModelSelectionSettings + + constructor(ctx: Context, config: Config = {}) { + super(ctx, 'subagentModelSelection') + const entry: SubagentModelSelectionSettings = { enabled: config.enabled === true } + this.source = () => entry + installSettingsSection( + ctx, + SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, + SUBAGENT_MODEL_SELECTION_SETTINGS_SCHEMA, + entry, + { + setSource: (source) => { this.source = source }, + // Consumers sample at Agent publication, so a settings update never + // rebuilds the tool definitions of an Agent that is already running. + onChange: () => {}, + }, + ) + } + + /** + * Read the preference for the next eligible Agent publication. + * @returns whether that Agent should receive model-selectable delegation. + */ + currentEnabled(): boolean { + return this.source().enabled + } +} + +export const name = 'subagent-model-selection-settings' +export default SubagentModelSelectionConfig diff --git a/packages/subagent/tool-subagent/src/model-selection-state.ts b/packages/subagent/tool-subagent/src/model-selection-state.ts new file mode 100644 index 0000000000..35345ac115 --- /dev/null +++ b/packages/subagent/tool-subagent/src/model-selection-state.ts @@ -0,0 +1,33 @@ +/** Durable per-session state for the user-controlled model-selection opt-in. */ + +import type { Session } from '@deepseek-ai/dsh-session' + +declare module '@deepseek-ai/dsh-session/types' { + interface SessionEventMap { + /** + * Records that this session's delegation tool exposes child provider, + * model, and reasoning-effort selection. Appended before the first model + * request; absence means the fixed-route definition. Log-only: it carries + * no `surfaceOp` and never enters model history. + */ + 'subagent/model-selection-enabled': Record + } +} + +/** + * Whether a session log records the enabled model-selection definition. + * @param session - session whose durable decision is read. + * @returns whether model-selectable delegation is enabled for the session. + */ +export function hasSubagentModelSelection(session: Session): boolean { + return session.events.some(event => event.type === 'subagent/model-selection-enabled') +} + +/** + * Append the enabled decision once, before its definition can reach a model request. + * @param session - session receiving the enabled decision. + */ +export function recordSubagentModelSelection(session: Session): void { + if (hasSubagentModelSelection(session)) return + session.append('subagent/model-selection-enabled', {}) +} diff --git a/packages/subagent/tool-subagent/src/model-selection.ts b/packages/subagent/tool-subagent/src/model-selection.ts new file mode 100644 index 0000000000..6b89d92742 --- /dev/null +++ b/packages/subagent/tool-subagent/src/model-selection.ts @@ -0,0 +1,112 @@ +/** Child LLM route selection for the subagent tool. */ + +import { ReasoningEffortId } from '@deepseek-ai/dsh-llm' +import type { LlmRuntime } from '@deepseek-ai/dsh-llm' +import type { AgentOptions } from '@deepseek-ai/dsh-agent' + +/** Model-facing child LLM route fields. */ +export interface DelegationModelRequest { + readonly provider?: string + readonly model?: string + readonly reasoning_effort?: string +} + +/** + * Whether a call explicitly selects any child LLM value. + * @param request - Model-facing route fields from the tool call. + * @returns Whether at least one route or effort field is present. + */ +export function hasDelegationModelRequest(request: DelegationModelRequest): boolean { + return request.provider !== undefined + || request.model !== undefined + || request.reasoning_effort !== undefined +} + +/** Reject an empty model-facing route value at the tool JSON boundary. */ +function assertNonEmpty(value: string | undefined, field: keyof DelegationModelRequest): void { + if (value !== undefined && value.length === 0) { + throw new Error(`child LLM \`${field}\` must be non-empty`) + } +} + +/** + * Merge model-supplied selection fields over configured child defaults. + * Provider and model form one route and must be supplied together. Changing + * that route without an effort clears the configured route-owned effort. + * @param parentOptions - Current parent values that supply missing child values. + * @param configured - Tool-instance child defaults. + * @param request - Model-facing route override. + * @param enabled - Whether this tool instance permits model-facing selection. + * @returns Child Agent options, preserving omission when no layer contributes one. + */ +export function requestedAgentOptions( + parentOptions: AgentOptions, + configured: AgentOptions | undefined, + request: DelegationModelRequest, + enabled: boolean, +): AgentOptions | undefined { + if (!hasDelegationModelRequest(request)) return configured + if (!enabled) { + throw new Error('child model selection is disabled for this tool instance') + } + assertNonEmpty(request.provider, 'provider') + assertNonEmpty(request.model, 'model') + assertNonEmpty(request.reasoning_effort, 'reasoning_effort') + if ((request.provider === undefined) !== (request.model === undefined)) { + throw new Error('child LLM `provider` and `model` must be supplied together') + } + + const baselineProvider = configured?.provider ?? parentOptions.provider + const baselineModel = configured?.model ?? parentOptions.model + const routeChanged = request.provider !== undefined + && (request.provider !== baselineProvider || request.model !== baselineModel) + const { reasoningEffort: _configuredReasoningEffort, ...configuredWithoutReasoning } = configured ?? {} + return { + ...routeChanged && request.reasoning_effort === undefined ? configuredWithoutReasoning : configured, + ...request.provider === undefined ? {} : { provider: request.provider, model: request.model }, + ...request.reasoning_effort === undefined + ? {} + : { reasoningEffort: ReasoningEffortId(request.reasoning_effort) }, + } +} + +/** + * Whether configured Agent options require route validation before delegation. + * @param options - Tool-instance child defaults. + * @returns Whether configured provider, model, or effort values must be resolved. + */ +export function hasConfiguredLlmSelection(options: AgentOptions | undefined): boolean { + return options?.provider !== undefined + || options?.model !== undefined + || options?.reasoningEffort !== undefined +} + +/** + * Resolve an effective child route through its live adapter before the child is + * created. The LLM runtime owns provider lookup, exact-model metadata, effort + * validation, and adapter defaults. + * @param llm - Live LLM runtime. + * @param parentOptions - Current parent values whose compatible fields the child inherits. + * @param requested - Per-child options after request/config merging. + * @param signal - Tool-call cancellation signal. + */ +export async function preflightChildLlmRoute( + llm: LlmRuntime, + parentOptions: AgentOptions, + requested: AgentOptions | undefined, + signal: AbortSignal, +): Promise { + const provider = requested?.provider ?? parentOptions.provider + const model = requested?.model ?? parentOptions.model + if (provider === undefined || model === undefined) { + throw new Error('cannot select child LLM values without an effective provider and model') + } + const routeChanged = provider !== parentOptions.provider || model !== parentOptions.model + const reasoningEffort = requested?.reasoningEffort + ?? (routeChanged ? undefined : parentOptions.reasoningEffort) + await llm.resolveCallConfig({ + provider, + model, + ...reasoningEffort === undefined ? {} : { reasoningEffort }, + }, signal) +} diff --git a/packages/subagent/tool-subagent/tests/harness.ts b/packages/subagent/tool-subagent/tests/harness.ts new file mode 100644 index 0000000000..36ac602c89 --- /dev/null +++ b/packages/subagent/tool-subagent/tests/harness.ts @@ -0,0 +1,57 @@ +import { Context } from '@deepseek-ai/cordis' +import LlmRuntime, { CallId } from '@deepseek-ai/dsh-llm' +import SystemPrompt from '@deepseek-ai/dsh-system-prompt' +import ToolRuntime from '@deepseek-ai/dsh-tools' +import type { Agent } from '@deepseek-ai/dsh-agent' +import SubagentRuntime from '@deepseek-ai/dsh-subagent' +import { Session, SessionId } from '@deepseek-ai/dsh-session' +import * as mock from './scripted-provider.ts' +import * as tool from '../src/index.ts' + +/** Shared non-aborted tool signal for package-local integration tests. */ +export const testToolSignal = new AbortController().signal + +/** Build the minimal parent Agent owned by the package-local scripted provider. */ +export function fakeAgent(id = 'parent-1'): Agent { + const sessionId = SessionId(id) + return { id: sessionId, options: {}, session: Session.create(sessionId) } as unknown as Agent +} + +/** Mount the real tool and service stack around one scripted subagent provider. */ +export async function setup(toolConfig: tool.Config, mockConfig: Partial = {}): Promise { + const ctx = new Context() + await ctx.plugin(LlmRuntime) + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRuntime) + await ctx.plugin(SubagentRuntime) + await mock.mountScriptedProvider(ctx, { name: 'mock', ...mockConfig }) + await ctx.plugin(tool, toolConfig) + return ctx +} + +let callCounter = 0 + +/** Execute the registered subagent tool through the real ToolRuntime pipeline. */ +export function callSubagent( + ctx: Context, + args: unknown, + over: { agent?: Agent | undefined; signal?: AbortSignal } = {}, +) { + // Distinguish "no override" (use a default agent) from an explicit + // `{ agent: undefined }` (test the no-agent path). Under + // exactOptionalPropertyTypes the key is omitted rather than set to undefined. + const agent = 'agent' in over ? over.agent : fakeAgent() + return ctx.tools.execute({ + signal: testToolSignal, + callId: CallId(`call-${++callCounter}`), + name: 'subagent', + arguments: args, + ...agent ? { agent } : {}, + ...over.signal ? { signal: over.signal } : {}, + }) +} + +/** Join text blocks from one rendered tool result. */ +export function text(result: { content: { type: string; text?: string }[] }): string { + return result.content.filter(block => block.type === 'text').map(block => block.text).join('') +} diff --git a/packages/subagent/tool-subagent/tests/list-models.spec.ts b/packages/subagent/tool-subagent/tests/list-models.spec.ts new file mode 100644 index 0000000000..de3643fd97 --- /dev/null +++ b/packages/subagent/tool-subagent/tests/list-models.spec.ts @@ -0,0 +1,191 @@ +import { describe, expect, it } from 'vitest' +import { Context } from '@deepseek-ai/cordis' +import LlmRuntime, { + CallId, + LlmAdapter, + ReasoningEffortId, +} from '@deepseek-ai/dsh-llm' +import type { + GenerateOptions, + LlmModelInfo, + LlmResolvedModelInfo, + StreamChunk, +} from '@deepseek-ai/dsh-llm' +import ToolRuntime from '@deepseek-ai/dsh-tools' +import SystemPrompt from '@deepseek-ai/dsh-system-prompt' +import SubagentRuntime from '@deepseek-ai/dsh-subagent' +import * as tool from '../src/index.ts' +import { testToolSignal, text } from './harness.ts' + +class CatalogAdapter extends LlmAdapter { + constructor(private readonly empty = false) { + super() + } + + override providerInfo(provider: string) { + return { id: provider, name: `${provider.toUpperCase()} API` } + } + + override listModels(provider: string): Promise { + if (this.empty) return Promise.resolve([]) + return Promise.resolve([ + { provider, id: 'fast', name: 'Fast', description: 'Focused work.' }, + { provider, id: 'plain', name: 'Plain' }, + ]) + } + + override resolveModel(provider: string, model: string): Promise { + if (model === 'plain') return Promise.resolve({ provider, id: model, name: 'Plain' }) + return Promise.resolve({ + provider, + id: model, + name: 'Fast', + description: 'Focused work.', + reasoning: { + efforts: [ + { id: ReasoningEffortId('low'), name: 'Low' }, + { id: ReasoningEffortId('high'), name: 'High', description: 'Quality first.' }, + ], + defaultEffort: ReasoningEffortId('high'), + }, + }) + } + + stream(_options: GenerateOptions): AsyncIterable { + return (async function* () { yield { type: 'finish' as const, reason: { kind: 'stop' as const } } })() + } +} + +async function setupListTool() { + const ctx = new Context() + await ctx.plugin(LlmRuntime) + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRuntime) + await ctx.plugin(SubagentRuntime) + const fiber = await ctx.plugin(tool, { provider: 'unused', enableModelSelection: true }) + return { ctx, fiber } +} + +let counter = 0 + +function call(ctx: Context, args: unknown) { + return ctx.tools.execute({ + signal: testToolSignal, + callId: CallId(`list-models-${++counter}`), + name: 'list_subagent_models', + arguments: args, + }) +} + +describe('list_subagent_models', () => { + it('is omitted unless its delegation-tool instance owns discovery', async () => { + const ctx = new Context() + await ctx.plugin(LlmRuntime) + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRuntime) + await ctx.plugin(SubagentRuntime) + await ctx.plugin(tool, { provider: 'unused' }) + expect(ctx.tools.get('list_subagent_models')).toBeUndefined() + }) + + it('stays registered without the optional LLM service and rejects discovery calls', async () => { + const ctx = new Context() + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRuntime) + await ctx.plugin(SubagentRuntime) + await ctx.plugin(tool, { provider: 'unused', enableModelSelection: true }) + const result = await call(ctx, {}) + expect(result.isError).toBe(true) + expect(text(result)).toContain('`llm` service is unavailable') + }) + + it('rejects two discovery-owning instances in one tool scope', async () => { + const { ctx } = await setupListTool() + await expect(ctx.plugin(tool, { + provider: 'another-unused', + toolName: 'subagent_other', + enableModelSelection: true, + }).then(() => undefined)).rejects.toThrow('tool "list_subagent_models" is already registered') + }) + + it('lists registered providers and follows live registration changes', async () => { + const { ctx, fiber } = await setupListTool() + const empty = await call(ctx, {}) + expect(empty.isError).toBe(false) + expect(text(empty)).toBe('(no LLM providers)') + + const registration = ctx.llm.registerAdapter(['alpha'], new CatalogAdapter()) + const providers = await call(ctx, {}) + expect(providers.isError).toBe(false) + expect(text(providers)).toBe('alpha — ALPHA API') + + registration.replace(['beta']) + const changed = await call(ctx, {}) + expect(text(changed)).toBe('beta — BETA API') + + await fiber.dispose() + expect(ctx.tools.get('list_subagent_models')).toBeUndefined() + }) + + it('lists one provider\'s advertised models without treating the catalog as a whitelist', async () => { + const { ctx } = await setupListTool() + ctx.llm.registerAdapter(['alpha'], new CatalogAdapter()) + const result = await call(ctx, { provider: 'alpha' }) + expect(result.isError).toBe(false) + expect(text(result)).toBe('alpha/fast — Fast: Focused work.\nalpha/plain — Plain') + }) + + it('renders an empty advertised model list', async () => { + const { ctx } = await setupListTool() + ctx.llm.registerAdapter(['alpha'], new CatalogAdapter(true)) + const result = await call(ctx, { provider: 'alpha' }) + expect(result.isError).toBe(false) + expect(text(result)).toBe('(no advertised models for alpha)') + }) + + it('inspects exact-model efforts, descriptions, and defaults', async () => { + const { ctx } = await setupListTool() + ctx.llm.registerAdapter(['alpha'], new CatalogAdapter()) + const result = await call(ctx, { provider: 'alpha', model: 'fast' }) + expect(result.isError).toBe(false) + expect(text(result)).toBe( + 'alpha/fast — Fast: Focused work.\nReasoning efforts:\n' + + 'low — Low\nhigh (default) — High: Quality first.', + ) + }) + + it('renders exact models without reasoning metadata', async () => { + const { ctx } = await setupListTool() + ctx.llm.registerAdapter(['alpha'], new CatalogAdapter()) + const result = await call(ctx, { provider: 'alpha', model: 'plain' }) + expect(result.isError).toBe(false) + expect(text(result)).toBe('alpha/plain — Plain\nReasoning efforts:\n(no advertised reasoning efforts)') + }) + + it.each([ + { args: { model: 'fast' }, expected: '`model` requires `provider`' }, + { args: { provider: '' }, expected: '`provider` must be non-empty' }, + { args: { provider: 'missing' }, expected: 'available providers: (none)' }, + ])('rejects incomplete or unavailable provider requests', async ({ args, expected }) => { + const { ctx } = await setupListTool() + const result = await call(ctx, args) + expect(result.isError).toBe(true) + expect(text(result)).toContain(expected) + }) + + it('rejects an empty exact model after resolving the provider', async () => { + const { ctx } = await setupListTool() + ctx.llm.registerAdapter(['alpha'], new CatalogAdapter()) + const result = await call(ctx, { provider: 'alpha', model: '' }) + expect(result.isError).toBe(true) + expect(text(result)).toContain('`model` must be non-empty') + }) + + it('reports registered alternatives for an unavailable provider', async () => { + const { ctx } = await setupListTool() + ctx.llm.registerAdapter(['alpha'], new CatalogAdapter()) + const result = await call(ctx, { provider: 'missing' }) + expect(result.isError).toBe(true) + expect(text(result)).toContain('available providers: alpha') + }) +}) diff --git a/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts b/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts new file mode 100644 index 0000000000..4f95db088f --- /dev/null +++ b/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts @@ -0,0 +1,248 @@ +/** Default-off settings and per-session model-selection decisions. */ + +import { describe, expect, it, vi } from 'vitest' +import { Context } from '@deepseek-ai/cordis' +import { Session, SessionId } from '@deepseek-ai/dsh-session' +import type { SessionEvent } from '@deepseek-ai/dsh-session' +import { bindScopeParent, createScope, scopeOf, scopeTarget } from '@deepseek-ai/dsh-scope' +import { SettingsProvider } from '@deepseek-ai/dsh-settings' +import type { SettingsNamespace } from '@deepseek-ai/dsh-settings' +import InvariantRegistry from '@deepseek-ai/dsh-invariants' +import AgentLoop from '@deepseek-ai/dsh-agent-loop' +import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' +import SubagentRuntime from '@deepseek-ai/dsh-subagent' +import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process' +import * as tool from '../src/index.ts' +import * as ToolInvariant from '../src/invariant.ts' +import SubagentModelSelectionConfig, { + SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, +} from '../src/model-selection-settings.ts' +import { hasSubagentModelSelection } from '../src/model-selection-state.ts' + +/** Writable in-memory settings provider for the package integration. */ +class MemorySettings extends SettingsProvider { + doc: Record = {} + + get writable(): boolean { + return true + } + + protected load(): Promise> { + return Promise.resolve(structuredClone(this.doc)) + } + + protected persist(ns: SettingsNamespace, section: Record): Promise { + this.doc = { ...this.doc, [ns]: structuredClone(section) } + return Promise.resolve() + } +} + +/** Read whether one Agent's delegation definition contains route fields. */ +function selectable(ctx: Context, agent: Awaited>['agent']): boolean { + const schema = ctx.tools.schemas(agent).find(candidate => candidate.name === 'subagent') + const properties = (schema?.parameters as { properties?: Record } | undefined)?.properties + return properties?.['provider'] !== undefined + && properties['model'] !== undefined + && properties['reasoning_effort'] !== undefined + && ctx.tools.schemas(agent).some(candidate => candidate.name === 'list_subagent_models') +} + +/** Mount the real settings, Agent, provider, and tool services. */ +async function boot(): Promise { + const ctx = new Context() + await ctx.plugin(MemorySettings) + await ctx.plugin(SubagentModelSelectionConfig) + await mountAgentLoopTestDependencies(ctx) + await ctx.plugin(AgentLoop, { agents: [] }) + await ctx.plugin(SubagentRuntime) + await ctx.plugin(SubagentSpawn, { providerName: 'spawn' }) + return ctx +} + +/** Create one Agent whose setup mounts the settings-controlled tool preset row. */ +async function createAgent(ctx: Context, id: string, options: { + meta?: { parentSession: SessionId; origin: 'subagent' } + seed?: readonly SessionEvent[] +} = {}) { + const handle = await ctx.agents.create({ + sessionId: SessionId(id), + ...options, + setup: async (agentCtx) => { + await agentCtx.plugin(tool, { + provider: 'spawn', + modelSelectionSettings: true, + backgroundMode: 'continuable', + }) + }, + }) + return handle.agent +} + +describe('SubagentModelSelectionConfig', () => { + it('uses the composed default without a settings provider', async () => { + const ctx = new Context() + await ctx.plugin(SubagentModelSelectionConfig, { enabled: true }) + + expect(ctx.subagentModelSelection.currentEnabled()).toBe(true) + await ctx.fiber.dispose() + }) + + it('defaults off and follows the validated user layer', async () => { + const ctx = new Context() + await ctx.plugin(MemorySettings) + await ctx.plugin(SubagentModelSelectionConfig) + + expect(ctx.subagentModelSelection.currentEnabled()).toBe(false) + await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, { enabled: true }) + expect(ctx.subagentModelSelection.currentEnabled()).toBe(true) + await ctx.fiber.dispose() + }) + + it('samples each new root session without changing existing Agents', async () => { + const ctx = await boot() + const disabled = await createAgent(ctx, 'disabled') + expect(selectable(ctx, disabled)).toBe(false) + expect(hasSubagentModelSelection(disabled.session)).toBe(false) + + await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, { enabled: true }) + const enabled = await createAgent(ctx, 'enabled') + expect(hasSubagentModelSelection(enabled.session)).toBe(true) + expect(selectable(ctx, enabled)).toBe(true) + expect(selectable(ctx, disabled)).toBe(false) + + await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, { enabled: false }) + const disabledAgain = await createAgent(ctx, 'disabled-again') + expect(selectable(ctx, disabledAgain)).toBe(false) + expect(selectable(ctx, enabled)).toBe(true) + await ctx.fiber.dispose() + }) + + it('installs per-Agent definitions for a shared preset scope', async () => { + const ctx = await boot() + const preset = createScope(ctx, { preset: 'standard' }) + const other = createScope(ctx, { preset: 'minimal' }) + await preset.ctx.plugin(tool, { + provider: 'spawn', + modelSelectionSettings: true, + backgroundMode: 'continuable', + }) + + let enabledBinding: ReturnType | undefined + const createComposed = async (id: string) => ctx.agents.create({ + sessionId: SessionId(id), + setup: (agentCtx) => { + const binding = bindScopeParent(scopeOf(agentCtx)!, scopeOf(preset.ctx)!) + if (id === 'preset-enabled') enabledBinding = binding + }, + }) + + const disabled = await createComposed('preset-disabled') + expect(selectable(ctx, disabled.agent)).toBe(false) + await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, { enabled: true }) + const enabled = await createComposed('preset-enabled') + expect(selectable(ctx, enabled.agent)).toBe(true) + expect(selectable(ctx, disabled.agent)).toBe(false) + + enabledBinding!.rebind(scopeOf(other.ctx)!) + ctx.emit(scopeTarget({}, scopeOf(preset.ctx)), 'tools/change') + await vi.waitFor(() => { expect(selectable(ctx, enabled.agent)).toBe(false) }) + enabledBinding!.rebind(scopeOf(preset.ctx)!) + ctx.emit(scopeTarget({}, scopeOf(preset.ctx)), 'tools/change') + await vi.waitFor(() => { expect(selectable(ctx, enabled.agent)).toBe(true) }) + + await enabled.dispose() + ctx.emit(scopeTarget({}, scopeOf(preset.ctx)), 'tools/change') + await disabled.dispose() + await ctx.fiber.dispose() + }) + + it('inherits the parent decision and preserves seeded decisions across composition', async () => { + const ctx = await boot() + await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, { enabled: true }) + const parent = await createAgent(ctx, 'parent') + await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, { enabled: false }) + const child = await createAgent(ctx, 'child', { + meta: { parentSession: parent.id, origin: 'subagent' }, + }) + expect(selectable(ctx, child)).toBe(true) + expect(hasSubagentModelSelection(child.session)).toBe(true) + + const enabledSeed = Session.create(SessionId('enabled-seed')) + enabledSeed.append('subagent/model-selection-enabled', {}) + const resumedEnabled = await createAgent(ctx, 'resumed-enabled', { seed: enabledSeed.events }) + expect(selectable(ctx, resumedEnabled)).toBe(true) + + const oldSeed = Session.create(SessionId('old-seed'), []) + await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, { enabled: true }) + const resumedDisabled = await createAgent(ctx, 'resumed-disabled', { seed: oldSeed.events }) + expect(selectable(ctx, resumedDisabled)).toBe(false) + expect(hasSubagentModelSelection(resumedDisabled.session)).toBe(false) + await ctx.fiber.dispose() + }) + + it('rejects ambiguous static and settings-controlled configuration', async () => { + const ctx = new Context() + await mountAgentLoopTestDependencies(ctx) + await ctx.plugin(SubagentRuntime) + expect(() => { + tool.apply(ctx, { + provider: 'missing', + enableModelSelection: true, + modelSelectionSettings: true, + }) + }).toThrow('mutually exclusive') + await ctx.fiber.dispose() + }) + + it('requires both the Host setting owner and a composition scope', async () => { + const withoutSettings = new Context() + await mountAgentLoopTestDependencies(withoutSettings) + await withoutSettings.plugin(SubagentRuntime) + expect(() => { + tool.apply(withoutSettings, { + provider: 'missing', + modelSelectionSettings: true, + maxDepth: 'provider-managed', + }) + }).toThrow('requires @deepseek-ai/dsh-tool-subagent/model-selection-settings') + await withoutSettings.fiber.dispose() + + const withoutAgent = await boot() + expect(() => { + tool.apply(withoutAgent, { + provider: 'spawn', + modelSelectionSettings: true, + backgroundMode: 'continuable', + }) + }).toThrow('requires an Agent or preset scope') + await withoutAgent.fiber.dispose() + }) + + it('checks the durable decision against the published tool definitions', async () => { + const ctx = await boot() + await ctx.plugin(InvariantRegistry, { enabled: true }) + await ctx.plugin(ToolInvariant) + const disabled = await createAgent(ctx, 'invariant-disabled') + const next = () => Promise.resolve({ kind: 'enter' as const, messages: [] }) + const payload = { + agent: disabled, + messages: [], + turn: 1, + step: 1, + signal: new AbortController().signal, + } + await expect(ctx.waterfall(ctx as never, 'agent/pre-step', payload, next)).resolves.toEqual({ + kind: 'enter', messages: [], + }) + + disabled.session.append('subagent/model-selection-enabled', {}) + await expect(ctx.waterfall(ctx as never, 'agent/pre-step', payload, next)) + .rejects.toThrow('must expose route fields and list_subagent_models') + + await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, { enabled: true }) + const enabled = await createAgent(ctx, 'invariant-enabled') + await expect(ctx.waterfall(ctx as never, 'agent/pre-step', { ...payload, agent: enabled }, next)) + .resolves.toEqual({ kind: 'enter', messages: [] }) + await ctx.fiber.dispose() + }) +}) diff --git a/packages/subagent/tool-subagent/tests/model-selection.spec.ts b/packages/subagent/tool-subagent/tests/model-selection.spec.ts new file mode 100644 index 0000000000..008ba66da4 --- /dev/null +++ b/packages/subagent/tool-subagent/tests/model-selection.spec.ts @@ -0,0 +1,365 @@ +import { describe, expect, it, vi } from 'vitest' +import { Context } from '@deepseek-ai/cordis' +import { ReasoningEffortId } from '@deepseek-ai/dsh-llm' +import SystemPrompt from '@deepseek-ai/dsh-system-prompt' +import ToolRuntime from '@deepseek-ai/dsh-tools' +import type { Agent } from '@deepseek-ai/dsh-agent' +import SubagentRuntime from '@deepseek-ai/dsh-subagent' +import type { SubagentStartRequest } from '@deepseek-ai/dsh-subagent' +import { Session, SessionId } from '@deepseek-ai/dsh-session' +import { MockAdapter } from '../../../core/agent-loop/tests/mock-adapter.ts' +import * as mock from './scripted-provider.ts' +import * as tool from '../src/index.ts' +import { callSubagent, setup, text } from './harness.ts' + +const REASONING = { + efforts: [ + { id: ReasoningEffortId('low'), name: 'Low' }, + { id: ReasoningEffortId('high'), name: 'High' }, + ], + defaultEffort: ReasoningEffortId('high'), +} as const + +function parentWithRoute( + options: Agent['options'] = { + provider: 'alpha', + model: 'parent-model', + reasoningEffort: ReasoningEffortId('high'), + }, +): Agent { + const id = SessionId('parent-with-route') + return { id, options, session: Session.create(id) } as unknown as Agent +} + +describe('dsh-tool-subagent model selection', () => { + it('exposes static route fields and discovery when selection is enabled', async () => { + const ctx = await setup({ provider: 'mock', enableModelSelection: true }) + const schema = ctx.tools.schemas().find(entry => entry.name === 'subagent')! + const props = (schema.parameters as { properties?: Record }).properties ?? {} + expect(Object.keys(props).sort()).toEqual([ + 'description', + 'model', + 'prompt', + 'provider', + 'reasoning_effort', + 'run_in_background', + ]) + expect(schema.description).toContain('list_subagent_models') + expect(ctx.tools.get('list_subagent_models')).toBeDefined() + expect(schema.description).not.toContain('alpha') + + const registration = ctx.llm.registerAdapter(['alpha'], new MockAdapter([])) + const definition = ctx.tools.get('subagent') + registration.replace(['beta']) + expect(ctx.tools.get('subagent')).toBe(definition) + expect(definition?.description).not.toContain('beta') + }) + + it('hides and rejects route fields when selection is disabled', async () => { + const ctx = await setup({ provider: 'mock' }) + const schema = ctx.tools.schemas().find(entry => entry.name === 'subagent')! + const props = (schema.parameters as { properties?: Record }).properties ?? {} + expect(Object.keys(props).sort()).toEqual(['description', 'prompt', 'run_in_background']) + expect(schema.description).not.toContain('list_subagent_models') + expect(ctx.tools.get('list_subagent_models')).toBeUndefined() + + const result = await callSubagent(ctx, { + description: 'forced route', + prompt: 'do it', + provider: 'alpha', + model: 'fast-model', + }) + expect(result.isError).toBe(true) + expect(text(result)).toContain('child model selection is disabled for this tool instance') + }) + + it('rejects enabled model selection when the provider cannot apply Agent options', async () => { + await expect(setup( + { provider: 'mock', enableModelSelection: true, maxDepth: 'provider-managed' }, + { capabilities: { agentOptions: false } }, + )).rejects.toThrow('provider "mock" does not support child model selection') + }) + + it('selects an unlisted complete route and clears a configured effort when the route changes', async () => { + const requests: SubagentStartRequest[] = [] + const ctx = await setup({ + provider: 'mock', + enableModelSelection: true, + agentOptions: { + provider: 'alpha', + model: 'configured-model', + reasoningEffort: ReasoningEffortId('high'), + maxTokens: 321, + }, + }, { onStart: (request) => { requests.push(request) } }) + ctx.llm.registerAdapter(['alpha'], new MockAdapter([], REASONING)) + + const selected = await callSubagent(ctx, { + description: 'route work', + prompt: 'do it', + provider: 'alpha', + model: 'unlisted-model', + }) + expect(selected.isError).toBe(false) + expect(requests[0]?.agentOptions).toEqual({ + provider: 'alpha', + model: 'unlisted-model', + maxTokens: 321, + }) + + const effort = await callSubagent(ctx, { + description: 'same route effort', + prompt: 'do it', + provider: 'alpha', + model: 'configured-model', + reasoning_effort: 'low', + }) + expect(effort.isError).toBe(false) + expect(requests[1]?.agentOptions).toEqual({ + provider: 'alpha', + model: 'configured-model', + reasoningEffort: 'low', + maxTokens: 321, + }) + }) + + it('accepts an effort-only override for the effective configured or parent route', async () => { + const requests: SubagentStartRequest[] = [] + const ctx = await setup({ + provider: 'mock', + enableModelSelection: true, + agentOptions: { provider: 'alpha' }, + }, { onStart: (request) => { requests.push(request) } }) + ctx.llm.registerAdapter(['alpha'], new MockAdapter([], REASONING)) + + const result = await callSubagent(ctx, { + description: 'effort work', + prompt: 'do it', + reasoning_effort: 'low', + }, { agent: parentWithRoute() }) + expect(result.isError).toBe(false) + expect(requests[0]?.agentOptions).toEqual({ provider: 'alpha', reasoningEffort: 'low' }) + + const inherited = await setup({ provider: 'mock', enableModelSelection: true }) + inherited.llm.registerAdapter(['alpha'], new MockAdapter([], REASONING)) + const inheritedResult = await callSubagent(inherited, { + description: 'parent effort work', + prompt: 'do it', + reasoning_effort: 'low', + }, { agent: parentWithRoute() }) + expect(inheritedResult.isError).toBe(false) + }) + + it('inherits a parent effort only when an explicit route stays unchanged', async () => { + const ctx = await setup({ provider: 'mock', enableModelSelection: true }) + ctx.llm.registerAdapter(['alpha'], new MockAdapter([], REASONING)) + const result = await callSubagent(ctx, { + description: 'same route work', + prompt: 'do it', + provider: 'alpha', + model: 'parent-model', + }, { agent: parentWithRoute() }) + expect(result.isError).toBe(false) + }) + + it('compares explicit routes with the latest logged parent selection', async () => { + const requests: SubagentStartRequest[] = [] + const ctx = await setup({ + provider: 'mock', + enableModelSelection: true, + agentOptions: { reasoningEffort: ReasoningEffortId('high') }, + }, { onStart: (request) => { requests.push(request) } }) + ctx.llm.registerAdapter(['current-provider'], new MockAdapter([], REASONING)) + const parent = parentWithRoute({ provider: 'created-provider', model: 'created-model' }) + parent.session.append('request/header', { + header: { config: { provider: 'current-provider', model: 'current-model' } }, + reason: 'initial', + }) + + const result = await callSubagent(ctx, { + description: 'same current route', + prompt: 'do it', + provider: 'current-provider', + model: 'current-model', + }, { agent: parent }) + + expect(result.isError).toBe(false) + expect(requests[0]?.agentOptions).toEqual({ + provider: 'current-provider', + model: 'current-model', + reasoningEffort: 'high', + }) + }) + + it('rejects an effort without any effective route', async () => { + const ctx = await setup({ provider: 'mock', enableModelSelection: true }) + const result = await callSubagent(ctx, { + description: 'missing route', + prompt: 'do it', + reasoning_effort: 'low', + }) + expect(result.isError).toBe(true) + expect(text(result)).toContain('without an effective provider and model') + }) + + it.each([ + { provider: 'alpha' }, + { model: 'fast-model' }, + ])('rejects a partial model-facing route before child creation', async (route) => { + let starts = 0 + const ctx = await setup({ provider: 'mock', enableModelSelection: true }, { onStart: () => { starts += 1 } }) + const result = await callSubagent(ctx, { description: 'partial route', prompt: 'do it', ...route }) + expect(result.isError).toBe(true) + expect(text(result)).toContain('`provider` and `model` must be supplied together') + expect(starts).toBe(0) + }) + + it.each([ + { provider: '', model: 'fast-model', expected: '`provider` must be non-empty' }, + { provider: 'alpha', model: '', expected: '`model` must be non-empty' }, + { reasoning_effort: '', expected: '`reasoning_effort` must be non-empty' }, + ])('rejects empty model-facing values', async ({ expected, ...selection }) => { + const ctx = await setup({ provider: 'mock', enableModelSelection: true }) + const result = await callSubagent(ctx, { description: 'empty route', prompt: 'do it', ...selection }) + expect(result.isError).toBe(true) + expect(text(result)).toContain(expected) + }) + + it('uses the LLM runtime for provider and reasoning-effort validation before child creation', async () => { + let starts = 0 + const ctx = await setup({ provider: 'mock', enableModelSelection: true }, { onStart: () => { starts += 1 } }) + ctx.llm.registerAdapter(['alpha'], new MockAdapter([], REASONING)) + + const unsupported = await callSubagent(ctx, { + description: 'bad effort', + prompt: 'do it', + provider: 'alpha', + model: 'fast-model', + reasoning_effort: 'max', + }) + expect(unsupported.isError).toBe(true) + expect(text(unsupported)).toContain('does not support reasoning effort "max"') + + const missing = await callSubagent(ctx, { + description: 'bad provider', + prompt: 'do it', + provider: 'missing', + model: 'fast-model', + }) + expect(missing.isError).toBe(true) + expect(text(missing)).toContain('no adapter registered for provider "missing"') + expect(starts).toBe(0) + }) + + it('validates a configured effort before child creation', async () => { + let starts = 0 + const ctx = await setup({ + provider: 'mock', + agentOptions: { + provider: 'alpha', + model: 'parent-model', + reasoningEffort: ReasoningEffortId('high'), + }, + }, { onStart: () => { starts += 1 } }) + ctx.llm.registerAdapter(['alpha'], new MockAdapter([], { + efforts: [{ id: ReasoningEffortId('low'), name: 'Low' }], + defaultEffort: ReasoningEffortId('low'), + })) + + const result = await callSubagent( + ctx, + { description: 'same route', prompt: 'do it' }, + { agent: parentWithRoute() }, + ) + expect(result.isError).toBe(true) + expect(text(result)).toContain('does not support reasoning effort "high"') + expect(starts).toBe(0) + }) + + it('validates a configured route before child creation', async () => { + let starts = 0 + const ctx = await setup({ + provider: 'mock', + agentOptions: { provider: 'missing', model: 'configured-model' }, + }, { onStart: () => { starts += 1 } }) + + const result = await callSubagent( + ctx, + { description: 'configured route', prompt: 'do it' }, + { agent: parentWithRoute() }, + ) + + expect(result.isError).toBe(true) + expect(text(result)).toContain('no adapter registered for provider "missing"') + expect(starts).toBe(0) + }) + + it('rejects selected routes or configured efforts when the LLM service is absent', async () => { + const ctx = new Context() + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRuntime) + await ctx.plugin(SubagentRuntime) + await mock.mountScriptedProvider(ctx, { name: 'mock' }) + await ctx.plugin(tool, { + provider: 'mock', + enableModelSelection: true, + agentOptions: { + provider: 'alpha', + model: 'fast-model', + reasoningEffort: ReasoningEffortId('high'), + }, + }) + + const configured = await callSubagent(ctx, { description: 'configured effort', prompt: 'do it' }) + expect(configured.isError).toBe(true) + expect(text(configured)).toContain('`llm` service is unavailable') + + const selected = await callSubagent(ctx, { + description: 'selected route', + prompt: 'do it', + provider: 'alpha', + model: 'other-model', + }) + expect(selected.isError).toBe(true) + expect(text(selected)).toContain('`llm` service is unavailable') + }) + + it('keeps pure inherited routing usable without an LLM service lookup', async () => { + let starts = 0 + const ctx = new Context() + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRuntime) + await ctx.plugin(SubagentRuntime) + await mock.mountScriptedProvider(ctx, { name: 'mock', onStart: () => { starts += 1 } }) + await ctx.plugin(tool, { provider: 'mock' }) + + const result = await callSubagent(ctx, { description: 'inherit route', prompt: 'do it' }) + expect(result.isError).toBe(false) + expect(starts).toBe(1) + }) + + it('warns that changing a fork route can lose inherited-prefix reuse', async () => { + const ctx = await setup({ provider: 'mock', enableModelSelection: true }, { inheritsParentContext: true }) + const schema = ctx.tools.schemas().find(entry => entry.name === 'subagent')! + expect(schema.description).toContain('inherits this conversation') + expect(schema.description).toContain('can prevent provider-side reuse of the inherited conversation prefix') + }) + + it('propagates an exact-route resolver failure before child creation', async () => { + let starts = 0 + const ctx = await setup({ provider: 'mock', enableModelSelection: true }, { onStart: () => { starts += 1 } }) + const adapter = new MockAdapter([]) + vi.spyOn(adapter, 'resolveModel').mockRejectedValue(new Error('selected route unavailable')) + ctx.llm.registerAdapter(['alpha'], adapter) + + const result = await callSubagent(ctx, { + description: 'route work', + prompt: 'do it', + provider: 'alpha', + model: 'fast-model', + }) + expect(result.isError).toBe(true) + expect(text(result)).toContain('selected route unavailable') + expect(starts).toBe(0) + }) +}) diff --git a/packages/subagent/tool-subagent/tests/scripted-provider.ts b/packages/subagent/tool-subagent/tests/scripted-provider.ts index c0da403cd4..a946c6f6fe 100644 --- a/packages/subagent/tool-subagent/tests/scripted-provider.ts +++ b/packages/subagent/tool-subagent/tests/scripted-provider.ts @@ -13,6 +13,7 @@ import type { } from '@deepseek-ai/dsh-subagent' const DEFAULT_CAPABILITIES: SubagentCapabilities = { + agentOptions: true, outputSchema: true, depthLimit: true, toolFilter: true, diff --git a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts index 1ee5e40228..334b721461 100644 --- a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts +++ b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts @@ -4,7 +4,7 @@ import { tmpdir } from 'node:os' import path from 'node:path' import { Context } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' -import { CallId } from '@deepseek-ai/dsh-llm' +import LlmRuntime, { CallId } from '@deepseek-ai/dsh-llm' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools' import { assembleContextFor, type Agent } from '@deepseek-ai/dsh-agent' @@ -20,9 +20,8 @@ import * as ToolTasks from '@deepseek-ai/dsh-tool-jobs' import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts' import * as mock from './scripted-provider.ts' import * as tool from '../src/index.ts' -import { SessionId } from '@deepseek-ai/dsh-session' - -const testToolSignal = new AbortController().signal +import { Session, SessionId } from '@deepseek-ai/dsh-session' +import { callSubagent, fakeAgent, setup, testToolSignal, text } from './harness.ts' /** * Drives the REAL plugin body: mounts `dsh-tool-subagent` on a real @@ -32,40 +31,6 @@ const testToolSignal = new AbortController().signal * shipping code path. */ -/** A minimal parent Agent passed through to the provider request. */ -function fakeAgent(id = 'parent-1'): Agent { - return { id: SessionId(id) } as unknown as Agent -} - -async function setup(toolConfig: tool.Config, mockConfig: Partial = {}) { - const ctx = new Context() - await ctx.plugin(SystemPrompt) - await ctx.plugin(ToolRuntime) - await ctx.plugin(SubagentRuntime) - await mock.mountScriptedProvider(ctx, { name: 'mock', ...mockConfig }) - await ctx.plugin(tool, toolConfig) - return ctx -} - -let callCounter = 0 -function callSubagent(ctx: Context, args: unknown, over: { agent?: Agent | undefined; signal?: AbortSignal } = {}) { - // Distinguish "no override" (use a default agent) from an explicit - // `{ agent: undefined }` (test the no-agent path). Under - // exactOptionalPropertyTypes the key is omitted rather than set to undefined. - const agent = 'agent' in over ? over.agent : fakeAgent() - return ctx.tools.execute({ - signal: testToolSignal, - callId: CallId(`call-${++callCounter}`), - name: 'subagent', - arguments: args, - ...agent ? { agent } : {}, - ...over.signal ? { signal: over.signal } : {}, - }) -} - -function text(result: { content: { type: string; text?: string }[] }): string { - return result.content.filter(b => b.type === 'text').map(b => b.text).join('') -} describe('dsh-tool-subagent', () => { it('rejects continuable background policy when the provider cannot prepare continuable children', async () => { @@ -83,6 +48,13 @@ describe('dsh-tool-subagent', () => { ) }) + it('rejects configured child agent options at mount when the provider cannot apply them', async () => { + await expect(setup( + { provider: 'mock', maxDepth: 'provider-managed', agentOptions: { model: 'configured-model' } }, + { capabilities: { agentOptions: false } }, + )).rejects.toThrow('does not support child agentOptions') + }) + it('registers a `subagent` tool that delegates to the configured provider and returns its output', async () => { const ctx = await setup({ provider: 'mock' }, { reply: 'child says hi' }) const result = await callSubagent(ctx, { @@ -100,20 +72,14 @@ describe('dsh-tool-subagent', () => { expect(text(result)).toBe('child says hi') }) - it('exposes description + prompt + run_in_background to the model (no provider/type parameter)', async () => { - const ctx = await setup({ provider: 'mock' }) - const schema = ctx.tools.schemas().find(s => s.name === 'subagent') - expect(schema).toBeDefined() - const props = (schema!.parameters as { properties?: Record }).properties ?? {} - expect(Object.keys(props).sort()).toEqual(['description', 'prompt', 'run_in_background']) - expect(schema!.description).toContain('job_output') - }) - it('omits run_in_background entirely when the instance disables it (schema and capability never disagree)', async () => { const ctx = await setup({ provider: 'mock', enableRunInBackground: false }) const schema = ctx.tools.schemas().find(s => s.name === 'subagent') const props = (schema!.parameters as { properties?: Record }).properties ?? {} - expect(Object.keys(props).sort()).toEqual(['description', 'prompt']) + expect(Object.keys(props).sort()).toEqual([ + 'description', + 'prompt', + ]) expect(schema!.description).not.toContain('job_output') }) @@ -121,7 +87,13 @@ describe('dsh-tool-subagent', () => { // Schema omission is advertising, not enforcement: the arg validator // allows undeclared keys, so the opt-out must also hold in execute(). const ctx = await setup({ provider: 'mock', enableRunInBackground: false }) - const parent = { id: SessionId('sess-off'), inject: () => {}, options: {}, session: { header: { version: 0, id: 'sess-off', createdAt: 0 } } } as unknown as Agent + const parentId = SessionId('sess-off') + const parent = { + id: parentId, + inject: () => {}, + options: {}, + session: Session.create(parentId), + } as unknown as Agent const forced = await callSubagent(ctx, { description: 'd', prompt: 'p', run_in_background: true }, { agent: parent }) expect(forced.isError).toBe(true) @@ -232,7 +204,7 @@ describe('dsh-tool-subagent', () => { await ctx.plugin(SubagentRuntime) ctx.subagents.registerProvider({ name: 'weird', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, start: async () => ({ id: SessionId('weird-child'), @@ -253,12 +225,13 @@ describe('dsh-tool-subagent', () => { // the request lets us assert the agentOptions reached it. let seen: { agentOptions?: { model?: string } } | undefined const ctx = new Context() + await ctx.plugin(LlmRuntime) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) await ctx.plugin(SubagentRuntime) ctx.subagents.registerProvider({ name: 'capture', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: true, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, start: async (request) => { seen = request @@ -270,10 +243,15 @@ describe('dsh-tool-subagent', () => { } }, }) - await ctx.plugin(tool, { provider: 'capture', agentOptions: { model: 'child-model' }, maxDepth: 'provider-managed' }) + ctx.llm.registerAdapter(['alpha'], new MockAdapter([])) + await ctx.plugin(tool, { + provider: 'capture', + agentOptions: { provider: 'alpha', model: 'child-model' }, + maxDepth: 'provider-managed', + }) await callSubagent(ctx, { description: 'd', prompt: 'p' }) - expect(seen?.agentOptions).toEqual({ model: 'child-model' }) + expect(seen?.agentOptions).toEqual({ provider: 'alpha', model: 'child-model' }) }) it('defaults toolName and omits agentOptions when apply() is called directly (schema bypass)', async () => { @@ -288,7 +266,7 @@ describe('dsh-tool-subagent', () => { await ctx.plugin(SubagentRuntime) ctx.subagents.registerProvider({ name: 'bare', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, start: async (request) => { seen = request @@ -378,7 +356,7 @@ describe('dsh-tool-subagent', () => { // the provider survives. ctx.subagents.registerProvider({ name: 'continuable', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, start: async () => { throw new Error('lifecycle test does not start a child') }, prepareContinuable: async () => ({}), @@ -429,10 +407,14 @@ describe('dsh-tool-subagent', () => { }) it('derives inherited-context wording from a seeded-conversation provider', async () => { - const ctx = await setup({ provider: 'mock', toolName: 'subagent' }, { inheritsParentContext: true }) + const ctx = await setup({ + provider: 'mock', + toolName: 'subagent', + }, { inheritsParentContext: true }) const schema = ctx.tools.schemas().find(s => s.name === 'subagent')! expect(schema.description).toContain('inherits this conversation') expect(schema.description).not.toContain('does not see this conversation') + expect(schema.description).not.toContain('can prevent provider-side reuse of the inherited conversation prefix') const props = (schema.parameters as { properties: Record }).properties expect(props['prompt']!.description).toContain('completed turns') }) @@ -447,7 +429,7 @@ describe('dsh-tool-subagent', () => { await ctx.plugin(SubagentRuntime) ctx.subagents.registerProvider({ name: 'spy', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, start: async () => ({ id: SessionId('spy-child'), @@ -470,7 +452,7 @@ describe('dsh-tool-subagent', () => { await ctx.plugin(SubagentRuntime) ctx.subagents.registerProvider({ name: 'spy', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, start: async () => ({ id: SessionId('spy-child'), @@ -494,7 +476,7 @@ describe('dsh-tool-subagent', () => { await ctx.plugin(SubagentRuntime) ctx.subagents.registerProvider({ name: 'spy', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, start: async () => ({ id: SessionId('spy-child'), @@ -522,7 +504,7 @@ describe('dsh-tool-subagent', () => { await ctx.plugin(SubagentRuntime) ctx.subagents.registerProvider({ name: 'spy', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, start: async () => ({ id: SessionId('spy-child'), @@ -549,7 +531,7 @@ describe('dsh-tool-subagent', () => { await ctx.plugin(SubagentRuntime) ctx.subagents.registerProvider({ name: 'spy', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, start: async (request) => { if (request.signal.aborted) throw new Error('start aborted') @@ -588,7 +570,7 @@ describe('dsh-tool-subagent', () => { await ctx.plugin(SubagentRuntime) ctx.subagents.registerProvider({ name: 'spy', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, start: async (request) => { if (request.signal.aborted) sawAborted() @@ -652,7 +634,7 @@ describe('dsh-tool-subagent', () => { await ctx.plugin(SubagentRuntime) ctx.subagents.registerProvider({ name: 'capture2', - capabilities: { outputSchema: false, depthLimit: true, toolFilter: true, persona: true }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: true, toolFilter: true, persona: true }, inheritsParentContext: false, start: async (request) => { seen = request @@ -709,7 +691,7 @@ describe('dsh-tool-subagent', () => { await ctx.plugin(SubagentRuntime) ctx.subagents.registerProvider({ name: 'capture3', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: true, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: true, persona: false }, inheritsParentContext: false, start: async (request) => { seen = request @@ -739,7 +721,7 @@ describe('dsh-tool-subagent', () => { await ctx.plugin(SubagentRuntime) ctx.subagents.registerProvider({ name: 'capture4', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, start: async (request) => { seen = request @@ -764,7 +746,7 @@ describe('dsh-tool-subagent', () => { await ctx.plugin(SubagentRuntime) ctx.subagents.registerProvider({ name: 'p', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: true, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: true, persona: false }, inheritsParentContext: false, start: () => { throw new Error('unreachable') }, }) @@ -783,7 +765,7 @@ describe('dsh-tool-subagent background mode', () => { ctx: scopeFiber.ctx, inject, options: {}, - session: { id, header: { version: 0, id, createdAt: 0 } }, + session: Session.create(id), } as unknown as Agent ctx.agents.register(agent) return agent @@ -803,7 +785,7 @@ describe('dsh-tool-subagent background mode', () => { let prepareCalls = 0 ctx.subagents.registerProvider({ name: 'resumable', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, start: async request => ({ id: SessionId('one-shot-child'), @@ -839,7 +821,7 @@ describe('dsh-tool-subagent background mode', () => { }) it('returns a job id immediately and the answer is collected through job_output', async () => { - const ctx = await backgroundSetup({ provider: 'mock', agentOptions: { model: 'child-model' } }, { reply: 'background answer' }) + const ctx = await backgroundSetup({ provider: 'mock' }, { reply: 'background answer' }) const parent = ownerAgent(ctx, 'sess-parent') const start = await callSubagent(ctx, { description: 'deep research', prompt: 'dig in', run_in_background: true }, { agent: parent }) @@ -919,12 +901,41 @@ describe('dsh-tool-subagent background mode', () => { expect(text(result)).toBe('Error: tool call aborted before dispatch') }) + it('skips background startup when cancellation wins asynchronous route preflight', async () => { + const ctx = await backgroundSetup({ provider: 'mock', enableModelSelection: true }) + const parent = ownerAgent(ctx, 'sess-parent') + const adapter = new MockAdapter([]) + let releasePreflight!: () => void + const preflightGate = new Promise((resolve) => { releasePreflight = resolve }) + const resolveModel = vi.spyOn(adapter, 'resolveModel').mockImplementation(async (provider, model) => { + await preflightGate + return { provider, id: model, name: model } + }) + ctx.llm.registerAdapter(['alpha'], adapter) + const controller = new AbortController() + + const resultPromise = callSubagent(ctx, { + description: 'cancelled selection', + prompt: 'do it', + provider: 'alpha', + model: 'selected-model', + run_in_background: true, + }, { agent: parent, signal: controller.signal }) + await vi.waitFor(() => { expect(resolveModel).toHaveBeenCalledOnce() }) + controller.abort() + releasePreflight() + const result = await resultPromise + + expect(result.isError).toBe(true) + expect(ctx.jobs.list(parent)).toEqual([]) + }) + it('settles an asynchronous provider-start failure as a failed task', async () => { const ctx = await backgroundSetup({ provider: 'mock' }) const parent = ownerAgent(ctx, 'sess-parent') ctx.subagents.registerProvider({ name: 'broken-start', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, start: async () => { throw new Error('setup failed') }, }) @@ -953,7 +964,7 @@ describe('dsh-tool-subagent background mode', () => { const parent = ownerAgent(ctx, 'sess-parent') ctx.subagents.registerProvider({ name: 'pending-start', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, start: request => new Promise((_resolve, reject) => { request.signal.addEventListener('abort', () => { reject(new Error('startup aborted')) }, { once: true }) @@ -990,7 +1001,7 @@ describe('dsh-tool-subagent background mode', () => { const parent = ownerAgent(ctx, 'sess-parent') ctx.subagents.registerProvider({ name: 'broken-start-rollback', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, start: request => new Promise((_resolve, reject) => { request.signal.addEventListener('abort', () => { @@ -1035,7 +1046,7 @@ describe('dsh-tool-subagent background mode', () => { let starts = 0 ctx.subagents.registerProvider({ name: 'hanging', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, start: async (request) => { let settle!: (value: { output: { type: 'text'; text: string }[]; stopReason: 'aborted' }) => void @@ -1182,7 +1193,7 @@ describe('dsh-tool-subagent continuable background mode', () => { let survivingChildId: ReturnType | undefined ctx.subagents.registerProvider({ name: 'gated', - capabilities: { outputSchema: true, depthLimit: true, toolFilter: true, persona: true }, + capabilities: { agentOptions: false, outputSchema: true, depthLimit: true, toolFilter: true, persona: true }, inheritsParentContext: false, start: async () => { throw new Error('continuable policy must not start a one-shot child') }, prepareContinuable: async (request) => { @@ -1250,14 +1261,14 @@ describe('background preflight failure (no orphaned child, by construction)', () ctx: scopeFiber.ctx, inject: () => {}, options: {}, - session: { id, header: { version: 0, id, createdAt: 0 } }, + session: Session.create(id), } as unknown as Agent ctx.agents.register(parent) let starts = 0 ctx.subagents.registerProvider({ name: 'probe', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, start: async () => { starts += 1 @@ -1295,7 +1306,7 @@ describe('depth budget configuration', () => { await ctx.plugin(SubagentRuntime) ctx.subagents.registerProvider({ name: 'capture', - capabilities: { outputSchema: true, depthLimit: true, toolFilter: true, persona: true }, + capabilities: { agentOptions: false, outputSchema: true, depthLimit: true, toolFilter: true, persona: true }, inheritsParentContext: false, start: async (request) => { requests.push(request) @@ -1333,7 +1344,7 @@ describe('depth budget configuration', () => { await ctx.plugin(SubagentRuntime) ctx.subagents.registerProvider({ name: 'no-depth', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, start: async () => { throw new Error('unreachable') }, }) @@ -1349,7 +1360,7 @@ describe('depth budget configuration', () => { await ctx.plugin(SubagentRuntime) ctx.subagents.registerProvider({ name: 'external', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, start: async (request) => { requests.push(request) diff --git a/packages/subagent/tool-subagent/tsconfig.json b/packages/subagent/tool-subagent/tsconfig.json index 8ee77dfb3b..57ee1dc9d6 100644 --- a/packages/subagent/tool-subagent/tsconfig.json +++ b/packages/subagent/tool-subagent/tsconfig.json @@ -20,6 +20,15 @@ { "path": "../../core/agent" }, + { + "path": "../../core/session" + }, + { + "path": "../../core/scope" + }, + { + "path": "../../settings/settings" + }, { "path": "../../llm/llm" }, diff --git a/packages/subagent/tool-subagent/tsdown.config.ts b/packages/subagent/tool-subagent/tsdown.config.ts new file mode 100644 index 0000000000..d91febcc25 --- /dev/null +++ b/packages/subagent/tool-subagent/tsdown.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'tsdown' + +const entry = (path: string) => ({ + entry: [path], + outDir: 'lib', + format: ['esm'] as const, + platform: 'node' as const, + target: 'es2024' as const, + fixedExtension: false, + dts: false, + clean: false, +}) + +/** Build self-contained Loader entries so the package needs no private chunks. */ +export default defineConfig([ + entry('lib/types/index.js'), + entry('lib/types/model-selection-settings.js'), + entry('lib/types/invariant.js'), +]) diff --git a/packages/workflow/tool-ralph/tests/tool-ralph.spec.ts b/packages/workflow/tool-ralph/tests/tool-ralph.spec.ts index 7c6f02e36c..0ccbbd16c6 100644 --- a/packages/workflow/tool-ralph/tests/tool-ralph.spec.ts +++ b/packages/workflow/tool-ralph/tests/tool-ralph.spec.ts @@ -56,6 +56,7 @@ class StubProvider implements SubagentProvider { constructor(options?: { outputSchema?: boolean; inheritsParentContext?: boolean }) { this.capabilities = { + agentOptions: true, outputSchema: options?.outputSchema ?? true, depthLimit: true, toolFilter: true, diff --git a/packages/workflow/tool-workflow/tests/tool-workflow.spec.ts b/packages/workflow/tool-workflow/tests/tool-workflow.spec.ts index 938f9a5502..f89083faba 100644 --- a/packages/workflow/tool-workflow/tests/tool-workflow.spec.ts +++ b/packages/workflow/tool-workflow/tests/tool-workflow.spec.ts @@ -423,7 +423,7 @@ describe('dsh-tool-workflow', () => { await ctx.plugin(SubagentRuntime) ctx.subagents.registerProvider({ name: 'spawn', - capabilities: { outputSchema: true, depthLimit: true, toolFilter: true, persona: true }, + capabilities: { agentOptions: true, outputSchema: true, depthLimit: true, toolFilter: true, persona: true }, inheritsParentContext: false, start: () => Promise.reject(new Error('the parked-script fixture must not start a child')), }) diff --git a/packages/workflow/workflow-worker-thread/tests/built-worker.e2e.ts b/packages/workflow/workflow-worker-thread/tests/built-worker.e2e.ts index 31998a22cf..cf7439e8eb 100644 --- a/packages/workflow/workflow-worker-thread/tests/built-worker.e2e.ts +++ b/packages/workflow/workflow-worker-thread/tests/built-worker.e2e.ts @@ -30,7 +30,7 @@ await ctx.plugin(SubagentRuntime) let selectedStarts = 0 ctx.subagents.registerProvider({ name: 'built-selected', - capabilities: { outputSchema: true, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { agentOptions: true, outputSchema: true, depthLimit: false, toolFilter: false, persona: false }, inheritsParentContext: false, async start() { selectedStarts += 1 diff --git a/packages/workflow/workflow-worker-thread/tests/source-worker.compat.spec.ts b/packages/workflow/workflow-worker-thread/tests/source-worker.compat.spec.ts index 8d33e373c7..77b87b59f8 100644 --- a/packages/workflow/workflow-worker-thread/tests/source-worker.compat.spec.ts +++ b/packages/workflow/workflow-worker-thread/tests/source-worker.compat.spec.ts @@ -21,7 +21,7 @@ it('runs the default config through the source worker', async () => { const subagents = await ctx.plugin(SubagentRuntime) const provider: SubagentProvider = { name: 'spawn', - capabilities: { outputSchema: true, depthLimit: true, toolFilter: true, persona: true }, + capabilities: { agentOptions: true, outputSchema: true, depthLimit: true, toolFilter: true, persona: true }, inheritsParentContext: false, start: () => Promise.reject(new Error('source-worker compat script must not start a child')), } diff --git a/packages/workflow/workflow-worker-thread/tests/workflow-worker-thread.spec.ts b/packages/workflow/workflow-worker-thread/tests/workflow-worker-thread.spec.ts index 5130071362..935dbae3fb 100644 --- a/packages/workflow/workflow-worker-thread/tests/workflow-worker-thread.spec.ts +++ b/packages/workflow/workflow-worker-thread/tests/workflow-worker-thread.spec.ts @@ -55,7 +55,13 @@ interface ControlledRun { * the request signal fires, like the real in-process backends. */ class StubProvider implements SubagentProvider { - readonly capabilities: SubagentCapabilities = { outputSchema: true, depthLimit: true, toolFilter: true, persona: false } + readonly capabilities: SubagentCapabilities = { + agentOptions: true, + outputSchema: true, + depthLimit: true, + toolFilter: true, + persona: false, + } readonly inheritsParentContext = false readonly runs: ControlledRun[] = [] @@ -459,7 +465,7 @@ describe('dsh-workflow-worker-thread', () => { await ctx.plugin(SubagentRuntime) const provider: SubagentProvider = { name: 'rejecting', - capabilities: { outputSchema: true, depthLimit: true, toolFilter: true, persona: false }, + capabilities: { agentOptions: true, outputSchema: true, depthLimit: true, toolFilter: true, persona: false }, inheritsParentContext: false, start: async () => ({ id: SessionId('reject-child'), @@ -518,7 +524,7 @@ describe('dsh-workflow-worker-thread', () => { await ctx.plugin(SubagentRuntime) const provider: SubagentProvider = { name: 'bad-dispose', - capabilities: { outputSchema: true, depthLimit: true, toolFilter: true, persona: false }, + capabilities: { agentOptions: true, outputSchema: true, depthLimit: true, toolFilter: true, persona: false }, inheritsParentContext: false, start: async () => ({ id: SessionId('bad-dispose-child'), @@ -540,7 +546,7 @@ describe('dsh-workflow-worker-thread', () => { await ctx.plugin(SubagentRuntime) const provider: SubagentProvider = { name: 'coercion-trap-dispose', - capabilities: { outputSchema: true, depthLimit: true, toolFilter: true, persona: false }, + capabilities: { agentOptions: true, outputSchema: true, depthLimit: true, toolFilter: true, persona: false }, inheritsParentContext: false, start: async () => ({ id: SessionId('trap-child'), @@ -891,7 +897,7 @@ describe('dsh-workflow-worker-thread', () => { const aborted: string[] = [] const provider: SubagentProvider = { name: 'signal-only', - capabilities: { outputSchema: true, depthLimit: true, toolFilter: true, persona: false }, + capabilities: { agentOptions: true, outputSchema: true, depthLimit: true, toolFilter: true, persona: false }, inheritsParentContext: false, start: async (request) => { let settle!: (result: SubagentResult) => void @@ -1189,7 +1195,7 @@ describe('dsh-workflow-worker-thread', () => { const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => ctx.logger) const provider: SubagentProvider = { name: 'late-ready', - capabilities: { outputSchema: true, depthLimit: true, toolFilter: true, persona: false }, + capabilities: { agentOptions: true, outputSchema: true, depthLimit: true, toolFilter: true, persona: false }, inheritsParentContext: false, start: (request) => { requested.resolve(request) @@ -1250,7 +1256,7 @@ describe('dsh-workflow-worker-thread', () => { const signalAborts: unknown[] = [] const provider: SubagentProvider = { name: 'doomed', - capabilities: { outputSchema: true, depthLimit: true, toolFilter: true, persona: false }, + capabilities: { agentOptions: true, outputSchema: true, depthLimit: true, toolFilter: true, persona: false }, inheritsParentContext: false, start: async (request) => { request.signal.addEventListener('abort', () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d275e43f00..94020341f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -225,6 +225,9 @@ importers: '@deepseek-ai/dsh-sdk-app': specifier: workspace:^ version: link:../../packages/bundle/sdk-app + '@deepseek-ai/dsh-sdk-minimal': + specifier: workspace:^ + version: link:../../packages/bundle/sdk-minimal '@deepseek-ai/dsh-session-projection': specifier: workspace:^ version: link:../../packages/session/session-projection @@ -1145,9 +1148,15 @@ importers: packages/boot/app-boot: dependencies: + '@deepseek-ai/dsh-atomic-write': + specifier: workspace:^ + version: link:../../util/atomic-write js-yaml: specifier: ^4.2.0 version: 4.2.0 + resolve.exports: + specifier: ^2.0.3 + version: 2.0.3 devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -1518,6 +1527,9 @@ importers: '@deepseek-ai/dsh-sdk-jsonrpc-server': specifier: workspace:^ version: link:../../sdk/server + '@deepseek-ai/schemastery': + specifier: link:../../../vendor/schemastery + version: link:../../../vendor/schemastery commander: specifier: ^15.0.0 version: 15.0.0 @@ -1532,6 +1544,64 @@ importers: specifier: workspace:^ version: link:../../runtime-diagnostics/invariants + packages/bundle/sdk-minimal: + dependencies: + '@deepseek-ai/dsh-agent-spine-demo': + specifier: workspace:^ + version: link:../../examples/agent-spine-demo + '@deepseek-ai/dsh-deepseek-llm-api-extensions': + specifier: workspace:^ + version: link:../../llm/deepseek-llm-api-extensions + '@deepseek-ai/dsh-fs-local': + specifier: workspace:^ + version: link:../../fs/fs-local + '@deepseek-ai/dsh-llm-deepseek': + specifier: workspace:^ + version: link:../../llm/llm-deepseek + '@deepseek-ai/dsh-plugin-package-inventory-deepseek': + specifier: workspace:^ + version: link:../../llm/plugin-package-inventory-deepseek + '@deepseek-ai/dsh-sandbox-local': + specifier: workspace:^ + version: link:../../sandbox/sandbox-local + '@deepseek-ai/dsh-sandbox-policy': + specifier: workspace:^ + version: link:../../sandbox/sandbox-policy + '@deepseek-ai/dsh-sdk-app': + specifier: workspace:^ + version: link:../sdk-app + '@deepseek-ai/dsh-sdk-jsonrpc-server': + specifier: workspace:^ + version: link:../../sdk/server + '@deepseek-ai/dsh-session-log-deepseek': + specifier: workspace:^ + version: link:../../session/session-log-deepseek + '@deepseek-ai/dsh-session-persistence-jsonl': + specifier: workspace:^ + version: link:../../session/session-persistence-jsonl + '@deepseek-ai/dsh-subprocess-local': + specifier: workspace:^ + version: link:../../subprocess/subprocess-local + '@deepseek-ai/dsh-terminal': + specifier: workspace:^ + version: link:../../terminal/terminal + '@deepseek-ai/dsh-terminal-bash': + specifier: workspace:^ + version: link:../../terminal/terminal-bash + '@deepseek-ai/dsh-tool-bash-persistent': + specifier: workspace:^ + version: link:../../shell/tool-bash-persistent + '@deepseek-ai/dsh-tool-str-replace-editor': + specifier: workspace:^ + version: link:../../fs/tool-str-replace-editor + devDependencies: + '@deepseek-ai/cordis': + specifier: workspace:^ + version: link:../../../vendor/cordis + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../runtime-diagnostics/invariants + packages/bundle/web-app: dependencies: '@deepseek-ai/dsh-agent-presets': @@ -1738,6 +1808,9 @@ importers: '@deepseek-ai/dsh-subprocess': specifier: workspace:^ version: link:../../subprocess/subprocess + '@deepseek-ai/dsh-tool-subagent': + specifier: workspace:^ + version: link:../../subagent/tool-subagent '@deepseek-ai/dsh-web-frontend': specifier: workspace:^ version: link:../../../apps/web @@ -6826,19 +6899,6 @@ importers: specifier: workspace:^ version: link:../../subagent/subagent - packages/sdk/python-runtime: - dependencies: - '@deepseek-ai/dsh-app-boot': - specifier: workspace:^ - version: link:../../boot/app-boot - devDependencies: - '@deepseek-ai/cordis': - specifier: workspace:^ - version: link:../../../vendor/cordis - '@deepseek-ai/dsh-invariants': - specifier: workspace:^ - version: link:../../runtime-diagnostics/invariants - packages/sdk/server: dependencies: '@deepseek-ai/schemastery': @@ -8512,6 +8572,9 @@ importers: '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm + '@deepseek-ai/dsh-scope': + specifier: workspace:^ + version: link:../../core/scope '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../core/session @@ -8521,6 +8584,9 @@ importers: '@deepseek-ai/dsh-session-persistence-jsonl': specifier: workspace:^ version: link:../../session/session-persistence-jsonl + '@deepseek-ai/dsh-settings': + specifier: workspace:^ + version: link:../../settings/settings '@deepseek-ai/dsh-subagent': specifier: workspace:^ version: link:../subagent @@ -9624,6 +9690,9 @@ importers: '@deepseek-ai/cosmokit': specifier: link:../../vendor/cosmokit version: link:../../vendor/cosmokit + '@deepseek-ai/dsh': + specifier: workspace:^ + version: link:../../apps/cli '@deepseek-ai/dsh-acp': specifier: workspace:^ version: link:../../packages/acp/acp @@ -9792,9 +9861,6 @@ importers: '@deepseek-ai/dsh-sdk-protocol': specifier: workspace:^ version: link:../../packages/sdk/protocol - '@deepseek-ai/dsh-sdk-python-runtime': - specifier: workspace:^ - version: link:../../packages/sdk/python-runtime '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../packages/core/session @@ -9825,6 +9891,9 @@ importers: '@deepseek-ai/dsh-session-reference': specifier: workspace:^ version: link:../../packages/context/session-reference + '@deepseek-ai/dsh-session-telemetry': + specifier: workspace:^ + version: link:../../packages/session/session-telemetry '@deepseek-ai/dsh-session-title': specifier: workspace:^ version: link:../../packages/session/session-title @@ -14975,6 +15044,10 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + retry@0.13.1: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} @@ -20617,6 +20690,8 @@ snapshots: resolve-pkg-maps@1.0.0: {} + resolve.exports@2.0.3: {} + retry@0.13.1: {} rfdc@1.4.1: {} diff --git a/python/README.i18n.yaml b/python/README.i18n.yaml index 8c5a23e37b..1076f068de 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: 75276a915eb4b63f84e0876de46e6d8d63540b59 -README.zh.md: f41822d66520869e5235a6fedd895b83a52639c9 +README.md: d82195fa8a3f39bea129030b2eaf7d65536041e0 +README.zh.md: cb0fb28fff7d2442c948205b07c457243fb85022 diff --git a/python/README.md b/python/README.md index 75276a915e..d82195fa8a 100644 --- a/python/README.md +++ b/python/README.md @@ -9,11 +9,11 @@ Python packages for driving DeepSeek Harness as a subprocess. The client SDK com | Directory | Dist / module | Role | |---|---|---| | [sdk](sdk/README.md) | `deepseek-harness-sdk` / `deepseek_harness` | High-level turns API and lower-level JSON-RPC client | -| [sdk-runtime](sdk-runtime/README.md) | `deepseek-harness-runtime-bin` / `deepseek_harness_runtime` | Bundled runtime binaries and default agent configuration | +| [sdk-runtime](sdk-runtime/README.md) | `deepseek-harness-runtime-bin` / `deepseek_harness_runtime` | Bundled `dsh` CLI executable and native sidecars | ## Behavior -The SDK starts the matching bundled runtime unless the caller selects an explicit channel. The client selects the channel and supplies default configuration; the runtime itself always requires an explicit configuration. The [SDK reference](sdk/README.md) and [runtime carrier reference](sdk-runtime/README.md) own the complete runtime-selection and configuration contracts. +The SDK starts the matching bundled `dsh --profile sdk` runtime unless the caller selects another `dsh` executable or profile. The runnable minimal example selects the shipped standalone `sdk-minimal` profile; the same runtime also packages `dsh web` and its frontend assets for separate CLI use. Every launch requires an explicitly selected Harness home; Python never silently reads `~/.dsh`. The [SDK reference](sdk/README.md) and [runtime carrier reference](sdk-runtime/README.md) own runtime selection, profiles, patches, and external plugin management. ## Contributor workflows diff --git a/python/README.zh.md b/python/README.zh.md index f41822d665..cb0fb28fff 100644 --- a/python/README.zh.md +++ b/python/README.zh.md @@ -9,11 +9,11 @@ | 目录 | 分发名/模块 | 职责 | |---|---|---| | [sdk](sdk/README.zh.md) | `deepseek-harness-sdk` / `deepseek_harness` | 高层轮次 API 与低层 JSON-RPC 客户端 | -| [sdk-runtime](sdk-runtime/README.zh.md) | `deepseek-harness-runtime-bin` / `deepseek_harness_runtime` | 内置运行时二进制与默认 agent(智能体)配置 | +| [sdk-runtime](sdk-runtime/README.zh.md) | `deepseek-harness-runtime-bin` / `deepseek_harness_runtime` | 内置 `dsh` CLI 可执行程序与原生伴随文件 | ## 行为 -除非调用方选择显式通道,否则 SDK 会启动匹配的内置运行时。客户端选择通道并提供默认配置;运行时本身始终要求显式配置。[SDK 参考](sdk/README.zh.md)和[运行时载体参考](sdk-runtime/README.zh.md)定义完整的运行时选择与配置约定。 +除非调用方选择另一个 `dsh` 可执行程序或 profile,否则 SDK 会启动匹配的内置 `dsh --profile sdk` 运行时。可运行极简示例选择随附的独立 `sdk-minimal` profile;同一运行时还会为独立 CLI 使用打包 `dsh web` 及其前端产物。每次启动都要求显式选择 Harness home;Python 绝不会静默读取 `~/.dsh`。[SDK 参考](sdk/README.zh.md)和[运行时载体参考](sdk-runtime/README.zh.md)定义运行时选择、profile、patch 与外部插件管理约定。 ## 贡献者工作流 diff --git a/python/development.i18n.yaml b/python/development.i18n.yaml index 75c657c153..a372e966ac 100644 --- a/python/development.i18n.yaml +++ b/python/development.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write python/development.md -development.md: e96be7af10e0008cc0fe5dea4ca51529f102fd7b -development.zh.md: e4ca1c980c36fdea381e9b8a6c615276e279b4d2 +development.md: 61094a277d2b91063a0d368ec30f444eeb132128 +development.zh.md: a73de6e091050cebb0b26037a7cca3adc814d961 diff --git a/python/development.md b/python/development.md index e96be7af10..61094a277d 100644 --- a/python/development.md +++ b/python/development.md @@ -31,10 +31,10 @@ That suite drives fake runtime peers. `scripts/smoke-python-runtime.py` drives t ```sh uv run --project python/sdk python scripts/smoke-python-runtime.py \ - --scenario sdk-minimal --exe dist-exe/dsh-jsonrpc-agent-pkg-macos-arm64 + --scenario sdk-minimal --exe dist-exe/deepseek-harness-sdk-runtime-macos-arm64 ``` -Three scenarios compare committed expected output under `scripts/snapshots/python-sdk-single-exe/`. `minimal/model-visible.json` pins the checked-in minimal composition's assembled system prompts, advertised tool schemas, and model-visible messages, so a plugin that contributes an unintended system section or user message fails the job; it drops the dynamic runtime-context snapshot, which the same composition emits on macOS and not on Linux ([#2488](https://github.com/deepseek-harness/deepseek-harness/issues/2488)). `advanced/` pins one complex process's SDK result and parent/child session logs. `restart/` launches two complete SDK runtime processes against one persistence root and snapshots their isolated model histories, high-level results, and separate durable logs. Rerun the owning scenario with `--update-snapshots` and review that diff before committing it. +Three scenarios compare committed expected output under `scripts/snapshots/python-sdk-single-exe/`. `minimal/model-visible.json` pins the shipped `sdk-minimal` profile's assembled system prompts, advertised tool schemas, and model-visible messages, so a plugin that contributes an unintended system section or user message fails the job. `advanced/` pins one complex process's SDK result and parent/child session logs. `restart/` launches two complete SDK runtime processes against one persistence root and snapshots their isolated model histories, high-level results, and separate durable logs. Rerun the owning scenario with `--update-snapshots` and review that diff before committing it. Trusted pull requests also run `--scenario sdk-live --installed-wheel` on every native target. That scenario performs two tool-using turns against `https://api.deepseek.com`, verifies the created file externally, and fails when the repository secret is absent instead of self-skipping. Fork and Dependabot pull requests run the complete keyless installed-wheel path but receive no key. @@ -43,18 +43,20 @@ An interactive smoke test needs `DEEPSEEK_API_KEY` in the environment or reposit ```python from deepseek_harness import DeepSeekHarness -with DeepSeekHarness() as harness: +with DeepSeekHarness(dsh_home="/absolute/path/to/test-dsh-home") as harness: print(harness.run("say hi").final_response) ``` +Alternatively export a non-empty `DSH_HOME`. The SDK rejects a launch that would silently use `~/.dsh`. + ## Run against Node source -Repository contributors can select either development carrier: +Repository contributors can select either development route; both execute the normal `dsh --profile sdk` launcher: - Set `DSH_RUNTIME_MODE=node` to use the built Node carrier on system Node `>=22.19`. The build script refreshes this carrier, but distributions never include or auto-select it. -- Set `launch_args_override=("./node_modules/.bin/tsx", "packages/sdk/python-runtime/src/packaged-bin.ts")` with the repository root as `cwd` to run the private carrier's unbuilt TypeScript source. Supply `cordis=...` when the default configuration is not suitable. +- Set `dsh_bin` to the absolute built `apps/cli/lib/bin.js` path to exercise the checkout's CLI directly. Supply an explicit `dsh_home`, plus `profile` and ordered `patches` as needed. -See `python/sdk/tests/manual_sdk_agent_smoke.py` for a complete source-mode invocation. +`python/sdk/tests/manual_sdk_agent_smoke.py` uses the internal `_launch_args` test adapter to exercise the unbuilt TypeScript CLI under tsx. Arbitrary argv replacement is intentionally absent from the public SDK. ## Build distributions @@ -71,7 +73,7 @@ print(release["pep440_version"](release["repository_version"]())) PY )" python scripts/build-python-release.py --package sdk --output-dir dist-python -python scripts/build-python-release.py --package runtime --platform macos-arm64 --runtime-exe dist-exe/dsh-jsonrpc-agent-pkg-macos-arm64 --output-dir dist-python +python scripts/build-python-release.py --package runtime --platform macos-arm64 --runtime-exe dist-exe/deepseek-harness-sdk-runtime-macos-arm64 --output-dir dist-python pip install \ "dist-python/deepseek_harness_sdk-$version-py3-none-any.whl" \ "dist-python/deepseek_harness_runtime_bin-$version-py3-none-macosx_14_0_arm64.whl" diff --git a/python/development.zh.md b/python/development.zh.md index e4ca1c980c..a73de6e091 100644 --- a/python/development.zh.md +++ b/python/development.zh.md @@ -31,10 +31,10 @@ uv run --project python/sdk pytest ```sh uv run --project python/sdk python scripts/smoke-python-runtime.py \ - --scenario sdk-minimal --exe dist-exe/dsh-jsonrpc-agent-pkg-macos-arm64 + --scenario sdk-minimal --exe dist-exe/deepseek-harness-sdk-runtime-macos-arm64 ``` -其中三个场景会比对 `scripts/snapshots/python-sdk-single-exe/` 下已提交的期望输出。`minimal/model-visible.json` 固定了签入的极简组合所组装的系统提示词、对外公布的工具 schema 以及模型可见消息,因此插件一旦贡献出计划外的系统分段或 user 消息,该任务即失败;它会丢弃动态运行时上下文快照——同一组合在 macOS 上会发出它,在 Linux 上不会([#2488](https://github.com/deepseek-harness/deepseek-harness/issues/2488))。`advanced/` 固定一个复杂进程的 SDK 结果及父/子会话日志。`restart/` 针对同一持久化根目录启动两个完整 SDK 运行时进程,并固定其彼此隔离的模型历史、高层结果与独立持久日志。重新运行对应场景时加上 `--update-snapshots`,并在提交前审阅该差异。 +其中三个场景会比对 `scripts/snapshots/python-sdk-single-exe/` 下已提交的期望输出。`minimal/model-visible.json` 固定随附 `sdk-minimal` profile 所组装的系统提示词、对外公布的工具 schema 与模型可见消息,因此插件一旦贡献出计划外的系统分段或 user 消息,该任务即失败。`advanced/` 固定一个复杂进程的 SDK 结果及父/子会话日志。`restart/` 针对同一持久化根目录启动两个完整 SDK 运行时进程,并固定其彼此隔离的模型历史、高层结果与独立持久日志。重新运行对应场景时加上 `--update-snapshots`,并在提交前审阅该差异。 可信拉取请求还会在每个原生目标上运行 `--scenario sdk-live --installed-wheel`。该场景面向 `https://api.deepseek.com` 执行两个使用工具的轮次,从外部验证已创建文件,并在仓库密钥缺失时失败而不是自行 skip。Fork 与 Dependabot 拉取请求会运行完整的 keyless 安装后 wheel 路径,但不会获得密钥。 @@ -43,18 +43,20 @@ uv run --project python/sdk python scripts/smoke-python-runtime.py \ ```python from deepseek_harness import DeepSeekHarness -with DeepSeekHarness() as harness: +with DeepSeekHarness(dsh_home="/absolute/path/to/test-dsh-home") as harness: print(harness.run("say hi").final_response) ``` +也可以导出非空 `DSH_HOME`。SDK 会拒绝可能静默使用 `~/.dsh` 的启动。 + ## 针对 Node 源码运行 -仓库贡献者可以选择以下任一开发载体: +仓库贡献者可以选择以下任一开发路径;两者都执行普通的 `dsh --profile sdk` 启动器: - 设置 `DSH_RUNTIME_MODE=node`,在系统 Node `>=22.19` 上使用已构建的 Node 载体。构建脚本会刷新该载体,但分发物绝不会包含或自动选择它。 -- 将仓库根目录设为 `cwd`,并设置 `launch_args_override=("./node_modules/.bin/tsx", "packages/sdk/python-runtime/src/packaged-bin.ts")`,以运行私有载体未构建的 TypeScript 源码。默认配置不合适时,请提供 `cordis=...`。 +- 将 `dsh_bin` 设置为已构建 `apps/cli/lib/bin.js` 的绝对路径,直接验证当前 checkout 的 CLI。请显式提供 `dsh_home`,并按需提供 `profile` 与有序 `patches`。 -完整的源码模式调用见 `python/sdk/tests/manual_sdk_agent_smoke.py`。 +`python/sdk/tests/manual_sdk_agent_smoke.py` 使用内部 `_launch_args` 测试适配器,通过 tsx 验证未构建的 TypeScript CLI。公开 SDK 刻意不提供任意 argv 替换。 ## 构建分发包 @@ -71,7 +73,7 @@ print(release["pep440_version"](release["repository_version"]())) PY )" python scripts/build-python-release.py --package sdk --output-dir dist-python -python scripts/build-python-release.py --package runtime --platform macos-arm64 --runtime-exe dist-exe/dsh-jsonrpc-agent-pkg-macos-arm64 --output-dir dist-python +python scripts/build-python-release.py --package runtime --platform macos-arm64 --runtime-exe dist-exe/deepseek-harness-sdk-runtime-macos-arm64 --output-dir dist-python pip install \ "dist-python/deepseek_harness_sdk-$version-py3-none-any.whl" \ "dist-python/deepseek_harness_runtime_bin-$version-py3-none-macosx_14_0_arm64.whl" diff --git a/python/sdk-runtime/README.i18n.yaml b/python/sdk-runtime/README.i18n.yaml index c4ccb18bf1..51965002ce 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: 67d3842a9255250f66f22ff1f9422b26c3cf3282 -README.zh.md: 47c94b29d68eb915fae5303274fe2888274c1f82 +README.md: 1552f2a120938ecab6d05dd244a745bec65bb00a +README.zh.md: 9524617ee1a950080476a91db5ec6e14727518ce diff --git a/python/sdk-runtime/README.md b/python/sdk-runtime/README.md index 67d3842a92..1552f2a120 100644 --- a/python/sdk-runtime/README.md +++ b/python/sdk-runtime/README.md @@ -1,31 +1,36 @@ -# DeepSeek Harness Runtime Wheel +# deepseek-harness-runtime-bin -English | [中文](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk-runtime/README.zh.md) +English | [中文](README.zh.md) -Runtime carrier package for the Python SDK (dist `deepseek-harness-runtime-bin`, module `deepseek_harness_runtime`): it locates the bundled runtime binaries the `deepseek-harness-sdk` client spawns, and ships the default configuration behind zero-config runs. +Platform runtime wheel for the DeepSeek Harness Python SDK. It packages the normal `dsh` CLI and its closed Node dependency tree into a native executable, so SDK use requires no system Node.js. This package publishes wheels only. -## Runtime carriers +## Installed commands and artifacts -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: +The wheel installs a `dsh` console command and the `deepseek_harness_runtime` Python module. `dsh` forwards its arguments to the bundled executable and requires a non-empty `DSH_HOME`; it never falls back to `~/.dsh`. -- **exe (production)** — a single-file Node executable `dsh-jsonrpc-agent-pkg--` (platform: `linux`/`macos`; arch: `x64`/`arm64`) with a target-native ripgrep `-rg` sidecar. 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-sdk-python-runtime/lib/packaged-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. +Production executables are named `deepseek-harness-sdk-runtime--` under the module's `runtime/` directory. Linux and macOS wheels include a target-native `-rg` sidecar; macOS also includes `-spawn-helper` for `node-pty`. Published targets are Linux x64, Linux arm64, and macOS arm64. The wheel tag and payload must match exactly. -Both carriers hold the same content, defined once: the [package.json](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk-runtime/package.json) at this package's root is the private `dsh-sdk-python-runtime-closure` 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. +Repository builds also materialize a dev-only `runtime/node/` carrier. It runs `node runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js` on system Node 22.19 or newer. It is never selected automatically and is excluded from wheels and sdists. -The bundled plugin set includes `@deepseek-ai/dsh-mcp-client`, so an external Cordis config can connect to stdio or Streamable HTTP MCP servers and expose their tools to the model. The wheel does not bundle MCP server programs or credentials: a stdio config supplies its executable and arguments, while a Streamable HTTP config supplies its URL and headers. The bridge supports MCP tools; MCP Resources and Prompts remain unsupported. +Both carriers execute the same `dsh` grammar and shipped profiles, including the standalone `sdk-minimal` tree and the full `web` profile with its frontend assets. The private `dsh-python-runtime-closure` manifest defines the packaged dependency closure; there is no Python-specific Node application or checked-in default `cordis.yml`. -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. +## Python module API -Each wheel contains exactly one runtime executable and its matching ripgrep `-rg` sidecar. The macOS wheel also contains its matching native spawn helper; any missing sidecar makes that installation incomplete and is a hard startup error, even for a selected Cordis composition that does not use filesystem-search or 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_14_0_arm64`; the macOS tag conservatively matches the bundled Node 24 executable's macOS 13.5 deployment target. This package's `platforms.json` owns the fixed tag and executable-name pairs used by both the repository release builder and the isolated build hook. The build hook rejects `py3-none-any`, absent or multiple runtime executables, missing or extra sidecars, 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-v` release tag must match it. +- `bundled_package_dir() -> Path` returns the installed module-data root and verifies its release metadata. +- `bundled_runtime_path() -> Path` returns the current platform executable and verifies required sidecars. +- `resolve_bundled_launch_args(mode=None) -> tuple[str, ...]` returns the executable argv by default. Explicit `mode="node"` or `DSH_RUNTIME_MODE=node` selects the repo-only Node carrier. +- `main()` implements the installed `dsh` console command and rejects an absent or blank `DSH_HOME` before replacing the Python process. -## Resolution API +Unsupported platforms and missing executables or sidecars raise `FileNotFoundError` with the build and installation routes. Unknown runtime modes raise `ValueError`. -- `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 the required sibling `-rg` sidecar on every platform and the `-spawn-helper` sidecar on macOS. 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. +## Packaged profile resolution -## Zero-config design +`dsh` initializes shipped profiles under the explicit home, composes their bundle patches, and loads bundled plugins from the executable's virtual filesystem. Because operating-system symlinks cannot enter that filesystem, packaged launches maintain small real ESM proxy packages under `$DSH_HOME/profiles/node_modules`. Each proxy mirrors explicit runtime exports, records the original package identity, and re-exports the virtual module URL. Built-in rows and external plugin peers therefore share one Cordis/module instance. Native shared libraries are packaged with native addons, while ripgrep and the macOS PTY helper remain executable sidecars. -The runtime binary always demands an explicit config (`$DSH_CORDIS_CONFIG`, or a config path as an argv positional argument) and exits loudly without one — that hard semantic is part of the runtime's design and this package does not soften it. The bin (`dsh-jsonrpc-agent`) boots only the plugins the config lists; the serving interface (the stdio JSON-RPC server) is itself one of its entries (`@deepseek-ai/dsh-sdk-jsonrpc-server`), and without it the booted agent has no channel to the outside. This package checks in `runtime/cordis.yml` with the JSON-RPC serving entry, agent core, a preloaded DeepSeek adapter, JSONL persistence, the explicitly composed semantic checkpoint policy, local bash, and a local filesystem provider for bounded workspace-instruction loading. The persistence backend owns durable storage while the separate policy selects request-, tool-dispatch-, and completed-step checkpoints. The adapter reads `DEEPSEEK_API_KEY` and `DEEPSEEK_BASE_URL`, while persistence, bash, and the filesystem provider use `DSH_SESSION_ROOT` and `DSH_CWD` with manual-run fallbacks. When the caller uses no explicit config channel, the `deepseek_harness` client injects that file's path via `DSH_CORDIS_CONFIG` (injection conditions: [sdk README](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk/README.md)). Zero-config is thus an explicit, visible parameter pass in the wrapper, not a hidden fallback in the runtime. +External profile management uses `dsh plugin --profile ...`. That command requires `pnpm` on `PATH`; ordinary SDK/profile execution does not. + +## Build and distribution + +From the repository root, `pnpm exec tsx scripts/build-exe-for-python-sdk.ts` verifies the closure, builds packages, deploys a symlink-free tree, packages the selected target, and syncs the executable and sidecars into this module. `scripts/build-python-release.py` stages release-shaped wheels at the root repository version and pins `deepseek-harness-sdk` to the exact runtime version. + +The installed-wheel smoke creates a clean virtual environment outside the checkout, proves distribution and executable provenance, then exercises default and customized SDK profiles, external plugins, MCP, native tools, direct JSON-RPC, committed snapshots, and the real provider on trusted runs. See the [Python contributor workflow](../development.md) and [installed-wheel testing decision](../../.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.md). diff --git a/python/sdk-runtime/README.zh.md b/python/sdk-runtime/README.zh.md index 47c94b29d6..9524617ee1 100644 --- a/python/sdk-runtime/README.zh.md +++ b/python/sdk-runtime/README.zh.md @@ -1,31 +1,36 @@ -# DeepSeek Harness 运行时 wheel 包 +# deepseek-harness-runtime-bin -[English](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk-runtime/README.md) | 中文 +[English](README.md) | 中文 -Python SDK 的运行时载体包(分发名 `deepseek-harness-runtime-bin`,模块名 `deepseek_harness_runtime`):它定位 `deepseek-harness-sdk` 客户端要 spawn 的内置运行时二进制,并附带支撑零配置运行的默认配置。 +DeepSeek Harness Python SDK 的平台运行时 wheel。它把普通 `dsh` CLI 及其封闭的 Node 依赖树打包成原生可执行程序,因此使用 SDK 不需要系统 Node.js。本包只发布 wheel。 -## 运行时载体 +## 安装命令与产物 -两种载体并存于 `src/deepseek_harness_runtime/runtime/` 之下,均由仓库的 `scripts/build-exe-for-python-sdk.ts` 构建注入,且均被 git 忽略: +Wheel 会安装 `dsh` 控制台命令和 `deepseek_harness_runtime` Python 模块。`dsh` 将参数转发给内置可执行程序,并要求非空 `DSH_HOME`;它不会回退到 `~/.dsh`。 -- **exe(生产)**——单文件 Node 可执行程序 `dsh-jsonrpc-agent-pkg--`(platform:`linux`/`macos`;arch:`x64`/`arm64`),以及匹配目标平台的 ripgrep `-rg` 伴随文件。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-sdk-python-runtime/lib/packaged-bin.js` 执行。它是当前检出的源码构建,仅用于仓库本地的开发与验证;不会被自动选中,也不进入分发物。 +生产可执行程序位于模块的 `runtime/` 目录,命名为 `deepseek-harness-sdk-runtime--`。Linux 与 macOS wheel 包含目标平台原生的 `-rg` 伴随程序;macOS 还包含 `node-pty` 使用的 `-spawn-helper`。已发布目标是 Linux x64、Linux arm64 与 macOS arm64。Wheel tag 必须与载荷严格匹配。 -两种载体承载相同的内容,且只定义一次:本包根目录的 [package.json](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk-runtime/package.json) 是 single-exe 流水线的私有 `dsh-sdk-python-runtime-closure` 部署根目录——一份零代码的纯依赖 manifest,其依赖闭包既是编译进 exe 的插件集,也是物化到 `runtime/node/` 的文件树。往分发物里加插件,就是在那里加一行依赖再重新构建。 +仓库构建还会物化仅限开发的 `runtime/node/` 载体。它在系统 Node 22.19 或更高版本上运行 `node runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js`。系统不会自动选择它,而且 wheel 与 sdist 均不包含它。 -内置插件集合包含 `@deepseek-ai/dsh-mcp-client`,因此外部 Cordis 配置可以连接 stdio 或 Streamable HTTP MCP server,并向模型提供这些 server 的工具。wheel 包不包含 MCP server 程序或凭据:stdio 配置需要提供可执行程序及其参数,Streamable HTTP 配置需要提供 URL 和请求头。该桥接仅支持 MCP 工具,尚不支持 MCP Resources 与 Prompts。 +两种载体执行相同的 `dsh` 语法与随附 profile,包括独立的 `sdk-minimal` 配置树,以及包含前端产物的完整 `web` profile。私有 `dsh-python-runtime-closure` manifest 定义打包依赖闭包;不存在 Python 专用 Node 应用或检入的默认 `cordis.yml`。 -exe 缺失时抛出 `FileNotFoundError`,并写明两种获取途径:在 deepseek-harness 检出中经 `scripts/build-exe-for-python-sdk.ts` 构建,或安装 `build-exe-for-python-sdk` CI 工作流生成的对应平台运行时 wheel 包。仅限开发的 node 载体缺失时只提示构建脚本这一条途径。该工作流只保留 wheel 包,不保留独立 exe 归档。获取策略与查找接口刻意分离,之后可以换成按需下载而不改动任何调用方。 +## Python 模块 API -每个 wheel 包只包含一个运行时可执行文件及其匹配的 ripgrep `-rg` 伴随文件。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_14_0_arm64`;macOS 标签保守匹配内置 Node 24 可执行文件的 macOS 13.5 部署目标。本包的 `platforms.json` 统一定义仓库发行构建器与隔离构建钩子使用的固定标签和可执行文件名。构建钩子会拒绝 `py3-none-any`、不存在或存在多个运行时可执行文件、缺失或多余的伴随文件、文件不可执行以及不支持的平台标签。仓库根目录的 `package.json` 为本包和 SDK 提供共同版本,`python-v` 发布标签必须与其匹配。 +- `bundled_package_dir() -> Path` 返回已安装模块数据根目录,并校验发布元数据。 +- `bundled_runtime_path() -> Path` 返回当前平台可执行程序,并校验必需伴随文件。 +- `resolve_bundled_launch_args(mode=None) -> tuple[str, ...]` 默认返回可执行程序 argv。显式 `mode="node"` 或 `DSH_RUNTIME_MODE=node` 会选择仅限仓库使用的 Node 载体。 +- `main()` 实现已安装的 `dsh` 控制台命令,并在替换 Python 进程前拒绝缺失或空白的 `DSH_HOME`。 -## 解析 API +不支持的平台以及缺失的可执行程序或伴随文件会抛出 `FileNotFoundError`,并指出构建与安装路径。未知运行时模式会抛出 `ValueError`。 -- `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 载体);它会在所有平台校验必要的 `-rg` 伴随文件,并在 macOS 上额外校验 `-spawn-helper` 伴随文件。node 载体没有单一路径的等价物,经由上面的 argv 元组启动。 -- `bundled_default_config_path() -> Path`——检入的默认配置(见下文)。 -- `bundled_package_dir() -> Path`——已安装包的数据根目录。 +## 打包后的 profile 解析 -## 零配置设计 +`dsh` 在显式 home 下初始化随附 profile、组合其 bundle patch,并从可执行程序的虚拟文件系统加载内置插件。操作系统符号链接无法进入该文件系统,因此打包运行会在 `$DSH_HOME/profiles/node_modules` 下维护小型真实 ESM 代理包。每个代理镜像显式运行时 exports、记录原包身份,并重新导出虚拟模块 URL。因此,内置配置项与外部插件 peer 会共享同一个 Cordis/模块实例。原生共享库与原生 addon 一同打包;ripgrep 与 macOS PTY helper 仍是可执行伴随程序。 -运行时二进制始终要求显式配置(`$DSH_CORDIS_CONFIG`,或作为 argv 位置参数的配置路径),缺了就报错退出——这一强制语义是运行时设计的一部分,本包不会弱化它。bin(`dsh-jsonrpc-agent`)只启动配置里列出的插件;对外服务接口(stdio JSON-RPC 服务器)也是其中一个条目(`@deepseek-ai/dsh-sdk-jsonrpc-server`),缺了它,启动出的 agent(智能体)就没有对外通道。本包检入的 `runtime/cordis.yml` 包含 JSON-RPC 服务条目、agent 核心、预载的 DeepSeek 适配器、JSONL 持久化、显式组合的语义检查点策略、本地 bash,以及用于有界加载工作区指令的本地文件系统提供方。持久化后端负责持久存储,独立的策略则选择请求、工具分发和已完成步骤的检查点。DeepSeek 适配器读取 `DEEPSEEK_API_KEY` 与 `DEEPSEEK_BASE_URL`,持久化、bash 和文件系统提供方则使用 `DSH_SESSION_ROOT` 和 `DSH_CWD`,并为手动运行提供回退值。调用方未使用任何显式配置通道时,`deepseek_harness` 客户端把该文件路径注入 `DSH_CORDIS_CONFIG`(注入条件见 [sdk README](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk/README.md))。因此,零配置是包装层中一次显式、可见的参数传递,而不是运行时中的隐藏回退。 +外部 profile 管理使用 `dsh plugin --profile ...`。该命令要求 `PATH` 中存在 `pnpm`;普通 SDK/profile 运行不需要它。 + +## 构建与分发 + +在仓库根目录运行 `pnpm exec tsx scripts/build-exe-for-python-sdk.ts`,会校验闭包、构建包、部署无符号链接的文件树、打包所选目标,并把可执行程序及伴随文件同步到本模块。`scripts/build-python-release.py` 按仓库根版本暂存发布形态的 wheel,并将 `deepseek-harness-sdk` 固定到完全相同的运行时版本。 + +Installed-wheel smoke 会在 checkout 外创建干净虚拟环境,证明 distribution 与可执行程序来源,然后覆盖默认及自定义 SDK profile、外部插件、MCP、原生工具、直接 JSON-RPC、检入快照,以及可信运行中的真实提供方。另见 [Python 贡献者工作流](../development.zh.md)与 [installed-wheel 测试决策](../../.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)。 diff --git a/python/sdk-runtime/hatch_build.py b/python/sdk-runtime/hatch_build.py index ef5a621f9d..c4083387a6 100644 --- a/python/sdk-runtime/hatch_build.py +++ b/python/sdk-runtime/hatch_build.py @@ -66,7 +66,9 @@ 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 []) + runtime_files = sorted( + runtime_dir.glob("deepseek-harness-sdk-runtime-*") if runtime_dir.is_dir() else [] + ) expected_files = [expected_executable, f"{expected_executable}-rg"] if "-macos-" in expected_executable: expected_files.append(f"{expected_executable}-spawn-helper") diff --git a/python/sdk-runtime/package.json b/python/sdk-runtime/package.json index dad5db82b7..d5684867e6 100644 --- a/python/sdk-runtime/package.json +++ b/python/sdk-runtime/package.json @@ -1,6 +1,6 @@ { - "name": "dsh-sdk-python-runtime-closure", - "description": "Dependency-only deploy root defining the executable and Python runtime closure; pnpm deploy materializes this manifest and node_modules.", + "name": "dsh-python-runtime-closure", + "description": "Dependency-only deploy root defining the dsh executable shipped by the Python runtime wheel.", "version": "0.0.1", "private": true, "type": "module", @@ -10,6 +10,7 @@ "@deepseek-ai/cordis-plugin-loader": "workspace:^", "@deepseek-ai/cordis-plugin-timer": "workspace:^", "@deepseek-ai/cosmokit": "workspace:^", + "@deepseek-ai/dsh": "workspace:^", "@deepseek-ai/dsh-acp": "workspace:^", "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", @@ -45,7 +46,6 @@ "@deepseek-ai/dsh-hooks-codex": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-sdk-jsonrpc-server": "workspace:^", - "@deepseek-ai/dsh-sdk-python-runtime": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-llm-deepseek": "workspace:^", "@deepseek-ai/dsh-deepseek-llm-api-extensions": "workspace:^", @@ -77,6 +77,7 @@ "@deepseek-ai/dsh-session-query": "workspace:^", "@deepseek-ai/dsh-session-query-sqlite": "workspace:^", "@deepseek-ai/dsh-session-reference": "workspace:^", + "@deepseek-ai/dsh-session-telemetry": "workspace:^", "@deepseek-ai/dsh-session-title": "workspace:^", "@deepseek-ai/dsh-settings": "workspace:^", "@deepseek-ai/dsh-skill": "workspace:^", diff --git a/python/sdk-runtime/platforms.json b/python/sdk-runtime/platforms.json index 069378e8cb..e65cd6a735 100644 --- a/python/sdk-runtime/platforms.json +++ b/python/sdk-runtime/platforms.json @@ -1,14 +1,14 @@ { "linux-x64": { "tag": "manylinux_2_28_x86_64", - "executable": "dsh-jsonrpc-agent-pkg-linux-x64" + "executable": "deepseek-harness-sdk-runtime-linux-x64" }, "linux-arm64": { "tag": "manylinux_2_28_aarch64", - "executable": "dsh-jsonrpc-agent-pkg-linux-arm64" + "executable": "deepseek-harness-sdk-runtime-linux-arm64" }, "macos-arm64": { "tag": "macosx_14_0_arm64", - "executable": "dsh-jsonrpc-agent-pkg-macos-arm64" + "executable": "deepseek-harness-sdk-runtime-macos-arm64" } } diff --git a/python/sdk-runtime/pyproject.toml b/python/sdk-runtime/pyproject.toml index 090efc6139..4005314aa0 100644 --- a/python/sdk-runtime/pyproject.toml +++ b/python/sdk-runtime/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "deepseek-harness-runtime-bin" version = "0.0.0.dev0" -description = "Pinned DeepSeek Harness runtime for the Python SDK" +description = "Bundled dsh CLI runtime for the DeepSeek Harness Python SDK" readme = "README.md" requires-python = ">=3.10" license = "MIT" @@ -17,10 +17,12 @@ Documentation = "https://github.com/deepseek-ai/deepseek-harness/blob/master/pyt Issues = "https://github.com/deepseek-ai/deepseek-harness/issues" Source = "https://github.com/deepseek-ai/deepseek-harness" -# Include the injected executable and default config; exclude the dev-only node -# closure from wheels and sdists. +[project.scripts] +dsh = "deepseek_harness_runtime:main" + +# Include the injected dsh executable and sidecars; exclude the dev-only node closure. [tool.hatch.build] -artifacts = ["src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-*"] +artifacts = ["src/deepseek_harness_runtime/runtime/deepseek-harness-sdk-runtime-*"] exclude = ["src/deepseek_harness_runtime/runtime/node"] [tool.hatch.build.targets.wheel] diff --git a/python/sdk-runtime/src/deepseek_harness_runtime/__init__.py b/python/sdk-runtime/src/deepseek_harness_runtime/__init__.py index 94aca48d9f..0fc4f416c0 100644 --- a/python/sdk-runtime/src/deepseek_harness_runtime/__init__.py +++ b/python/sdk-runtime/src/deepseek_harness_runtime/__init__.py @@ -1,22 +1,21 @@ -"""Locate the bundled DeepSeek Harness SDK runtime shipped with this package. +"""Locate and execute the bundled dsh CLI shipped with the Python SDK runtime. Two runtime carriers coexist under ``runtime/``, both injected by the repo's ``scripts/build-exe-for-python-sdk.ts`` build (neither is checked into git): - **exe (production)**: single-file Node executables named - ``dsh-jsonrpc-agent-pkg--`` (platform in {linux, macos}, arch in + ``deepseek-harness-sdk-runtime--`` (platform in {linux, macos}, arch in {x64, arm64}) with a sibling ``-rg`` executable; 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-sdk-python-runtime/lib/packaged-bin.js`` on a + runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js`` on a system Node >= 22.19. It is the current checkout's source build, never selected automatically, and excluded from wheel/sdist distributions. -``runtime/cordis.yml`` IS checked in: it is the default agent configuration -the client SDK injects via ``$DSH_CORDIS_CONFIG`` for zero-config runs — the -runtime itself always requires an explicit config and has no built-in -fallback. +Both carriers execute the same dsh command grammar. The Python SDK selects the +``sdk`` profile and requires an explicit Harness home; the installed ``dsh`` +console command requires ``DSH_HOME`` for the same reason. """ from __future__ import annotations @@ -52,21 +51,6 @@ def bundled_package_dir() -> Path: return root -def bundled_default_config_path() -> Path: - """Path of the checked-in default runtime configuration (``runtime/cordis.yml``). - - The client SDK injects this path via ``$DSH_CORDIS_CONFIG`` when the caller - supplies no config and the launch resolves to the bundled runtime — the - runtime binary itself always demands an explicit config. - """ - path = bundled_package_dir() / "runtime" / "cordis.yml" - if not path.is_file(): - raise FileNotFoundError( - f"deepseek-harness-runtime-bin is missing the default runtime config at {path}" - ) - return path - - def bundled_runtime_path() -> Path: """Absolute path of the bundled single-file runtime executable for the current platform. @@ -78,7 +62,7 @@ def bundled_runtime_path() -> Path: touching callers). """ tag = _current_platform_tag() - path = bundled_package_dir() / "runtime" / f"dsh-jsonrpc-agent-pkg-{tag}" + path = bundled_package_dir() / "runtime" / f"deepseek-harness-sdk-runtime-{tag}" if not path.is_file(): raise FileNotFoundError( f"deepseek-harness-runtime-bin is missing the runtime executable at {path}. " @@ -128,7 +112,7 @@ def _current_platform_tag() -> str: arch = _ARCH_TAGS.get(platform.machine().lower()) if plat is None or arch is None: raise FileNotFoundError( - "no bundled dsh-jsonrpc-agent executable exists for this platform " + "no bundled DeepSeek Harness SDK runtime exists for this platform " f"(sys.platform={sys.platform!r}, machine={platform.machine()!r}); supported: " "linux/macos on x64/arm64. " + _EXE_ACQUISITION_HINT ) @@ -141,9 +125,9 @@ def _node_launch_args() -> tuple[str, str]: node_root / "node_modules" / "@deepseek-ai" - / "dsh-sdk-python-runtime" + / "dsh" / "lib" - / "packaged-bin.js" + / "bin.js" ) if not bin_js.is_file(): raise FileNotFoundError( @@ -161,11 +145,24 @@ def _node_launch_args() -> tuple[str, str]: return (node, str(bin_js)) +def main() -> None: + """Execute the bundled dsh CLI with an explicitly selected Harness home.""" + if not os.environ.get("DSH_HOME", "").strip(): + print( + "dsh: the Python runtime command requires an explicit DSH_HOME; " + "it never uses ~/.dsh implicitly", + file=sys.stderr, + ) + raise SystemExit(2) + argv = (*resolve_bundled_launch_args(), *sys.argv[1:]) + os.execvpe(argv[0], argv, os.environ) + + __all__ = [ "PACKAGE_METADATA_FILENAME", "RUNTIME_MODE_ENV_VAR", - "bundled_default_config_path", "bundled_package_dir", "bundled_runtime_path", + "main", "resolve_bundled_launch_args", ] diff --git a/python/sdk-runtime/src/deepseek_harness_runtime/runtime/cordis.yml b/python/sdk-runtime/src/deepseek_harness_runtime/runtime/cordis.yml deleted file mode 100644 index 02f8cac145..0000000000 --- a/python/sdk-runtime/src/deepseek_harness_runtime/runtime/cordis.yml +++ /dev/null @@ -1,58 +0,0 @@ -# Bundled default config. The runtime still requires an explicit -# $DSH_CORDIS_CONFIG or argv path; the SDK injects this path for bundled -# zero-config launches. SDK-set session-root and cwd variables have manual-run fallbacks. - -# Stdio JSON-RPC server entry; without it the agent has no SDK client. -- id: sdk-jsonrpc-server - name: '@deepseek-ai/dsh-sdk-jsonrpc-server' - -# Agent spine; the SDK server creates agents per sessionId. -- id: agent-core - name: '@deepseek-ai/dsh-agent-spine-demo' - config: - workspaceContext: - maxBytes: 65536 - -# Stock DeepSeek adapters. The adapter resolves DEEPSEEK_API_KEY through the -# credential seam and, with no provider mounted here, from the launching -# environment; DEEPSEEK_BASE_URL follows the same environment ladder. Neither -# is inlined, so this file names no secret and no route. -- id: deepseek-llm-api-extensions - name: '@deepseek-ai/dsh-deepseek-llm-api-extensions' - -- id: session-log-deepseek - name: '@deepseek-ai/dsh-session-log-deepseek' - -- id: plugin-package-inventory-deepseek - name: '@deepseek-ai/dsh-plugin-package-inventory-deepseek' - -- id: llm-deepseek - name: '@deepseek-ai/dsh-llm-deepseek' - -# JSONL persistence; $DSH_SESSION_ROOT wins over ./.sessions in the process cwd. -- id: sessions - name: '@deepseek-ai/dsh-session-persistence-jsonl' - config: - root: !!js process.env.DSH_SESSION_ROOT ?? './.sessions' - -# Persistence owns durable storage; this separate policy explicitly selects -# the request, tool-dispatch, and completed-step durability checkpoints. -- id: session-checkpoints - name: '@deepseek-ai/dsh-session-checkpoint-policy' - -# Local bash executor; $DSH_CWD wins over the process cwd. -# Managed child-process groups for the bash executor (spawn/kill/output plumbing). -- id: subprocess - name: '@deepseek-ai/dsh-subprocess-local' - -- id: bash - name: '@deepseek-ai/dsh-bash-local' - config: - cwd: !!js process.env.DSH_CWD ?? process.cwd() - -# Local filesystem provider for workspace instruction loading. This does not -# expose model-facing file tools by itself. -- id: fs-local - name: '@deepseek-ai/dsh-fs-local' - config: - cwd: !!js process.env.DSH_CWD ?? process.cwd() diff --git a/python/sdk/README.i18n.yaml b/python/sdk/README.i18n.yaml index 4b93a8d04e..5295ed3c11 100644 --- a/python/sdk/README.i18n.yaml +++ b/python/sdk/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write python/sdk/README.md -README.md: 99515c52e6314dc29788a324338699d75c0a4451 -README.zh.md: 6ec268545107478ce9f347cdfb7f17d4a8afd151 +README.md: cf9bb3e3ccac4908e9212d8f7247545b5a6b5d8e +README.zh.md: 9acb8f26129144a77a834f94854cdd3f1a200086 diff --git a/python/sdk/README.md b/python/sdk/README.md index 99515c52e6..cf9bb3e3cc 100644 --- a/python/sdk/README.md +++ b/python/sdk/README.md @@ -1,51 +1,66 @@ # DeepSeek Harness Python SDK -English | [中文](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk/README.zh.md) +English | [中文](README.zh.md) -Python subprocess SDK for driving DeepSeek Harness over JSON-RPC stdio. The -runtime inherits normal DeepSeek Harness environment variables such as -`DEEPSEEK_BASE_URL` and `DEEPSEEK_API_KEY`, so callers can use real model -endpoints directly or point those variables at a local proxy. - -Install the `deepseek-harness-sdk` distribution from PyPI; the import module remains `deepseek_harness`: +Python subprocess SDK for driving DeepSeek Harness over newline-delimited JSON-RPC on stdio. Install `deepseek-harness-sdk`; it installs the exact same-version `deepseek-harness-runtime-bin` wheel for the current platform. ```sh python -m pip install deepseek-harness-sdk ``` -Installing `deepseek-harness-sdk` installs the exact same-version `deepseek-harness-runtime-bin` platform wheel. The normal entry point therefore needs no executable argument: +## Start a runtime -```py -from deepseek_harness import DeepSeekHarness +The Python SDK has no separate application entrypoint. It launches the bundled `dsh` CLI with `--profile sdk`; the selected profile owns the JSON-RPC server, agent composition, credentials, persistence, tools, and shutdown behavior. -with DeepSeekHarness() as harness: - result = harness.run("Say hi.") -``` - -`DeepSeekHarness` keeps its lazily started runtime subprocess for reuse across calls. Use it as a context manager, as above, or call `close()` explicitly when finished. - -By default, the SDK launches the bundled single-file `dsh-jsonrpc-agent` executable from the `deepseek-harness-runtime-bin` package and injects that package's default configuration (the stdio JSON-RPC server, agent core, preloaded DeepSeek adapter, JSONL session persistence with an explicitly composed semantic checkpoint policy, local bash) via `DSH_CORDIS_CONFIG`. To run a plugin composition of your own, keep the `@deepseek-ai/dsh-sdk-jsonrpc-server` entry in the config and pass the Cordis config path. +Every launch requires an explicit Harness home. Pass `dsh_home` or provide a non-empty `DSH_HOME` in the child environment. The SDK deliberately never discovers `~/.dsh`. ```py from deepseek_harness import DeepSeekHarness with DeepSeekHarness( - provider="deepseek-official", - model="deepseek-v4-flash", - max_tokens=49_152, - cordis="examples/python-sdk-agent/cordis.yml", + dsh_home="/absolute/path/to/isolated-dsh-home", + cwd="/absolute/path/to/workspace", +) as harness: + result = harness.run("Say hi.", session_id="example-001") + +print(result.final_response) +``` + +`DeepSeekHarness` starts lazily and reuses its runtime until `close()` or context-manager exit. The initial profile handshake has an independent 10-second default bound through `initialize_timeout_seconds`; ordinary turns remain unbounded unless `request_timeout_seconds` is set. A timeout names the selected profile and includes retained runtime diagnostics. `cwd` is the agent workspace; `runtime_cwd` independently selects the subprocess working directory. Both become absolute before launch. `provider`, `model`, and optional positive `max_tokens` are sent during JSON-RPC initialization. `base_url` and `api_key` explicitly override `DEEPSEEK_BASE_URL` and `DEEPSEEK_API_KEY` in the child environment. + +## Customize plugins + +Persistent customization belongs to a `dsh` profile. Initialize the shipped SDK profile and install an external bundle with the runtime wheel's `dsh` command: + +```sh +export DSH_HOME=/absolute/path/to/isolated-dsh-home +dsh --profile sdk --dump-default-config >/dev/null +dsh plugin --profile sdk add file:/absolute/path/to/my-plugin-bundle +``` + +The `file:` form installs the local bundle into the profile package tree, where its peer imports reach the bundled installation fallback. The profile manifest records installed dependencies and ordered bundle layers; its `$DSH_HOME/profiles/sdk/cordis.patch.yml` is the persistent user patch. `dsh plugin` needs `pnpm` only when managing external packages. Running the SDK does not require system Node.js. + +For an invocation-specific change, pass one or more patch files. They become absolute and are forwarded in order after the profile and home patch layers: + +```py +with DeepSeekHarness( + dsh_home="/absolute/path/to/isolated-dsh-home", + profile="sdk", + patches=("/absolute/path/to/first.patch.yml", "/absolute/path/to/last.patch.yml"), ) as harness: result = harness.run("Make the requested code change.") ``` -`provider` selects a provider route registered by the chosen Cordis composition; `model` is the model id resolved by that adapter. `max_tokens` is an optional positive per-request output-token cap for the root agent and its in-process descendants; omission leaves the provider default in control. Compaction summaries keep the separate limit configured by their compaction plugin. The bundled default composition registers `deepseek-official`. A custom composition can mount `llm-pi-ai`, configure provider-specific credentials/endpoints there, and select any provider/model present in pi-ai's installed catalog. +`profile` may select another existing profile, but that composition must retain `@deepseek-ai/dsh-sdk-app` or another `@deepseek-ai/dsh-sdk-jsonrpc-server` row. Misconfiguration fails during CLI boot or SDK initialization; there is no complete-config fallback. `dsh_bin` may select another `dsh` executable while preserving the same profile grammar. Arbitrary argv replacement remains an internal fake-runtime test adapter, not public API. -The [Python SDK tutorial](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/guide/python-sdk.md) provides an ordered installation and first-run path without the Web UI. The [`python-sdk-agent` example](https://github.com/deepseek-ai/deepseek-harness/blob/master/examples/python-sdk-agent/README.md) owns the complete standalone Cordis file used there. +The shipped `sdk-minimal` profile is a standalone explicit tree rather than an overlay on `dsh-base`. Select it with `profile="sdk-minimal"`; the ordinary `model` argument is the sole runtime model selection, including for model ids outside the adapter's advisory catalog. It provides persistent Bash, the string-replace editor, local execution, and JSONL sessions; settings, managed credentials, telemetry, Web tools, and the full default tool roster remain available through the separate full `sdk` and `web` profiles. -`Session.run()` owns an activity interval from its prompt's durable inbox receipt through the next whole-agent idle and returns `RunResult(session_id, final_response, finish_reason, events, notifications, session_root)`. `final_response` is the last committed root-session assistant text in the interval. `finish_reason` is the `kind` of the last root-session `turn/end` in the interval, such as `completed`, `max-tokens`, or `error`, and is `None` when no turn ended. A `turn/end` without a string `data.reason.kind` violates the runtime protocol and raises `SdkProtocolError`. Both result fields describe the owned interval rather than an output or ending causally assigned to the prompt. Steering, injected context, and other queued work may contribute before idle. +## Results and notifications -`HarnessClient` retains discovered subagent ancestry for the lifetime of the runtime process. During each `Session.run()`, `RunResult.notifications` and `on_notification` receive the root session and all known descendant notifications in wire order, including nested subagent lifecycle and session events. `RunResult.events` contains root-session events only, so descendant messages cannot replace the root response. The low-level `session_prompt()` returns the queued `MessageId` immediately; callers that bypass `Session.run()` own any later activity boundary themselves. +`Session.run()` owns an activity interval from its prompt's durable inbox receipt through the next whole-agent idle and returns `RunResult(session_id, final_response, finish_reason, events, notifications)`. `final_response` is the last committed root-session assistant text in the interval. `finish_reason` is the `kind` of the last root-session `turn/end`, such as `completed`, `max-tokens`, or `error`, and is `None` when no turn ended. A `turn/end` without a string `data.reason.kind` violates the protocol and raises `SdkProtocolError`. -The same behavior can be selected for the runtime subprocess with `DSH_CORDIS_CONFIG`. The injection lives in `HarnessClient.start()`, so the low-level client's default launch gets it too: when the launch resolves to the bundled runtime and neither `cordis` nor a non-empty `DSH_CORDIS_CONFIG` is set (the runtime treats an empty value as absent, and so does the injection check), the bundled default configuration is used; an explicit `runtime_bin`, `bridge_bin`, or `launch_args_override` disables the injection entirely. See the [sdk-runtime README](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk-runtime/README.md) for the runtime carriers (production exe vs dev-only node closure) and how to obtain them. +`HarnessClient` retains discovered subagent ancestry for the runtime process lifetime. During `Session.run()`, `RunResult.notifications` and `on_notification` receive the root session and known descendants in wire order. `RunResult.events` contains root-session events only, so descendant output cannot replace the root response. The low-level `session_prompt()` returns the queued message id immediately; callers that bypass `Session.run()` own the later activity boundary. -`cwd` and `runtime_cwd` are resolved to absolute paths before subprocess launch, environment injection, and the wire handshake. The public API exposes only applied options: deployment persona and persistence belong in `cordis.yml`, while `session_root` remains the high-level convenience that sets `DSH_SESSION_ROOT`. +The selected home stores profiles, plugins, and every profile-owned durable resource. The full `sdk` profile uses its credentials, settings, and session stores; `sdk-minimal` uses only its JSONL session store. Use a fresh home when those resources must be isolated, and a fresh session id for independent work. Reusing both a harness and session id continues the durable conversation and session-owned resources. + +See the [Python tutorial](../../docs/user/guide/python-sdk.md), [`python-sdk-agent` example](../../examples/python-sdk-agent/README.md), and [runtime wheel reference](../sdk-runtime/README.md). diff --git a/python/sdk/README.zh.md b/python/sdk/README.zh.md index 6ec2685451..9acb8f2612 100644 --- a/python/sdk/README.zh.md +++ b/python/sdk/README.zh.md @@ -1,48 +1,66 @@ # DeepSeek Harness Python SDK -[English](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk/README.md) | 中文 +[English](README.md) | 中文 -通过 JSON-RPC stdio 驱动 DeepSeek Harness 的 Python 子进程 SDK。运行时继承常规的 DeepSeek Harness 环境变量(如 `DEEPSEEK_BASE_URL` 与 `DEEPSEEK_API_KEY`),调用方可以直接使用真实模型端点,也可以把这些变量指向本地代理。 - -请从 PyPI 安装 `deepseek-harness-sdk` 分发包;导入模块仍为 `deepseek_harness`: +用于通过 stdio 上按行分隔的 JSON-RPC 驱动 DeepSeek Harness 的 Python 子进程 SDK。安装 `deepseek-harness-sdk` 时,会同时安装当前平台上版本完全相同的 `deepseek-harness-runtime-bin` wheel。 ```sh python -m pip install deepseek-harness-sdk ``` -安装 `deepseek-harness-sdk` 会同时安装版本完全相同的 `deepseek-harness-runtime-bin` 平台 wheel 包。因此常规入口不需要传可执行文件参数: +## 启动运行时 -```py -from deepseek_harness import DeepSeekHarness +Python SDK 没有独立的应用入口。它以 `--profile sdk` 启动内置的 `dsh` CLI;所选 profile 负责 JSON-RPC 服务器、agent 组合、凭据、持久化、工具和关闭流程。 -with DeepSeekHarness() as harness: - result = harness.run("Say hi.") -``` - -`DeepSeekHarness` 会保留其按需启动的运行时子进程,以便在多次调用之间复用。请像上例一样将其用作上下文管理器,或在使用完毕后显式调用 `close()`。 - -默认情况下,SDK 会启动 `deepseek-harness-runtime-bin` 包内置的单文件可执行程序 `dsh-jsonrpc-agent`,并通过 `DSH_CORDIS_CONFIG` 注入该包的默认配置,其中包括 stdio JSON-RPC 服务器、agent core(智能体核心)、预载的 DeepSeek 适配器、采用显式组合语义检查点策略的 JSONL 会话持久化,以及本地 bash。要运行自己的插件组合,请在配置中保留 `@deepseek-ai/dsh-sdk-jsonrpc-server` 配置项,并传入 Cordis 配置文件路径。 +每次启动都必须显式指定 Harness home。请传入 `dsh_home`,或在子进程环境中提供非空的 `DSH_HOME`。SDK 刻意不会发现 `~/.dsh`。 ```py from deepseek_harness import DeepSeekHarness with DeepSeekHarness( - provider="deepseek-official", - model="deepseek-v4-flash", - max_tokens=49_152, - cordis="examples/python-sdk-agent/cordis.yml", + dsh_home="/absolute/path/to/isolated-dsh-home", + cwd="/absolute/path/to/workspace", +) as harness: + result = harness.run("Say hi.", session_id="example-001") + +print(result.final_response) +``` + +`DeepSeekHarness` 延迟启动运行时,并在调用 `close()` 或退出上下文管理器前复用该进程。首次 profile 握手通过 `initialize_timeout_seconds` 使用独立的 10 秒默认上限;普通轮次在未设置 `request_timeout_seconds` 时仍不设上限。超时诊断会指明所选 profile,并包含保留的运行时诊断。`cwd` 是 agent workspace;`runtime_cwd` 独立选择子进程工作目录。两者都会在启动前转成绝对路径。`provider`、`model` 和可选的正整数 `max_tokens` 通过 JSON-RPC 初始化发送。`base_url` 与 `api_key` 会显式覆盖子进程环境中的 `DEEPSEEK_BASE_URL` 与 `DEEPSEEK_API_KEY`。 + +## 自定义插件 + +持久自定义属于 `dsh` profile。使用运行时 wheel 提供的 `dsh` 命令初始化随附的 SDK profile,并安装外部 bundle: + +```sh +export DSH_HOME=/absolute/path/to/isolated-dsh-home +dsh --profile sdk --dump-default-config >/dev/null +dsh plugin --profile sdk add file:/absolute/path/to/my-plugin-bundle +``` + +`file:` 形式会把本地 bundle 安装到 profile 包树中,使其 peer import 可以到达内置安装后备。Profile manifest 会记录已安装依赖与有序 bundle 层;`$DSH_HOME/profiles/sdk/cordis.patch.yml` 是持久用户 patch。只有管理外部包时,`dsh plugin` 才需要 `pnpm`。运行 SDK 不需要系统 Node.js。 + +对于单次调用的变更,可传入一个或多个 patch 文件。它们会转成绝对路径,并在 profile 层与 home patch 层之后按顺序传给 CLI: + +```py +with DeepSeekHarness( + dsh_home="/absolute/path/to/isolated-dsh-home", + profile="sdk", + patches=("/absolute/path/to/first.patch.yml", "/absolute/path/to/last.patch.yml"), ) as harness: result = harness.run("Make the requested code change.") ``` -`provider` 选择指定 Cordis 组合所注册的提供方路由;`model` 是该适配器解析出的模型 ID。`max_tokens` 是一个可选的正整数,用于限制根 agent 及其进程内后代在每次请求中输出的 token 数量;省略该参数时,由提供方的默认行为决定输出上限。压缩摘要继续使用压缩插件单独配置的上限。内置默认组合注册 `deepseek-official`。自定义组合可以挂载 `llm-pi-ai`,在其中配置各提供方专属的凭据和端点,并选择 pi-ai 已安装 catalog 中存在的任意提供方/模型组合。 +`profile` 可以选择另一个已存在的 profile,但该组合必须保留 `@deepseek-ai/dsh-sdk-app` 或另一个 `@deepseek-ai/dsh-sdk-jsonrpc-server` 配置项。配置错误会在 CLI 启动或 SDK 初始化时失败;不存在完整配置回退。`dsh_bin` 可以选择另一个 `dsh` 可执行程序,同时保持相同的 profile 语法。任意 argv 替换仅是内部 fake-runtime 测试适配器,不属于公开 API。 -[Python SDK 教程](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/guide/python-sdk.md)提供一套无需使用 Web UI、按步骤完成安装和首次运行的流程。该教程所用的完整独立 Cordis 配置文件位于 [`python-sdk-agent` 示例](https://github.com/deepseek-ai/deepseek-harness/blob/master/examples/python-sdk-agent/README.md)中。 +随附的 `sdk-minimal` profile 是独立显式配置树,而不是 `dsh-base` 上的 overlay。使用 `profile="sdk-minimal"` 选择它;普通 `model` 参数是唯一运行时模型选择,也适用于不在适配器建议目录中的模型 id。它提供持久 Bash、字符串替换 editor、本地执行与 JSONL 会话;settings、托管凭据、遥测、Web 工具与完整默认工具清单仍由独立的完整 `sdk` 与 `web` profile 提供。 -`Session.run()` 的活动区间从其提示词被持久 inbox 接收时开始,到整个 agent 下一次进入空闲状态时结束,并返回 `RunResult(session_id, final_response, finish_reason, events, notifications, session_root)`。`final_response` 是该区间内根会话最后提交的助手文本。`finish_reason` 是该区间内根会话最后一个 `turn/end` 的 `kind`,例如 `completed`、`max-tokens` 或 `error`;没有轮次结束时为 `None`。缺少字符串 `data.reason.kind` 的 `turn/end` 违反运行时协议,并会抛出 `SdkProtocolError`。这两个结果字段描述的是 `Session.run()` 所界定的活动区间,并不表示某项输出或结束原因在因果上归属于该提示词。steering(中途引导)、注入的上下文和其他排队工作,也可能在 agent 进入空闲状态前参与这段活动。 +## 结果与通知 -`HarnessClient` 会在运行时进程的整个生命周期内保留已发现的 subagent 谱系。每次执行 `Session.run()` 时,`RunResult.notifications` 与 `on_notification` 会按协议传输顺序收到根会话及所有已知后代的通知,其中包括嵌套 subagent 的生命周期事件与会话事件。`RunResult.events` 只包含根会话事件,因此后代消息不会覆盖根会话回复。底层 `session_prompt()` 会立即返回已排队消息的 `MessageId`;绕过 `Session.run()` 的调用方必须自行负责后续的活动边界。 +`Session.run()` 的活动区间从提示词被持久 inbox 接收时开始,到整个 agent 下一次进入 idle 时结束,并返回 `RunResult(session_id, final_response, finish_reason, events, notifications)`。`final_response` 是该区间内根会话最后提交的 assistant 文本。`finish_reason` 是最后一个根会话 `turn/end` 的 `kind`,例如 `completed`、`max-tokens` 或 `error`;没有轮次结束时为 `None`。缺少字符串 `data.reason.kind` 的 `turn/end` 违反协议,并会抛出 `SdkProtocolError`。 -也可以通过 `DSH_CORDIS_CONFIG` 为运行时子进程指定配置。注入逻辑位于 `HarnessClient.start()`,因此底层客户端按默认方式启动时也具有该行为:如果启动方式最终解析为内置运行时,且既没有设置 `cordis`,也没有设置非空的 `DSH_CORDIS_CONFIG`(运行时将空值视为未设置,注入检查也是如此),系统就会使用内置默认配置;显式指定 `runtime_bin`、`bridge_bin` 或 `launch_args_override` 时,则会完全禁用该注入。运行时载体(生产用 exe 与仅限开发的 `node` 闭包)及其获取方式见 [sdk-runtime README](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk-runtime/README.md)。 +`HarnessClient` 会在运行时进程的整个生命周期内保留已发现的子 agent 祖先关系。在 `Session.run()` 期间,`RunResult.notifications` 与 `on_notification` 按协议顺序接收根会话和已知后代的通知。`RunResult.events` 只包含根会话事件,因此后代输出不会替换根响应。底层 `session_prompt()` 会立即返回已排队消息的 id;绕过 `Session.run()` 的调用方自行负责后续活动边界。 -`cwd` 与 `runtime_cwd` 会在启动子进程、注入环境变量和协议握手前解析为绝对路径。公开 API 只暴露由 SDK 直接应用的选项:部署 persona 和持久化配置应在 `cordis.yml` 中定义;`session_root` 则保留为设置 `DSH_SESSION_ROOT` 的高层便捷参数。 +所选 home 保存 profile、插件与每个 profile 自有的持久资源。完整 `sdk` profile 使用其中的凭据、设置与会话存储;`sdk-minimal` 只使用自己的 JSONL 会话存储。需要隔离这些资源时应使用新的 home;独立工作应使用新的 session id。同时复用 harness 与 session id 会延续持久对话和会话资源。 + +另见 [Python 教程](../../docs/user/guide/python-sdk.zh.md)、[`python-sdk-agent` 示例](../../examples/python-sdk-agent/README.zh.md)和[运行时 wheel 参考](../sdk-runtime/README.zh.md)。 diff --git a/python/sdk/src/deepseek_harness/api.py b/python/sdk/src/deepseek_harness/api.py index 9c542c012a..09286a1ad1 100644 --- a/python/sdk/src/deepseek_harness/api.py +++ b/python/sdk/src/deepseek_harness/api.py @@ -24,11 +24,12 @@ class DeepSeekHarnessConfig: max_tokens: int | None = None cwd: str | None = None runtime_cwd: str | None = None - session_root: str | None = None - cordis: str | None = None + dsh_bin: str | None = None + profile: str = "sdk" + patches: tuple[str, ...] = () + dsh_home: str | None = None env: dict[str, str] = field(default_factory=dict) - runtime_bin: str | None = None - launch_args_override: tuple[str, ...] | None = None + initialize_timeout_seconds: float = 10.0 request_timeout_seconds: float | None = None shutdown_timeout_seconds: float | None = 1.0 base_url: str | None = None @@ -42,7 +43,6 @@ class RunResult: finish_reason: str | None events: list[JsonObject] notifications: list[Notification] - session_root: str | None = None class DeepSeekHarness: @@ -53,7 +53,13 @@ class DeepSeekHarness: :meth:`close` explicitly when finished, so the subprocess is always reaped. """ - def __init__(self, config: DeepSeekHarnessConfig | None = None, **kwargs: object) -> None: + def __init__( + self, + config: DeepSeekHarnessConfig | None = None, + *, + _launch_args: tuple[str, ...] | None = None, + **kwargs: object, + ) -> None: if config is not None and kwargs: raise TypeError("pass either DeepSeekHarnessConfig or keyword options, not both") self.config = config or DeepSeekHarnessConfig(**kwargs) @@ -61,11 +67,6 @@ class DeepSeekHarness: runtime_cwd = str(Path(self.config.runtime_cwd).resolve()) if self.config.runtime_cwd is not None else cwd self._cwd = cwd env = dict(self.config.env) - if self.config.session_root is not None: - env["DSH_SESSION_ROOT"] = self.config.session_root - if self.config.cordis is not None: - env["DSH_CORDIS_CONFIG"] = self.config.cordis - env["DSH_CWD"] = cwd if self.config.base_url is not None: env["DEEPSEEK_BASE_URL"] = self.config.base_url if self.config.api_key is not None: @@ -73,13 +74,17 @@ class DeepSeekHarness: self._client = HarnessClient( HarnessConfig( - runtime_bin=self.config.runtime_bin, - launch_args_override=self.config.launch_args_override, + dsh_bin=self.config.dsh_bin, + profile=self.config.profile, + patches=self.config.patches, + dsh_home=self.config.dsh_home, cwd=runtime_cwd, env=env, + initialize_timeout_seconds=self.config.initialize_timeout_seconds, request_timeout_seconds=self.config.request_timeout_seconds, shutdown_timeout_seconds=self.config.shutdown_timeout_seconds, - ) + ), + _launch_args=_launch_args, ) self._initialized = False @@ -179,7 +184,6 @@ class Session: finish_reason=finish_reason(events), events=events, notifications=notifications, - session_root=self.harness.config.session_root, ) diff --git a/python/sdk/src/deepseek_harness/client.py b/python/sdk/src/deepseek_harness/client.py index 5442c7e144..5978d849ab 100644 --- a/python/sdk/src/deepseek_harness/client.py +++ b/python/sdk/src/deepseek_harness/client.py @@ -25,20 +25,29 @@ NotificationFilter: TypeAlias = Callable[[Notification], bool] class HarnessConfig: """Configuration for launching the local DeepSeek Harness SDK runtime.""" - runtime_bin: str | None = None - bridge_bin: str | None = None - launch_args_override: tuple[str, ...] | None = None + dsh_bin: str | None = None + profile: str = "sdk" + patches: tuple[str, ...] = () + dsh_home: str | None = None cwd: str | None = None env: dict[str, str] | None = None + initialize_timeout_seconds: float = 10.0 request_timeout_seconds: float | None = None shutdown_timeout_seconds: float | None = 1.0 + _launch_args: tuple[str, ...] | None = None class HarnessClient: """Synchronous JSON-RPC client for the DeepSeek Harness SDK runtime over stdio.""" - def __init__(self, config: HarnessConfig | None = None) -> None: + def __init__( + self, + config: HarnessConfig | None = None, + *, + _launch_args: tuple[str, ...] | None = None, + ) -> None: self.config = config or HarnessConfig() + self._launch_args = _launch_args or self.config._launch_args self._proc: subprocess.Popen[str] | None = None self._lock = threading.Lock() self._write_lock = threading.Lock() @@ -65,11 +74,10 @@ class HarnessClient: return with self._lock: self._session_parents.clear() - args = list(self.config.launch_args_override or self._default_launch_args()) env = os.environ.copy() if self.config.env: env.update(self.config.env) - self._inject_bundled_default_config(env) + args = list(self._launch_args or self._default_launch_args(env)) self._proc = subprocess.Popen( args, stdin=subprocess.PIPE, @@ -130,9 +138,24 @@ class HarnessClient: if max_tokens is not None: payload["maxTokens"] = max_tokens try: - return self.request("initialize", payload, response_model=InitializeResponse) - except BaseException: + return self.request( + "initialize", + payload, + response_model=InitializeResponse, + timeout_seconds=self.config.initialize_timeout_seconds, + ) + except TimeoutError as error: self.close() + raise TimeoutError(f"{error}\nselected dsh profile {self.config.profile!r}") from error + except BaseException as error: + self.close() + diagnostics = self._runtime_diagnostics() + if isinstance(error, JsonRpcError) and diagnostics: + raise JsonRpcError( + error.code, + f"{error.message}\n{diagnostics}", + error.data, + ) from error raise def session_prompt( @@ -421,37 +444,35 @@ class HarnessClient: parts.append("stderr tail:\n" + "\n".join(self._stderr_lines)) return "\n".join(parts) - def _default_launch_args(self) -> tuple[str, ...]: - if self.config.runtime_bin is not None: - return (self.config.runtime_bin,) - if self.config.bridge_bin is not None: - return (self.config.bridge_bin,) - try: - from deepseek_harness_runtime import resolve_bundled_launch_args - except ImportError as exc: - raise FileNotFoundError( - "Unable to locate the bundled DeepSeek Harness SDK runtime. " - "Install deepseek-harness-runtime-bin or set HarnessConfig.runtime_bin." - ) from exc - return resolve_bundled_launch_args() + def _default_launch_args(self, env: dict[str, str]) -> tuple[str, ...]: + if self.config.dsh_bin is None: + try: + from deepseek_harness_runtime import resolve_bundled_launch_args + except ImportError as exc: + raise FileNotFoundError( + "Unable to locate the bundled DeepSeek Harness dsh runtime. " + "Install deepseek-harness-runtime-bin." + ) from exc + base = resolve_bundled_launch_args() + else: + base = (str(Path(self.config.dsh_bin).expanduser().resolve()),) - def _inject_bundled_default_config(self, env: dict[str, str]) -> None: - """Inject the default config for a bundled launch with no non-empty config. + if self.config.dsh_home is not None: + if not self.config.dsh_home.strip(): + raise ValueError("HarnessConfig requires a non-empty dsh_home") + env["DSH_HOME"] = str(Path(self.config.dsh_home).expanduser().resolve()) + elif not env.get("DSH_HOME", "").strip(): + raise ValueError( + "HarnessConfig requires an explicit dsh_home or non-empty DSH_HOME; " + "the Python SDK never uses ~/.dsh implicitly" + ) - Both bundled carriers require an explicit config. Explicit runtime, - launch-argument, and config channels remain untouched. - """ - uses_bundled_runtime = ( - self.config.launch_args_override is None - and self.config.runtime_bin is None - and self.config.bridge_bin is None + patches = tuple( + argument + for patch in self.config.patches + for argument in ("--patch", str(Path(patch).expanduser().resolve())) ) - if not uses_bundled_runtime or env.get("DSH_CORDIS_CONFIG"): - return - # _default_launch_args already imported the package or raised its install error. - from deepseek_harness_runtime import bundled_default_config_path - - env["DSH_CORDIS_CONFIG"] = str(bundled_default_config_path()) + return (*base, "--profile", self.config.profile, *patches) def _unsubscribe_notifications(self, subscription_id: str) -> None: with self._lock: diff --git a/python/sdk/tests/manual_sdk_agent_smoke.py b/python/sdk/tests/manual_sdk_agent_smoke.py index c0305d9211..acadb495be 100644 --- a/python/sdk/tests/manual_sdk_agent_smoke.py +++ b/python/sdk/tests/manual_sdk_agent_smoke.py @@ -1,4 +1,4 @@ -"""Drive the repo-source JSON-RPC bin through the SDK and a keyless mock SSE server. +"""Drive the repo-source dsh SDK profile through the SDK and a keyless mock SSE server. Requires ``pnpm install`` but no build. This manual test is not collected by pytest; run ``python tests/manual_sdk_agent_smoke.py``. @@ -16,7 +16,6 @@ from pathlib import Path from typing import Any from deepseek_harness import DeepSeekHarness -from deepseek_harness_runtime import bundled_default_config_path class MockCompletionHandler(BaseHTTPRequestHandler): @@ -43,15 +42,16 @@ class MockCompletionHandler(BaseHTTPRequestHandler): def run_smoke(repo_root: Path, keep_sessions: bool) -> None: - session_root = Path(tempfile.mkdtemp(prefix="dsh-sdk-smoke-sessions-")) - runtime_entry = repo_root / "packages/sdk/python-runtime/src/packaged-bin.ts" + dsh_home = Path(tempfile.mkdtemp(prefix="dsh-sdk-smoke-home-")) + session_root = dsh_home / "sessions" + runtime_entry = repo_root / "apps/cli/src/bin.ts" server = ThreadingHTTPServer(("127.0.0.1", 0), MockCompletionHandler) thread = threading.Thread(target=server.serve_forever, name="mock-openai-compatible-server", daemon=True) thread.start() base_url = f"http://127.0.0.1:{server.server_address[1]}" print(f"repo_root={repo_root}") - print(f"session_root={session_root}") + print(f"dsh_home={dsh_home}") print(f"mock_base_url={base_url}") try: @@ -59,10 +59,18 @@ def run_smoke(repo_root: Path, keep_sessions: bool) -> None: model="sdk-smoke-model", cwd=str(repo_root / "python/sdk"), runtime_cwd=str(repo_root), - session_root=str(session_root), - cordis=str(bundled_default_config_path()), - launch_args_override=("node", "--import", "tsx", str(runtime_entry)), + _launch_args=( + "node", + "--import", + "tsx", + str(runtime_entry), + "--profile", + "sdk", + ), env={ + "DSH_HOME": str(dsh_home), + "DSH_PERMISSION_MODE": "danger-full-access", + "DSH_TELEMETRY_DISABLED": "1", "DEEPSEEK_BASE_URL": base_url, "DEEPSEEK_API_KEY": "sdk-smoke-key", }, @@ -92,10 +100,10 @@ def run_smoke(repo_root: Path, keep_sessions: bool) -> None: server.server_close() if keep_sessions: - print(f"kept_session_root={session_root}") + print(f"kept_dsh_home={dsh_home}") else: - shutil.rmtree(session_root) - print("removed temporary session root") + shutil.rmtree(dsh_home) + print("removed temporary dsh home") def main() -> None: diff --git a/python/sdk/tests/test_bundled_runtime.py b/python/sdk/tests/test_bundled_runtime.py index 52d84cd161..3da5247795 100644 --- a/python/sdk/tests/test_bundled_runtime.py +++ b/python/sdk/tests/test_bundled_runtime.py @@ -1,4 +1,4 @@ -"""Keyless boot tests for the production exe and development node carrier. +"""Keyless boot tests for the production exe and development dsh carrier. Each carrier skips independently when absent. The dummy API key only satisfies adapter loading; initialize and shutdown do not call a model. @@ -6,64 +6,39 @@ adapter loading; initialize and shutdown do not call a model. from __future__ import annotations +import json from pathlib import Path import pytest from deepseek_harness import DeepSeekHarness, HarnessClient, HarnessConfig -from deepseek_harness.errors import TransportClosedError -from deepseek_harness_runtime import resolve_bundled_launch_args +from deepseek_harness.errors import JsonRpcError, TransportClosedError +from deepseek_harness_runtime import RUNTIME_MODE_ENV_VAR, resolve_bundled_launch_args _MODES = ("exe", "node") -_REPO_ROOT = Path(__file__).parents[3] -_MINIMAL_CONFIG = _REPO_ROOT / "examples" / "python-sdk-agent" / "minimal.cordis.yml" - -# The config must include the JSON-RPC serving plugin. -_CORDIS_YML = """\ -- id: sdk-jsonrpc-server - name: '@deepseek-ai/dsh-sdk-jsonrpc-server' -- id: agent-core - name: '@deepseek-ai/dsh-agent-spine-demo' - config: - workspaceContext: false -- id: sessions - name: '@deepseek-ai/dsh-session-persistence-jsonl' - config: - root: './sessions' -- id: session-checkpoints - name: '@deepseek-ai/dsh-session-checkpoint-policy' -- id: subprocess - name: '@deepseek-ai/dsh-subprocess-local' -- id: bash - name: '@deepseek-ai/dsh-bash-local' - config: - cwd: '.' -- id: todo - name: '@deepseek-ai/dsh-tool-todo' - config: - allowParallelInProgress: true -""" -def _launch_args(mode: str) -> tuple[str, ...]: +def _select_mode(mode: str, monkeypatch: pytest.MonkeyPatch) -> None: try: - return resolve_bundled_launch_args(mode) + resolve_bundled_launch_args(mode) except FileNotFoundError as exc: pytest.skip(f"bundled {mode}-mode runtime unavailable on this machine: {exc}") + monkeypatch.setenv(RUNTIME_MODE_ENV_VAR, mode) -def _client(tmp_path: Path, launch_args: tuple[str, ...]) -> HarnessClient: +def _client(tmp_path: Path, mode: str, monkeypatch: pytest.MonkeyPatch, *patches: Path) -> HarnessClient: + _select_mode(mode, monkeypatch) return HarnessClient( HarnessConfig( - launch_args_override=launch_args, + dsh_home=str(tmp_path / "home"), + patches=tuple(str(patch) for patch in patches), cwd=str(tmp_path), env={ - "DSH_CORDIS_CONFIG": "./cordis.yml", - "DSH_SESSION_ROOT": str(tmp_path / "sessions"), - "DSH_CWD": str(tmp_path), # The lazily mounted adapter requires a key even without a model call. "DEEPSEEK_API_KEY": "sk-dummy-for-boot", "DEEPSEEK_BASE_URL": "http://127.0.0.1:9", + "DSH_PERMISSION_MODE": "danger-full-access", + "DSH_TELEMETRY_DISABLED": "1", }, request_timeout_seconds=120, ) @@ -71,34 +46,39 @@ def _client(tmp_path: Path, launch_args: tuple[str, ...]) -> HarnessClient: @pytest.mark.parametrize("mode", _MODES) -def test_bundled_runtime_boots_a_cordis_config(tmp_path: Path, mode: str) -> None: - launch_args = _launch_args(mode) - (tmp_path / "cordis.yml").write_text(_CORDIS_YML) - - with _client(tmp_path, launch_args) as client: +def test_bundled_runtime_boots_the_sdk_profile( + tmp_path: Path, mode: str, monkeypatch: pytest.MonkeyPatch +) -> None: + with _client(tmp_path, mode, monkeypatch) as client: init = client.initialize(provider="deepseek-official", cwd=str(tmp_path), model="deepseek-v4-pro") assert init.serverInfo is not None assert init.serverInfo.name == "deepseek-harness-sdk-runtime" + profile = json.loads((tmp_path / "home" / "profiles" / "sdk" / "package.json").read_text()) + assert profile["dsh"]["profile"]["bundles"] == [ + "@deepseek-ai/dsh-base", + "@deepseek-ai/dsh-sdk-app", + ] @pytest.mark.parametrize("mode", _MODES) -def test_python_sdk_boots_minimal_jsonrpc_config(tmp_path: Path, mode: str) -> None: - launch_args = _launch_args(mode) - model = "minimal-environment-model" +def test_python_sdk_applies_an_ordered_profile_patch( + tmp_path: Path, mode: str, monkeypatch: pytest.MonkeyPatch +) -> None: + _select_mode(mode, monkeypatch) + patch = tmp_path / "persona.patch.yml" + patch.write_text(json.dumps([{ + "id": "system-prompt", + "config": {"persona": "Python SDK ordered patch marker."}, + }])) harness = DeepSeekHarness( - model=model, + model="deepseek-v4-pro", cwd=str(tmp_path), - session_root=str(tmp_path / "sessions"), - cordis=str(_MINIMAL_CONFIG), - env={ - "DSH_MODEL": model, - "DSH_CONTEXT_WINDOW": "1000000", - "DSH_SYSTEM_PROMPT": "You are the Python SDK minimal boot test agent.", - }, + dsh_home=str(tmp_path / "home"), + patches=(str(patch),), + env={"DSH_PERMISSION_MODE": "danger-full-access"}, api_key="sk-dummy-for-boot", base_url="http://127.0.0.1:9", - launch_args_override=launch_args, request_timeout_seconds=120, ) @@ -107,42 +87,20 @@ def test_python_sdk_boots_minimal_jsonrpc_config(tmp_path: Path, mode: str) -> N @pytest.mark.parametrize("mode", _MODES) -def test_bundled_runtime_surfaces_unbundled_plugin_failure(tmp_path: Path, mode: str) -> None: - launch_args = _launch_args(mode) - (tmp_path / "cordis.yml").write_text( - "- id: missing\n name: '@deepseek-ai/dsh-does-not-exist'\n" - ) +def test_bundled_runtime_surfaces_unbundled_plugin_failure( + tmp_path: Path, mode: str, monkeypatch: pytest.MonkeyPatch +) -> None: + patch = tmp_path / "missing.patch.yml" + patch.write_text(json.dumps([{ + "insert": [{"id": "missing", "name": "@deepseek-ai/dsh-does-not-exist"}], + }])) - client = _client(tmp_path, launch_args) + client = _client(tmp_path, mode, monkeypatch, patch) client.start() try: - with pytest.raises((TransportClosedError, TimeoutError)) as excinfo: + with pytest.raises((JsonRpcError, TransportClosedError, TimeoutError)) as excinfo: client.initialize(provider="deepseek-official", cwd=str(tmp_path), model="deepseek-v4-pro") finally: client.close() assert "@deepseek-ai/dsh-does-not-exist" in str(excinfo.value) - - -@pytest.mark.parametrize("mode", _MODES) -@pytest.mark.parametrize("ambient_config", [None, ""], ids=["unset", "empty-counts-as-absent"]) -def test_zero_config_run_injects_bundled_default_cordis_config( - tmp_path: Path, mode: str, ambient_config: str | None, monkeypatch: pytest.MonkeyPatch -) -> None: - _launch_args(mode) # skip early when this carrier is unavailable - monkeypatch.setenv("DSH_RUNTIME_MODE", mode) - if ambient_config is None: - monkeypatch.delenv("DSH_CORDIS_CONFIG", raising=False) - else: - monkeypatch.setenv("DSH_CORDIS_CONFIG", ambient_config) - - harness = DeepSeekHarness( - model="deepseek-v4-pro", - cwd=str(tmp_path), - session_root=str(tmp_path / "sessions"), - api_key="sk-dummy-for-boot", - base_url="http://127.0.0.1:9", - request_timeout_seconds=120, - ) - with harness: - pass diff --git a/python/sdk/tests/test_client.py b/python/sdk/tests/test_client.py index 51c9dacb31..1ad89ffd4b 100644 --- a/python/sdk/tests/test_client.py +++ b/python/sdk/tests/test_client.py @@ -9,7 +9,8 @@ from pathlib import Path import pytest -from deepseek_harness import DeepSeekHarness, HarnessClient, HarnessConfig, Notification, SdkProtocolError +from deepseek_harness import DeepSeekHarness, HarnessClient, HarnessConfig, Notification, RunResult, SdkProtocolError +from deepseek_harness.errors import JsonRpcError def test_high_level_sdk_runs_turn_and_collects_final_response(tmp_path: Path) -> None: @@ -95,9 +96,7 @@ for line in sys.stdin: model="deepseek-v4-flash", max_tokens=4096, cwd=str(tmp_path), - cordis=str(tmp_path / "cordis.yml"), - session_root=str(tmp_path / "sessions"), - launch_args_override=(sys.executable, str(script)), + _launch_args=(sys.executable, str(script)), env={ "ENV_DUMP": str(env_dump), "INIT_DUMP": str(init_dump), @@ -113,9 +112,9 @@ for line in sys.stdin: dumped_env = json.loads(env_dump.read_text()) assert dumped_env["DEEPSEEK_API_KEY"] == "env-key" assert dumped_env["DEEPSEEK_BASE_URL"] == "http://127.0.0.1:4321" - assert dumped_env["DSH_CWD"] == str(tmp_path) - assert dumped_env["DSH_SESSION_ROOT"] == str(tmp_path / "sessions") - assert dumped_env["DSH_CORDIS_CONFIG"] == str(tmp_path / "cordis.yml") + assert dumped_env["DSH_CWD"] is None + assert dumped_env["DSH_SESSION_ROOT"] is None + assert dumped_env["DSH_CORDIS_CONFIG"] is None assert json.loads(init_dump.read_text()) == { "cwd": str(tmp_path), "provider": "deepseek-official", @@ -150,7 +149,7 @@ for line in sys.stdin: seen: list[str] = [] with DeepSeekHarness( - launch_args_override=(sys.executable, str(script)), + _launch_args=(sys.executable, str(script)), cwd=str(tmp_path), ) as harness: session = harness.start_session("main") @@ -188,7 +187,7 @@ for line in sys.stdin: ) with DeepSeekHarness( - launch_args_override=(sys.executable, str(script)), + _launch_args=(sys.executable, str(script)), cwd=str(tmp_path), ) as harness: with pytest.raises( @@ -224,7 +223,7 @@ for line in sys.stdin: with DeepSeekHarness( cwd=".", runtime_cwd=".", - launch_args_override=(sys.executable, str(script)), + _launch_args=(sys.executable, str(script)), env={"CAPTURE": str(capture)}, ): pass @@ -232,7 +231,7 @@ for line in sys.stdin: expected = str(tmp_path.resolve()) assert json.loads(capture.read_text()) == { "process": expected, - "environment": expected, + "environment": None, "wire": expected, } @@ -263,7 +262,7 @@ for line in sys.stdin: ) with DeepSeekHarness( - launch_args_override=(sys.executable, str(script)), + _launch_args=(sys.executable, str(script)), cwd=str(tmp_path), ) as harness: result = harness.run("spawn a helper", session_id="main") @@ -312,7 +311,7 @@ for line in sys.stdin: seen: list[str] = [] with DeepSeekHarness( - launch_args_override=(sys.executable, str(script)), + _launch_args=(sys.executable, str(script)), cwd=str(tmp_path), ) as harness: result = harness.run( @@ -367,7 +366,7 @@ for line in sys.stdin: ) with DeepSeekHarness( - launch_args_override=(sys.executable, str(script)), + _launch_args=(sys.executable, str(script)), cwd=str(tmp_path), ) as harness: result = harness.run("stay in your lane", session_id="main") @@ -401,7 +400,7 @@ for line in sys.stdin: """.strip() ) - with DeepSeekHarness(launch_args_override=(sys.executable, str(script)), cwd=str(tmp_path)) as harness: + with DeepSeekHarness(_launch_args=(sys.executable, str(script)), cwd=str(tmp_path)) as harness: result = harness.run("one turn", session_id="main") assert harness.client._notifications.qsize() == 0 @@ -441,7 +440,7 @@ for line in sys.stdin: """.strip() ) - with DeepSeekHarness(launch_args_override=(sys.executable, str(script)), cwd=str(tmp_path)) as harness: + with DeepSeekHarness(_launch_args=(sys.executable, str(script)), cwd=str(tmp_path)) as harness: first = harness.run("first turn", session_id="main") second = harness.run("second turn", session_id="main") @@ -473,7 +472,7 @@ for line in sys.stdin: ) with HarnessClient( - HarnessConfig(launch_args_override=(sys.executable, str(script))) + HarnessConfig(_launch_args=(sys.executable, str(script))) ) as client: init = client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent") assert init.serverInfo.name == "fake-dsh" @@ -612,7 +611,7 @@ for line in sys.stdin: def broken_filter(_notification: object) -> bool: raise RuntimeError("bad notification filter") - with HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script)))) as client: + with HarnessClient(HarnessConfig(_launch_args=(sys.executable, str(script)))) as client: client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent") with ( client.subscribe_notifications(broken_filter) as broken, @@ -649,7 +648,7 @@ for line in sys.stdin: """.strip() ) - with HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script)))) as client: + with HarnessClient(HarnessConfig(_launch_args=(sys.executable, str(script)))) as client: client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent") with pytest.raises(ValueError): client.session_prompt("main", [{"type": "text", "text": "fix it"}]) @@ -677,7 +676,7 @@ for line in sys.stdin: ) with HarnessClient( - HarnessConfig(launch_args_override=(sys.executable, str(script))) + HarnessConfig(_launch_args=(sys.executable, str(script))) ) as client: client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent") @@ -711,7 +710,7 @@ for line in sys.stdin: ) with HarnessClient( - HarnessConfig(launch_args_override=(sys.executable, str(script))) + HarnessConfig(_launch_args=(sys.executable, str(script))) ) as client: init = client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent") assert init.serverInfo.name == "fake-dsh" @@ -731,8 +730,9 @@ time.sleep(60) with HarnessClient( HarnessConfig( - launch_args_override=(sys.executable, str(script)), - request_timeout_seconds=0.1, + _launch_args=(sys.executable, str(script)), + profile="web", + initialize_timeout_seconds=0.1, ) ) as client: start = time.monotonic() @@ -741,6 +741,7 @@ time.sleep(60) except TimeoutError as exc: assert time.monotonic() - start < 2 assert "bridge is still starting" in str(exc) + assert "profile 'web'" in str(exc) else: raise AssertionError("initialize should time out") @@ -767,7 +768,7 @@ for line in sys.stdin: client = HarnessClient( HarnessConfig( - launch_args_override=(sys.executable, str(script)), + _launch_args=(sys.executable, str(script)), shutdown_timeout_seconds=0.1, ) ) @@ -792,6 +793,7 @@ import sys for line in sys.stdin: msg = json.loads(line) if msg.get("method") == "initialize": + print("initialize diagnostic", file=sys.stderr, flush=True) print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "error": {"code": -32000, "message": "bad initialize"}}), flush=True) elif msg.get("method") == "shutdown": print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True) @@ -799,14 +801,16 @@ for line in sys.stdin: """.strip() ) - client = HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script)))) + client = HarnessClient(HarnessConfig(_launch_args=(sys.executable, str(script)))) client.start() proc = client._proc assert proc is not None - with pytest.raises(Exception, match="bad initialize"): + with pytest.raises(JsonRpcError, match="bad initialize") as excinfo: client.initialize(provider="deepseek-official", cwd=".", model="dsagent") + assert excinfo.value.code == -32000 + assert "initialize diagnostic" in str(excinfo.value) assert proc.wait(timeout=1) is not None assert client._proc is None @@ -824,6 +828,18 @@ def test_public_signatures_omit_unsupported_wire_parameters() -> None: assert "max_tokens" in inspect.signature(HarnessClient.initialize).parameters assert "client_name" not in HarnessConfig.__dataclass_fields__ assert "client_version" not in HarnessConfig.__dataclass_fields__ + assert {"dsh_bin", "profile", "patches", "dsh_home"} <= set( + DeepSeekHarnessConfig.__dataclass_fields__ + ) + assert {"dsh_bin", "profile", "patches", "dsh_home"} <= set( + HarnessConfig.__dataclass_fields__ + ) + assert "initialize_timeout_seconds" in DeepSeekHarnessConfig.__dataclass_fields__ + assert "initialize_timeout_seconds" in HarnessConfig.__dataclass_fields__ + for removed in ("cordis", "session_root", "runtime_bin", "bridge_bin", "launch_args_override"): + assert removed not in DeepSeekHarnessConfig.__dataclass_fields__ + assert removed not in HarnessConfig.__dataclass_fields__ + assert "session_root" not in RunResult.__dataclass_fields__ def test_client_close_is_idempotent_before_and_after_start(tmp_path: Path) -> None: @@ -845,7 +861,7 @@ for line in sys.stdin: """.strip() ) - client = HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script)))) + client = HarnessClient(HarnessConfig(_launch_args=(sys.executable, str(script)))) client.start() client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent") client.close() @@ -865,7 +881,7 @@ sys.exit(42) with HarnessClient( HarnessConfig( - launch_args_override=(sys.executable, str(script)), + _launch_args=(sys.executable, str(script)), request_timeout_seconds=2, ) ) as client: @@ -897,7 +913,7 @@ with open(os.environ["SEEN"], "w") as seen: with HarnessClient( HarnessConfig( - launch_args_override=(sys.executable, str(script)), + _launch_args=(sys.executable, str(script)), env={"SEEN": str(output)}, ) ) as client: @@ -915,21 +931,22 @@ with open(os.environ["SEEN"], "w") as seen: json.loads(line) -def _install_fake_bundled_runtime( +def _install_fake_bundled_dsh( tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> Path: - """Install a fake runtime package that records config and serves lifecycle calls. - - Returns the fake bundled default config path. - """ - runtime = tmp_path / "dsh-jsonrpc-agent" +) -> None: + """Install a fake runtime package that records dsh argv and serves lifecycle calls.""" + runtime = tmp_path / "dsh.py" runtime.write_text( - """#!/usr/bin/env python3 + """ import json import os import sys -json.dump({"DSH_CORDIS_CONFIG": os.environ.get("DSH_CORDIS_CONFIG")}, open(os.environ["ENV_DUMP"], "w")) +json.dump({ + "argv": sys.argv[1:], + "DSH_HOME": os.environ.get("DSH_HOME"), + "DSH_CORDIS_CONFIG": os.environ.get("DSH_CORDIS_CONFIG"), +}, open(os.environ["ENV_DUMP"], "w")) for line in sys.stdin: msg = json.loads(line) if msg.get("method") == "initialize": @@ -939,58 +956,75 @@ for line in sys.stdin: break """.strip() ) - runtime.chmod(0o755) - default_config = tmp_path / "default-cordis.yml" module_dir = tmp_path / "deepseek_harness_runtime" module_dir.mkdir() (module_dir / "__init__.py").write_text( f""" def resolve_bundled_launch_args(mode=None): - return ({str(runtime)!r},) - - -def bundled_default_config_path(): - return {str(default_config)!r} + return ({sys.executable!r}, {str(runtime)!r}) """.strip() ) monkeypatch.syspath_prepend(str(tmp_path)) monkeypatch.delitem(sys.modules, "deepseek_harness_runtime", raising=False) - return default_config -@pytest.mark.parametrize("ambient_config", [None, ""], ids=["unset", "empty-counts-as-absent"]) -def test_client_default_launch_uses_bundled_runtime_and_injects_default_config( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ambient_config: str | None -) -> None: - env_dump = tmp_path / "env.json" - default_config = _install_fake_bundled_runtime(tmp_path, monkeypatch) - if ambient_config is None: - monkeypatch.delenv("DSH_CORDIS_CONFIG", raising=False) - else: - monkeypatch.setenv("DSH_CORDIS_CONFIG", ambient_config) - - with HarnessClient(HarnessConfig(env={"ENV_DUMP": str(env_dump)})) as client: - init = client.initialize(provider="deepseek-official", cwd="/workspace", model="deepseek-v4-pro") - - assert init.serverInfo.name == "bundled-runtime" - assert json.loads(env_dump.read_text())["DSH_CORDIS_CONFIG"] == str(default_config) - - -def test_client_respects_explicit_config_over_bundled_default( +def test_client_default_launch_uses_bundled_dsh_sdk_profile_and_explicit_home( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: env_dump = tmp_path / "env.json" - _install_fake_bundled_runtime(tmp_path, monkeypatch) + home = tmp_path / "home" + patch = tmp_path / "sdk.patch.yml" + patch.write_text("[]\n") + _install_fake_bundled_dsh(tmp_path, monkeypatch) + monkeypatch.chdir(tmp_path) + monkeypatch.setenv("DSH_HOME", str(tmp_path / "ambient-home")) monkeypatch.delenv("DSH_CORDIS_CONFIG", raising=False) + with HarnessClient(HarnessConfig( + profile="sdk", + patches=("sdk.patch.yml",), + dsh_home=str(home), + env={"ENV_DUMP": str(env_dump), "DSH_HOME": str(tmp_path / "env-home")}, + )) as client: + init = client.initialize(provider="deepseek-official", cwd="/workspace", model="deepseek-v4-pro") + + assert init.serverInfo.name == "bundled-runtime" + assert json.loads(env_dump.read_text()) == { + "argv": ["--profile", "sdk", "--patch", str(patch)], + "DSH_HOME": str(home), + "DSH_CORDIS_CONFIG": None, + } + + +def test_client_accepts_explicit_environment_dsh_home( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + env_dump = tmp_path / "env.json" + home = tmp_path / "environment-home" + _install_fake_bundled_dsh(tmp_path, monkeypatch) + with HarnessClient( - HarnessConfig(env={"ENV_DUMP": str(env_dump), "DSH_CORDIS_CONFIG": "./explicit.yml"}) + HarnessConfig(profile="custom", env={"ENV_DUMP": str(env_dump), "DSH_HOME": str(home)}) ) as client: client.initialize(provider="deepseek-official", cwd="/workspace", model="deepseek-v4-pro") - assert json.loads(env_dump.read_text())["DSH_CORDIS_CONFIG"] == "./explicit.yml" + assert json.loads(env_dump.read_text()) == { + "argv": ["--profile", "custom"], + "DSH_HOME": str(home), + "DSH_CORDIS_CONFIG": None, + } + + +def test_client_rejects_an_implicit_default_dsh_home( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _install_fake_bundled_dsh(tmp_path, monkeypatch) + monkeypatch.delenv("DSH_HOME", raising=False) + + with pytest.raises(ValueError, match="explicit dsh_home or non-empty DSH_HOME"): + HarnessClient(HarnessConfig(env={})).start() def test_client_reports_missing_bundled_runtime_dependency(monkeypatch: pytest.MonkeyPatch) -> None: @@ -998,4 +1032,4 @@ def test_client_reports_missing_bundled_runtime_dependency(monkeypatch: pytest.M monkeypatch.setattr(sys, "path", []) with pytest.raises(FileNotFoundError, match="Install deepseek-harness-runtime-bin"): - HarnessClient().start() + HarnessClient(HarnessConfig(dsh_home="/explicit/home")).start() diff --git a/python/sdk/tests/test_release_version.py b/python/sdk/tests/test_release_version.py index b21a485949..deaa65b8a8 100644 --- a/python/sdk/tests/test_release_version.py +++ b/python/sdk/tests/test_release_version.py @@ -59,6 +59,7 @@ def test_pep440_version_spells_a_prerelease_the_python_way() -> None: def test_macos_wheel_tag_does_not_claim_unsupported_node_platforms() -> None: assert build_python_release.PLATFORMS["macos-arm64"][0] == "macosx_14_0_arm64" + assert build_python_release.PLATFORMS["macos-arm64"][1] == "deepseek-harness-sdk-runtime-macos-arm64" def test_platform_manifest_rejects_incomplete_entries(tmp_path: Path) -> None: @@ -88,7 +89,7 @@ def test_stage_sdk_keeps_distribution_module_and_runtime_pin_distinct(tmp_path: 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 = tmp_path / f"deepseek-harness-sdk-runtime-{target}" executable.write_bytes(b"runtime") executable.chmod(0o755) expected = {executable.name: b"runtime"} @@ -106,10 +107,14 @@ def test_stage_runtime_copies_platform_payload( build_python_release.stage_runtime(destination, "1.2.3", executable, executable.name) runtime_dir = destination / "src" / "deepseek_harness_runtime" / "runtime" - assert {path.name: path.read_bytes() for path in runtime_dir.glob("dsh-jsonrpc-agent-pkg-*")} == expected + assert { + path.name: path.read_bytes() + for path in runtime_dir.glob("deepseek-harness-sdk-runtime-*") + } == expected pyproject = (destination / "pyproject.toml").read_text() assert 'license = "MIT"' in pyproject assert 'license-files = ["LICENSE", "THIRD_PARTY_NOTICES.md"]' in pyproject + assert 'dsh = "deepseek_harness_runtime:main"' in pyproject assert (destination / "platforms.json").read_bytes() == ( ROOT / "python" / "sdk-runtime" / "platforms.json" ).read_bytes() diff --git a/python/sdk/tests/test_runtime_resolution.py b/python/sdk/tests/test_runtime_resolution.py index 14e90f3283..fc54171b75 100644 --- a/python/sdk/tests/test_runtime_resolution.py +++ b/python/sdk/tests/test_runtime_resolution.py @@ -9,21 +9,12 @@ import pytest from deepseek_harness_runtime import ( RUNTIME_MODE_ENV_VAR, - bundled_default_config_path, bundled_package_dir, + main, resolve_bundled_launch_args, ) -def test_default_config_is_shipped_with_the_package() -> None: - path = bundled_default_config_path() - assert path == bundled_package_dir() / "runtime" / "cordis.yml" - config = path.read_text() - assert "@deepseek-ai/dsh-agent-spine-demo" in config - assert "@deepseek-ai/dsh-session-persistence-jsonl" in config - assert "@deepseek-ai/dsh-session-checkpoint-policy" in config - - def test_unknown_explicit_mode_fails_loud() -> None: with pytest.raises(ValueError, match="expected 'exe' or 'node'"): resolve_bundled_launch_args("bogus") @@ -49,10 +40,10 @@ def test_runtime_requires_spawn_helper_only_on_macos( ) -> None: runtime_dir = tmp_path / "runtime" runtime_dir.mkdir() - linux = runtime_dir / "dsh-jsonrpc-agent-pkg-linux-x64" + linux = runtime_dir / "deepseek-harness-sdk-runtime-linux-x64" linux.touch() Path(f"{linux}-rg").touch() - macos = runtime_dir / "dsh-jsonrpc-agent-pkg-macos-arm64" + macos = runtime_dir / "deepseek-harness-sdk-runtime-macos-arm64" macos.touch() Path(f"{macos}-rg").touch() monkeypatch.setattr(runtime, "bundled_package_dir", lambda: tmp_path) @@ -69,9 +60,55 @@ def test_runtime_requires_ripgrep_sidecar( ) -> None: runtime_dir = tmp_path / "runtime" runtime_dir.mkdir() - (runtime_dir / "dsh-jsonrpc-agent-pkg-linux-x64").touch() + (runtime_dir / "deepseek-harness-sdk-runtime-linux-x64").touch() monkeypatch.setattr(runtime, "bundled_package_dir", lambda: tmp_path) monkeypatch.setattr(runtime, "_current_platform_tag", lambda: "linux-x64") with pytest.raises(FileNotFoundError, match="ripgrep sidecar"): runtime.bundled_runtime_path() + + +def test_node_mode_runs_the_deployed_dsh_cli( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + bin_js = tmp_path / "runtime" / "node" / "node_modules" / "@deepseek-ai" / "dsh" / "lib" / "bin.js" + bin_js.parent.mkdir(parents=True) + bin_js.touch() + monkeypatch.setattr(runtime, "bundled_package_dir", lambda: tmp_path) + monkeypatch.setattr(runtime.shutil, "which", lambda _name: "/node") + + assert resolve_bundled_launch_args("node") == ("/node", str(bin_js)) + + +def test_python_dsh_command_requires_explicit_home( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + monkeypatch.delenv("DSH_HOME", raising=False) + + with pytest.raises(SystemExit) as excinfo: + main() + + assert excinfo.value.code == 2 + assert "explicit DSH_HOME" in capsys.readouterr().err + + +def test_python_dsh_command_executes_the_bundled_cli( + monkeypatch: pytest.MonkeyPatch +) -> None: + called: dict[str, object] = {} + monkeypatch.setenv("DSH_HOME", "/explicit/home") + monkeypatch.setattr(runtime, "resolve_bundled_launch_args", lambda: ("/runtime",)) + monkeypatch.setattr(runtime.sys, "argv", ["dsh", "plugin", "--profile", "sdk", "list"]) + + def execvpe(file: str, args: tuple[str, ...], env: dict[str, str]) -> None: + called.update(file=file, args=args, home=env.get("DSH_HOME")) + + monkeypatch.setattr(runtime.os, "execvpe", execvpe) + + main() + + assert called == { + "file": "/runtime", + "args": ("/runtime", "plugin", "--profile", "sdk", "list"), + "home": "/explicit/home", + } diff --git a/scripts/build-exe-for-python-sdk-assets.spec.ts b/scripts/build-exe-for-python-sdk-assets.spec.ts new file mode 100644 index 0000000000..27167c5307 --- /dev/null +++ b/scripts/build-exe-for-python-sdk-assets.spec.ts @@ -0,0 +1,27 @@ +import { spawnSync } from 'node:child_process' +import { resolve } from 'node:path' +import { describe, expect, it } from 'vitest' + +const root = resolve(import.meta.dirname, '..') +const script = resolve(root, 'scripts/build-exe-for-python-sdk.ts') + +describe('Python runtime executable assets', () => { + it('packages the dynamically resolved web frontend distribution', () => { + const result = spawnSync(process.execPath, [ + '--import', + 'tsx/esm', + script, + '--skip-build', + '--dry-run', + '--targets=node24-macos-arm64', + ], { + cwd: root, + encoding: 'utf8', + env: { ...process.env, npm_execpath: 'C:\\tools\\pnpm.cjs' }, + }) + + expect(result.status).toBe(0) + expect(result.stdout).toContain('node_modules/@deepseek-ai/dsh-web-frontend/dist/**/*') + expect(result.stdout).toContain('node_modules/@deepseek-ai/dsh-skill-badge/assets/**/*') + }) +}) diff --git a/scripts/build-exe-for-python-sdk.ts b/scripts/build-exe-for-python-sdk.ts index 7516fcabb6..c8c30b4301 100644 --- a/scripts/build-exe-for-python-sdk.ts +++ b/scripts/build-exe-for-python-sdk.ts @@ -1,5 +1,5 @@ /** - * Build the SDK runtime executables and Python node carrier. The fixed + * Build the dsh executables and development Node carrier for the Python runtime wheel. The fixed * `@yao-pkg/pkg --sea` route, deploy flags, and artifact layout are owned by * .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md. * The staged closure is symlink-free, and whole-tree assets cover Cordis's @@ -16,11 +16,11 @@ import { resolveLinuxNodePtyAddon } from './build-exe-for-python-sdk-native-pty. const root = resolve(import.meta.dirname, '..') /** The closure manifest whose dependencies define the executable. */ -const DEPLOY_ROOT_PACKAGE = 'dsh-sdk-python-runtime-closure' -/** The closed-runtime app entry inside the deployed closure. */ -const ENTRY_BIN = 'node_modules/@deepseek-ai/dsh-sdk-python-runtime/lib/packaged-bin.js' -/** Stable Python-visible executable basename; rename with the later Python runtime migration. */ -const OUTPUT_BASENAME = 'dsh-jsonrpc-agent-pkg' +const DEPLOY_ROOT_PACKAGE = 'dsh-python-runtime-closure' +/** The sole application launcher inside the deployed closure. */ +const ENTRY_BIN = 'node_modules/@deepseek-ai/dsh/lib/bin.js' +/** Python-visible executable basename. */ +const OUTPUT_BASENAME = 'deepseek-harness-sdk-runtime' /** Default Node major; SEA mode requires at least Node 22. */ const DEFAULT_NODE_RANGE = 'node24' /** Pinned for reproducible builds. */ @@ -47,8 +47,20 @@ const ASSET_GLOBS = [ 'node_modules/**/*.mjs', 'node_modules/**/package.json', 'node_modules/**/*.json', + // Package-owned Markdown includes runtime skill instructions and badge content. + 'node_modules/**/*.md', + 'node_modules/**/*.dylib', + 'node_modules/**/*.dll', 'node_modules/**/*.node', + 'node_modules/**/*.so', + 'node_modules/**/*.so.*', 'node_modules/**/*.wasm', + 'node_modules/**/*.yaml', + 'node_modules/**/*.yml', + // web-app builds this path dynamically, so pkg cannot discover the static frontend. + 'node_modules/@deepseek-ai/dsh-web-frontend/dist/**/*', + // skill-badge resolves both Markdown and image resources through import.meta.url. + 'node_modules/@deepseek-ai/dsh-skill-badge/assets/**/*', ] const PLATFORMS = ['linux', 'macos'] as const @@ -220,8 +232,7 @@ function formatCommand(command: string, args: string[]): string { */ class SingleExeBuild { /** - * The cleared deploy target, pkg input, and Python node-mode carrier. The - * checked-in default `cordis.yml` remains in its parent directory. + * The cleared deploy target, pkg input, and Python node-mode carrier. */ readonly staging = resolve(root, PYTHON_RUNTIME_DIR, PYTHON_NODE_SUBDIR) private readonly outDir = resolve(root, OUT_DIR) diff --git a/scripts/build-python-release.py b/scripts/build-python-release.py index 0b4157f40d..c546c6bb42 100644 --- a/scripts/build-python-release.py +++ b/scripts/build-python-release.py @@ -150,7 +150,7 @@ def copy_package(source: Path, destination: Path) -> None: "*.pyc", "dist", "node_modules", - "dsh-jsonrpc-agent-pkg-*", + "deepseek-harness-sdk-runtime-*", ), ) @@ -246,7 +246,7 @@ def verify_wheel( f"{wheel} has license files {license_files}, expected {expected_license_files}" ) runtime_files = [ - name for name in archive.namelist() if "/runtime/dsh-jsonrpc-agent-pkg-" in name + name for name in archive.namelist() if "/runtime/deepseek-harness-sdk-runtime-" in name ] if package == "runtime": assert platform is not None diff --git a/scripts/check-workspace-constraints.spec.ts b/scripts/check-workspace-constraints.spec.ts index 4bd39ac878..dc3b63227f 100644 --- a/scripts/check-workspace-constraints.spec.ts +++ b/scripts/check-workspace-constraints.spec.ts @@ -1,12 +1,9 @@ /** Experimental-package publication and dependency constraints. */ -import { readFileSync } from 'node:fs' -import { join } from 'node:path' import { describe, expect, it } from 'vitest' import { checkExperimentalDependencyIsolation, checkExperimentalManifest, - checkWorkspaceManifest, type WorkspaceManifest, } from './check-workspace-constraints.ts' @@ -77,24 +74,3 @@ describe('experimental workspace constraints', () => { ]) }) }) - -describe('private Python runtime carrier', () => { - const manifest = JSON.parse( - readFileSync(new URL('../packages/sdk/python-runtime/package.json', import.meta.url), 'utf8'), - ) as WorkspaceManifest['manifest'] - - it('participates in dsh package checks without becoming an npm release member', () => { - expect(checkWorkspaceManifest({ dir: 'packages/sdk/python-runtime', manifest })).toEqual([]) - }) - - it('rejects publication metadata on the private carrier', () => { - const path = join('packages', 'sdk', 'python-runtime', 'package.json') - expect(checkWorkspaceManifest({ - dir: 'packages/sdk/python-runtime', - manifest: { ...manifest, private: false, publishConfig: { access: 'public' } }, - })).toEqual([ - `${path}: @deepseek-ai/dsh-sdk-python-runtime: private carrier must set "private": true`, - `${path}: @deepseek-ai/dsh-sdk-python-runtime: private carrier must omit publishConfig`, - ]) - }) -}) diff --git a/scripts/check-workspace-constraints.ts b/scripts/check-workspace-constraints.ts index 842e0ca3a7..cd9620e0fa 100644 --- a/scripts/check-workspace-constraints.ts +++ b/scripts/check-workspace-constraints.ts @@ -54,9 +54,6 @@ const experimentalPackageDirectory = /^packages\/experimental\/[^/]+$/ const experimentalPackageNamePrefix = '@deepseek-ai/dsh-experimental-' /** Directories whose packages this repository publishes: one release member each. */ const releaseMemberDirectory = /^(?:packages\/(?!experimental\/)[^/]+\/[^/]+|apps\/[^/]+|vendor\/[^/]+)$/ -/** Named dsh packages that remain private because another distribution embeds them. */ -const privateCarrierDirectories = new Set(['packages/sdk/python-runtime']) - const localArtifactDirs = new Set(['node_modules']) const appPackageFiles: Readonly> = { '@deepseek-ai/dsh': ['lib/*.js'], @@ -156,8 +153,9 @@ const packageFileExtras: Readonly> = { '@deepseek-ai/dsh-code-runtime-python': ['py/**/*.py'], // The shipped preset compositions travel inside the roster package. '@deepseek-ai/dsh-agent-presets': ['presets'], - // The private Python carrier ships only its closed-resolution entry. - '@deepseek-ai/dsh-sdk-python-runtime': ['lib/packaged-bin.js'], + // The Web Host mounts the default-off settings owner independently of each + // Agent-scoped delegation-tool instance. + '@deepseek-ai/dsh-tool-subagent': ['lib/model-selection-settings.js'], // The argv-prefix runner entry ships beside the lib as its own bundle; // sandbox-local resolves it through the package's ./runner export. tsdown // also shares its generated FFI code through a hashed runtime chunk. @@ -292,13 +290,6 @@ export function checkWorkspaceManifest({ dir, manifest }: WorkspaceManifest): st || manifest.repository.directory !== expectedDirectory) { errors.push(`${label}: published Landlock package repository must use ${repositoryUrl} with directory ${expectedDirectory} for trusted publishing`) } - } else if (privateCarrierDirectories.has(dir)) { - if (manifest.private !== true) { - errors.push(`${label}: private carrier must set "private": true`) - } - if (manifest.publishConfig !== undefined) { - errors.push(`${label}: private carrier must omit publishConfig`) - } } else if (releaseMemberDirectory.test(dir)) { // Release members state that they are publishable: npm refuses a private // package, and the repository field is how a consumer finds the source of diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index db52420840..05a4c8947c 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -100,6 +100,7 @@ export const SERVICE_PAGE: Record = { spillStore: 'spill.md', storage: 'storage.md', storageDomain: 'storage.md', + subagentModelSelection: 'subagent.md', subagents: 'subagent.md', subprocess: 'subprocess.md', systemPrompt: 'system-prompt.md', diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index 79407ece40..a47f94b943 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -205,6 +205,14 @@ const SERVICE_ROLES: ServiceRole[] = [ consumers: ['llm-deepseek', 'llm-pi-ai', 'apiproxy'], note: 'Plugins register namespace schemas and resolve layered values; providers store the raw document. The LLM adapters register their entry config as the composition base under the user section; the web gateway serves redacted layered descriptors and writes the user layer.', }, + { + key: 'subagentModelSelection', + pkg: 'tool-subagent', + title: 'Subagent model-selection preference', + mode: 'core', + consumers: ['tool-subagent'], + note: 'Owns the default-off settings namespace that Agent-scoped delegation tools sample when composing a new top-level Session.', + }, { key: 'credentials', pkg: 'credentials', @@ -763,7 +771,7 @@ const APP_EXAMPLES = [ title: 'DSH Base Composition', label: 'packages/bundle/base/cordis.patch.yml', config: 'packages/bundle/base/cordis.patch.yml', - summary: 'The dsh-base bundle patch every profile applies first; mode bundles (dsh-web-app, dsh-headless) and the user\'s profile layer patch over it.', + summary: 'The dsh-base bundle patch shared by the web, headless, sdk, and acp profiles; their mode bundles and user layers patch over it, while sdk-minimal owns a separate standalone tree.', }, { id: 'headless', diff --git a/scripts/gen-tool-catalog.ts b/scripts/gen-tool-catalog.ts index 19ab10c455..3d69da2cc3 100644 --- a/scripts/gen-tool-catalog.ts +++ b/scripts/gen-tool-catalog.ts @@ -9,6 +9,7 @@ import { globSync, readFileSync, writeFileSync } from 'node:fs' import { basename, resolve } from 'node:path' import { Context } from '@deepseek-ai/cordis' +import LlmRuntime from '@deepseek-ai/dsh-llm' import type { ToolSchema } from '@deepseek-ai/dsh-llm' import AgentRegistry from '@deepseek-ai/dsh-agent' import type { Agent } from '@deepseek-ai/dsh-agent' @@ -104,7 +105,7 @@ const OUT = 'docs/tool-catalog.md' function registerCatalogSubagentProvider(ctx: Context, name: string): void { const provider: SubagentProvider = { name, - capabilities: { outputSchema: true, depthLimit: true, toolFilter: true, persona: true }, + capabilities: { agentOptions: true, outputSchema: true, depthLimit: true, toolFilter: true, persona: true }, inheritsParentContext: false, start: () => Promise.reject(new Error('tool-catalog provider cannot start a child')), // Declared so consumers configured for continuable background mode mount. @@ -455,17 +456,21 @@ const TOOL_PACKAGES: ToolPackage[] = [ { pkg: '@deepseek-ai/dsh-tool-subagent', dir: 'tool-subagent', - source: 'packages/subagent/tool-subagent/src/index.ts', - requires: ['ctx.tools', 'ctx.subagents', 'ctx.systemPrompt'], + source: { + list_subagent_models: 'packages/subagent/tool-subagent/src/list-models.ts', + subagent: 'packages/subagent/tool-subagent/src/index.ts', + }, + requires: ['ctx.tools', 'ctx.subagents', 'ctx.systemPrompt', 'ctx.llm for model discovery and selected-route validation'], writes: ['tool/call', 'tool/result', 'child session events through the chosen provider'], shippedNames: ['subagent', 'subagent_fork'], async mount(ctx) { await ctx.plugin(SubagentRuntime) + await ctx.plugin(LlmRuntime) registerCatalogSubagentProvider(ctx, 'mock') - await ctx.plugin(ToolSubagent, { provider: 'mock' }) + await ctx.plugin(ToolSubagent, { provider: 'mock', enableModelSelection: true }) }, note: - 'The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped compositions load this package once per subagent backend, so the model additionally sees `subagent_fork` bound to the fork backend. Each instance\'s description, `run_in_background` parameter, and system-prompt policy follow its own `backgroundMode` and `enableRunInBackground`, so the two shipped schemas are not identical: `subagent` is `continuable` and defaults omitted calls to background with automatic settlement delivery, while `subagent_fork` stays `one-shot` and defaults them to foreground — see `packages/bundle/base/cordis.patch.yml` and `examples/acp-agent/cordis.yml`.', + 'The registered delegation name is the load-time `toolName` config (default `subagent`); the schema above shows static model selection enabled for reference. Model selection defaults off. Web presets sample the default-off Models preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` remains fixed-route. Explicit compositions may instead use static `enableModelSelection`. Each instance independently controls model selection, discovery ownership, and background behavior through `enableModelSelection`, `modelSelectionSettings`, `backgroundMode`, and `enableRunInBackground`.', }, { pkg: '@deepseek-ai/dsh-tool-subagent-control', diff --git a/scripts/smoke-python-runtime.py b/scripts/smoke-python-runtime.py index 71e7100268..4a5a3eb0be 100644 --- a/scripts/smoke-python-runtime.py +++ b/scripts/smoke-python-runtime.py @@ -12,6 +12,7 @@ import os import queue import subprocess import sys +import sysconfig import tempfile import threading import time @@ -37,14 +38,35 @@ FS_SEARCH_TEXT = "filesystem search smoke ok" FS_SEARCH_MARKER = "PACKAGED_FS_SEARCH_OK" MCP_PROMPT = "Exercise the packaged MCP client with one external stdio server." MCP_TEXT = "MCP client smoke ok" -MINIMAL_CORDIS = ( - Path(__file__).resolve().parent.parent / "examples" / "python-sdk-agent" / "minimal.cordis.yml" -) +PROFILE_PLUGIN_PROMPT = "Verify the Python-installed dsh profile plugin." +PROFILE_PLUGIN_TEXT = "profile plugin smoke ok" +PROFILE_PLUGIN_MARKER = "PYTHON_INSTALLED_DSH_PROFILE_PLUGIN" MINIMAL_BASH_COMMAND = ( "counter=$(( ${counter:-0} + 1 )); export counter; " "printf 'COUNT=%s CWD=%s\\n' \"$counter\" \"$PWD\"; " "if [ \"$counter\" -eq 1 ]; then cd /tmp; fi" ) +LEGACY_CUSTOM_DISABLED_ROWS = ( + "agent-instructions", + "goal", + "goal-round-driver", + "command-goal", + "plan-mode", + "skill", + "skill-filesystem", + "tool-fs", + "tool-fs-search", + "tool-goal", + "tool-ralph", + "tool-skill", + "tool-str-replace-editor", + "tool-subagent-control", + "tool-subagent-list-agents", + "tool-subagent-fork", + "tool-subagent-report", + "tool-todo", + "tool-web", +) SNAPSHOT_PROMPT = "Run the advanced packaged-runtime snapshot scenario." SNAPSHOT_SESSION_ID = "advanced-executable" SNAPSHOT_DIRECT_CHILD_PROMPT = "Reply with exactly DIRECT_CHILD_OK and nothing else." @@ -91,79 +113,6 @@ RESTART_SNAPSHOT_DIRECTORY = ( Path(__file__).resolve().parent / "snapshots" / "python-sdk-single-exe" / "restart" ) RESTART_SNAPSHOT_FILENAMES = ("result.json", "requests.json", "session.1.jsonl", "session.2.jsonl") -# The agent loop's dynamic runtime-context snapshot is the one model-visible message this -# expected output cannot carry: the same composition emits it on macOS and not on Linux -# (deepseek-harness#2488), and the file must replay on both. Everything else is compared. -RUNTIME_CONTEXT_PREFIX = "Current runtime context" -CUSTOM_CORDIS = """\ -- id: sdk-jsonrpc-server - name: '@deepseek-ai/dsh-sdk-jsonrpc-server' -- id: deepseek-llm-api-extensions - name: '@deepseek-ai/dsh-deepseek-llm-api-extensions' -- id: session-log-deepseek - name: '@deepseek-ai/dsh-session-log-deepseek' - config: - enabled: true -- id: agent-core - name: '@deepseek-ai/dsh-agent-spine-demo' - config: - workspaceContext: false - skills: - enabled: false - toolBash: false - tools: - mode: both -- id: sessions - name: '@deepseek-ai/dsh-session-persistence-jsonl' - config: - root: !!js process.env.DSH_SESSION_ROOT - compression: 'none' -- id: code-runtime - name: '@deepseek-ai/dsh-code-runtime-worker-thread' -- id: subagents - name: '@deepseek-ai/dsh-subagent' -- id: subagent-spawn-in-process - name: '@deepseek-ai/dsh-subagent-spawn-in-process' - config: - providerName: spawn -- id: subagent-tool - name: '@deepseek-ai/dsh-tool-subagent' - config: - provider: spawn -- id: workflow-engine - name: '@deepseek-ai/dsh-workflow-worker-thread' - config: - provider: spawn -- id: workflow-tool - name: '@deepseek-ai/dsh-tool-workflow' -- id: cordis-host-runner - name: '@deepseek-ai/dsh-cordis-host-runner' -- id: cordis-tool - name: '@deepseek-ai/dsh-tool-cordis' -""" -FS_SEARCH_CORDIS = """\ -- id: sdk-jsonrpc-server - name: '@deepseek-ai/dsh-sdk-jsonrpc-server' -- id: agent-core - name: '@deepseek-ai/dsh-agent-spine-demo' - config: - workspaceContext: false - skills: - enabled: false - toolBash: false - toolJobs: false -- id: sessions - name: '@deepseek-ai/dsh-session-persistence-jsonl' - config: - root: !!js process.env.DSH_SESSION_ROOT - compression: 'none' -- id: subprocess - name: '@deepseek-ai/dsh-subprocess-local' -- id: fs-search - name: '@deepseek-ai/dsh-tool-fs-search' - config: - sampleOverCapGlobResults: false -""" MCP_SERVER_SCRIPT = """\ import json import os @@ -242,28 +191,29 @@ for line in sys.stdin: """ -def mcp_cordis(server_script: Path) -> str: - """Build an external config that mounts the packaged MCP client.""" - return json.dumps([ +def write_profile_patch( + root: Path, + name: str, + sessions: Path, + patches: list[dict[str, object]], +) -> Path: + """Write one JSON-form dsh profile patch with deterministic persistence.""" + path = root / name + path.write_text(json.dumps([ { - "id": "sdk-jsonrpc-server", - "name": "@deepseek-ai/dsh-sdk-jsonrpc-server", + "id": "session-persistence-jsonl", + "config": {"root": str(sessions), "compression": "none"}, }, - { - "id": "agent-core", - "name": "@deepseek-ai/dsh-agent-spine-demo", - "config": { - "workspaceContext": False, - "skills": {"enabled": False}, - "toolBash": False, - }, - }, - { - "id": "sessions", - "name": "@deepseek-ai/dsh-session-persistence-jsonl", - "config": {"root": "./sessions", "compression": "none"}, - }, - { + {"id": "session-telemetry-otel", "disabled": True}, + *patches, + ], indent=2)) + return path + + +def write_mcp_patch(root: Path, sessions: Path, server_script: Path) -> Path: + """Write a profile patch that mounts the packaged MCP client.""" + return write_profile_patch(root, "mcp.patch.yml", sessions, [{ + "insert": [{ "id": "mcp-fixture", "name": "@deepseek-ai/dsh-mcp-client", "config": { @@ -275,8 +225,8 @@ def mcp_cordis(server_script: Path) -> str: "failOnStartupError": True, "reconnect": {"enabled": False}, }, - }, - ], indent=2) + }], + }]) class MockModelHandler(BaseHTTPRequestHandler): @@ -364,6 +314,7 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]: MCP_PROMPT, RESTART_FIRST_PROMPT, RESTART_SECOND_PROMPT, + PROFILE_PLUGIN_PROMPT, } prompt = next( (candidate for candidate in user_prompts if candidate in scenario_prompts), @@ -430,6 +381,15 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]: "mcp__fixture__add", {"a": 19, "b": 23}, ) + if prompt == PROFILE_PLUGIN_PROMPT: + system_text = "\n".join( + message_text(message.get("content")) + for message in messages + if isinstance(message, dict) and message.get("role") == "system" + ) + if PROFILE_PLUGIN_MARKER not in system_text: + raise AssertionError("external profile plugin contributed no model-visible marker") + return text_chunks(PROFILE_PLUGIN_TEXT) return text_chunks(EXPECTED_TEXT) @@ -710,7 +670,7 @@ def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "--scenario", - choices=("all", "sdk-default", "sdk-custom", "sdk-minimal", "sdk-fs-search", "sdk-mcp", "sdk-snapshot", "sdk-restart", "sdk-live", "direct"), + choices=("all", "sdk-default", "sdk-custom", "sdk-minimal", "sdk-fs-search", "sdk-mcp", "sdk-snapshot", "sdk-restart", "sdk-profile-plugin", "sdk-live", "direct"), default="all", ) parser.add_argument("--exe", type=Path) @@ -725,6 +685,8 @@ def main() -> None: parser.error("--installed-wheel resolves the wheel's own runtime and cannot be combined with --exe") if args.scenario == "sdk-live" and not args.installed_wheel: parser.error("--scenario sdk-live requires --installed-wheel") + if args.scenario == "sdk-profile-plugin" and not args.installed_wheel: + parser.error("--scenario sdk-profile-plugin requires --installed-wheel") if args.installed_wheel: args.exe = assert_installed_wheel_environment() if args.scenario in {"all", "sdk-custom", "sdk-minimal", "sdk-fs-search", "sdk-snapshot", "sdk-restart", "direct"} and args.exe is None: @@ -759,6 +721,8 @@ def main() -> None: if args.scenario in {"all", "sdk-restart"}: assert args.exe is not None smoke_sdk_restart_snapshot(model.url, args.exe.resolve(), args.update_snapshots) + if args.installed_wheel and args.scenario in {"all", "sdk-profile-plugin"}: + smoke_sdk_profile_plugin(model.url) if args.scenario in {"all", "direct"}: assert args.exe is not None smoke_direct(model.url, args.exe.resolve()) @@ -832,7 +796,8 @@ def smoke_sdk_live() -> None: with tempfile.TemporaryDirectory(prefix="dsh-sdk-live-") as temporary: root = Path(temporary).resolve() - sessions = root / "sessions" + dsh_home = root / "home" + sessions = dsh_home / "sessions" marker = root / "live-api-marker.txt" session_id = "installed-wheel-live-api" create_prompt = ( @@ -847,7 +812,11 @@ def smoke_sdk_live() -> None: provider="deepseek-official", model="deepseek-v4-flash", cwd=str(root), - session_root=str(sessions), + dsh_home=str(dsh_home), + env={ + "DSH_PERMISSION_MODE": "danger-full-access", + "DSH_TELEMETRY_DISABLED": "1", + }, api_key=api_key, base_url=base_url, request_timeout_seconds=180, @@ -910,18 +879,27 @@ def smoke_sdk_default(base_url: str) -> None: with tempfile.TemporaryDirectory(prefix="dsh-sdk-default-") as temporary: root = Path(temporary).resolve() - sessions = root / "sessions" + dsh_home = root / "home" + sessions = dsh_home / "sessions" with DeepSeekHarness( provider="deepseek-official", model="smoke-model", cwd=str(root), - session_root=str(sessions), + dsh_home=str(dsh_home), + env={ + "DSH_PERMISSION_MODE": "danger-full-access", + "DSH_TELEMETRY_DISABLED": "1", + }, api_key="sk-keyless-smoke", base_url=base_url, request_timeout_seconds=60, ) as harness: result = harness.run("reply with the smoke text", session_id="default-smoke") - assert result.final_response == EXPECTED_TEXT, result.final_response + assert result.final_response == EXPECTED_TEXT, ( + f"final={result.final_response!r} finish={result.finish_reason!r} " + f"events={[event.get('type') for event in result.events]!r} " + f"turn_end={safe_turn_end(next((event.get('data', event) for event in reversed(result.events) if event.get('type') == 'turn/end'), {}))!r}" + ) assert_zstd_session_log(sessions) @@ -930,16 +908,44 @@ def smoke_sdk_custom(base_url: str, executable: Path) -> None: with tempfile.TemporaryDirectory(prefix="dsh-sdk-custom-") as temporary: root = Path(temporary).resolve() - sessions = root / "sessions" - cordis = root / "cordis.yml" - cordis.write_text(CUSTOM_CORDIS) + dsh_home = root / "home" + sessions = dsh_home / "sessions" + patch = write_profile_patch(root, "custom.patch.yml", sessions, [ + {"id": "tools", "config": {"mode": "both"}}, + { + "id": "system-prompt", + "config": { + "persona": "You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.", + }, + }, + {"id": "session-log-deepseek", "config": {"enabled": True}}, + *({"id": row_id, "disabled": True} for row_id in LEGACY_CUSTOM_DISABLED_ROWS), + {"id": "tool-bash", "disabled": True}, + { + "id": "tool-subagent", + "config": { + "provider": "spawn", + "toolName": "subagent", + "backgroundMode": "one-shot", + }, + }, + {"insert": [ + {"id": "code-runtime", "name": "@deepseek-ai/dsh-code-runtime-worker-thread"}, + {"id": "cordis-host-runner", "name": "@deepseek-ai/dsh-cordis-host-runner"}, + {"id": "cordis-tool", "name": "@deepseek-ai/dsh-tool-cordis"}, + ]}, + ]) with DeepSeekHarness( provider="deepseek-official", model="smoke-model", cwd=str(root), - session_root=str(sessions), - cordis=str(cordis), - runtime_bin=str(executable), + dsh_bin=str(executable), + dsh_home=str(dsh_home), + patches=(str(patch),), + env={ + "DSH_PERMISSION_MODE": "danger-full-access", + "DSH_TELEMETRY_DISABLED": "1", + }, api_key="sk-keyless-smoke", base_url=base_url, request_timeout_seconds=60, @@ -954,7 +960,7 @@ def smoke_sdk_custom(base_url: str, executable: Path) -> None: def smoke_sdk_minimal(base_url: str, executable: Path, update_snapshots: bool) -> None: - """Exercise the checked-in minimal composition through the packaged executable.""" + """Exercise the shipped standalone minimal profile through the packaged executable.""" from deepseek_harness import DeepSeekHarness # One mock model serves every scenario of a run, so the snapshot takes this turn's slice. @@ -963,14 +969,15 @@ def smoke_sdk_minimal(base_url: str, executable: Path, update_snapshots: bool) - root = Path(temporary).resolve() editor_path = root / "created.txt" prompt = f"{MINIMAL_PROMPT}\n{MINIMAL_EDITOR_PATH_PREFIX}{editor_path}" - sessions = root / "sessions" + dsh_home = root / "home" + sessions = dsh_home / "sessions" with DeepSeekHarness( provider="deepseek-official", model="smoke-model", cwd=str(root), - session_root=str(sessions), - cordis=str(MINIMAL_CORDIS), - runtime_bin=str(executable), + dsh_bin=str(executable), + dsh_home=str(dsh_home), + profile="sdk-minimal", api_key="sk-keyless-smoke", base_url=base_url, request_timeout_seconds=60, @@ -997,16 +1004,23 @@ def smoke_sdk_fs_search(base_url: str, executable: Path) -> None: with tempfile.TemporaryDirectory(prefix="dsh-sdk-fs-search-") as temporary: root = Path(temporary).resolve() (root / "needle.txt").write_text(f"{FS_SEARCH_MARKER}\n") - sessions = root / "sessions" - cordis = root / "cordis.yml" - cordis.write_text(FS_SEARCH_CORDIS) + dsh_home = root / "home" + sessions = dsh_home / "sessions" + patch = write_profile_patch(root, "fs-search.patch.yml", sessions, [ + {"id": "skill-filesystem", "disabled": True}, + {"id": "tool-fs-search", "config": {"sampleOverCapGlobResults": False}}, + ]) with DeepSeekHarness( provider="deepseek-official", model="smoke-model", cwd=str(root), - session_root=str(sessions), - cordis=str(cordis), - runtime_bin=str(executable), + dsh_bin=str(executable), + dsh_home=str(dsh_home), + patches=(str(patch),), + env={ + "DSH_PERMISSION_MODE": "danger-full-access", + "DSH_TELEMETRY_DISABLED": "1", + }, api_key="sk-keyless-smoke", base_url=base_url, request_timeout_seconds=60, @@ -1023,19 +1037,23 @@ def smoke_sdk_mcp(base_url: str, executable: Path | None) -> None: with tempfile.TemporaryDirectory(prefix="dsh-sdk-mcp-") as temporary: root = Path(temporary).resolve() - sessions = root / "sessions" + dsh_home = root / "home" + sessions = dsh_home / "sessions" server_script = root / "mcp_server.py" server_script.write_text(MCP_SERVER_SCRIPT) - cordis = root / "cordis.yml" - cordis.write_text(mcp_cordis(server_script)) + patch = write_mcp_patch(root, sessions, server_script) discovery_log = server_script.with_suffix(".log") with DeepSeekHarness( provider="deepseek-official", model="smoke-model", cwd=str(root), - session_root=str(sessions), - cordis=str(cordis), - runtime_bin=None if executable is None else str(executable), + dsh_bin=None if executable is None else str(executable), + dsh_home=str(dsh_home), + patches=(str(patch),), + env={ + "DSH_PERMISSION_MODE": "danger-full-access", + "DSH_TELEMETRY_DISABLED": "1", + }, api_key="sk-keyless-smoke", base_url=base_url, request_timeout_seconds=60, @@ -1052,22 +1070,131 @@ def smoke_sdk_mcp(base_url: str, executable: Path | None) -> None: assert_session_log(sessions, root, MCP_TEXT, "mcp__fixture__add", "42") +def smoke_sdk_profile_plugin(base_url: str) -> None: + """Install an external bundle through Python's dsh command and load it in the SDK.""" + from deepseek_harness import DeepSeekHarness + + with tempfile.TemporaryDirectory(prefix="dsh-sdk-profile-plugin-") as temporary: + root = Path(temporary).resolve() + dsh_home = root / "home" + plugin = root / "plugin" + plugin.mkdir() + (plugin / "package.json").write_text(json.dumps({ + "name": "dsh-python-blackbox-plugin", + "version": "1.0.0", + "private": True, + "type": "module", + "exports": "./index.js", + "peerDependencies": {"@deepseek-ai/cordis": "*"}, + "dsh": {"bundle": {"patch": "./cordis.patch.yml"}}, + }, indent=2)) + (plugin / "index.js").write_text( + "import { Context } from '@deepseek-ai/cordis'\n" + "export const name = 'python-sdk-blackbox-plugin'\n" + "export const inject = ['systemPrompt']\n" + "export function apply(ctx) {\n" + " if (!(ctx instanceof Context)) throw new Error('external plugin loaded a second Cordis instance')\n" + " ctx.effect(() => ctx.systemPrompt.section({\n" + " name: 'python-sdk:blackbox-plugin',\n" + " order: 10,\n" + f" text: '{PROFILE_PLUGIN_MARKER}',\n" + " }))\n" + "}\n" + ) + (plugin / "cordis.patch.yml").write_text(json.dumps([{ + "insert": [{"id": "python-sdk-blackbox-plugin", "name": "dsh-python-blackbox-plugin"}], + }], indent=2)) + + dsh = Path(sysconfig.get_path("scripts")) / "dsh" + environment = {**os.environ, "DSH_HOME": str(dsh_home)} + installed = subprocess.run( + [str(dsh), "plugin", "--profile", "sdk", "add", f"file:{plugin}"], + cwd=root, + env=environment, + text=True, + capture_output=True, + check=False, + ) + if installed.returncode != 0: + raise AssertionError( + f"Python-installed dsh could not add the external profile plugin: " + f"stdout={installed.stdout!r} stderr={installed.stderr!r}" + ) + manifest = json.loads((dsh_home / "profiles" / "sdk" / "package.json").read_text()) + if "dsh-python-blackbox-plugin" not in manifest.get("dependencies", {}): + raise AssertionError(f"dsh plugin did not record the external dependency: {manifest}") + if "dsh-python-blackbox-plugin" not in manifest["dsh"]["profile"]["bundles"]: + raise AssertionError(f"dsh plugin did not activate the external bundle: {manifest}") + + harness = DeepSeekHarness( + provider="deepseek-official", + model="smoke-model", + cwd=str(root), + dsh_home=str(dsh_home), + env={ + "DSH_PERMISSION_MODE": "danger-full-access", + "DSH_TELEMETRY_DISABLED": "1", + }, + api_key="sk-keyless-smoke", + base_url=base_url, + request_timeout_seconds=60, + ) + try: + with harness: + result = harness.run(PROFILE_PLUGIN_PROMPT, session_id="profile-plugin-smoke") + except Exception as error: + raise AssertionError( + f"external profile plugin runtime failed: {harness.client._runtime_diagnostics()}" + ) from error + + assert result.final_response == PROFILE_PLUGIN_TEXT, result.final_response + assert_zstd_session_log(dsh_home / "sessions") + + def smoke_sdk_snapshot(base_url: str, executable: Path, update_snapshots: bool) -> None: """Drive and compare the advanced SDK/executable behavioral snapshot.""" from deepseek_harness import DeepSeekHarness with tempfile.TemporaryDirectory(prefix="dsh-sdk-snapshot-") as temporary: root = Path(temporary).resolve() - sessions = root / "sessions" - cordis = root / "cordis.yml" - cordis.write_text(CUSTOM_CORDIS) + dsh_home = root / "home" + sessions = dsh_home / "sessions" + patch = write_profile_patch(root, "snapshot.patch.yml", sessions, [ + {"id": "tools", "config": {"mode": "both"}}, + { + "id": "system-prompt", + "config": { + "persona": "You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.", + }, + }, + {"id": "session-log-deepseek", "config": {"enabled": True}}, + *({"id": row_id, "disabled": True} for row_id in LEGACY_CUSTOM_DISABLED_ROWS), + {"id": "tool-bash", "disabled": True}, + { + "id": "tool-subagent", + "config": { + "provider": "spawn", + "toolName": "subagent", + "backgroundMode": "one-shot", + }, + }, + {"insert": [ + {"id": "code-runtime", "name": "@deepseek-ai/dsh-code-runtime-worker-thread"}, + {"id": "cordis-host-runner", "name": "@deepseek-ai/dsh-cordis-host-runner"}, + {"id": "cordis-tool", "name": "@deepseek-ai/dsh-tool-cordis"}, + ]}, + ]) with DeepSeekHarness( provider="deepseek-official", model="smoke-model", cwd=str(root), - session_root=str(sessions), - cordis=str(cordis), - runtime_bin=str(executable), + dsh_bin=str(executable), + dsh_home=str(dsh_home), + patches=(str(patch),), + env={ + "DSH_PERMISSION_MODE": "danger-full-access", + "DSH_TELEMETRY_DISABLED": "1", + }, api_key="sk-keyless-smoke", base_url=base_url, request_timeout_seconds=60, @@ -1103,9 +1230,33 @@ def smoke_sdk_restart_snapshot(base_url: str, executable: Path, update_snapshots with tempfile.TemporaryDirectory(prefix="dsh-sdk-restart-") as temporary: root = Path(temporary).resolve() - sessions = root / "sessions" - cordis = root / "cordis.yml" - cordis.write_text(CUSTOM_CORDIS) + dsh_home = root / "home" + sessions = dsh_home / "sessions" + patch = write_profile_patch(root, "restart.patch.yml", sessions, [ + {"id": "tools", "config": {"mode": "both"}}, + { + "id": "system-prompt", + "config": { + "persona": "You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.", + }, + }, + {"id": "session-log-deepseek", "config": {"enabled": True}}, + *({"id": row_id, "disabled": True} for row_id in LEGACY_CUSTOM_DISABLED_ROWS), + {"id": "tool-bash", "disabled": True}, + { + "id": "tool-subagent", + "config": { + "provider": "spawn", + "toolName": "subagent", + "backgroundMode": "one-shot", + }, + }, + {"insert": [ + {"id": "code-runtime", "name": "@deepseek-ai/dsh-code-runtime-worker-thread"}, + {"id": "cordis-host-runner", "name": "@deepseek-ai/dsh-cordis-host-runner"}, + {"id": "cordis-tool", "name": "@deepseek-ai/dsh-tool-cordis"}, + ]}, + ]) first_request = len(MockModelHandler.requests) def run(prompt: str, session_id: str) -> "RunResult": @@ -1113,9 +1264,13 @@ def smoke_sdk_restart_snapshot(base_url: str, executable: Path, update_snapshots provider="deepseek-official", model="smoke-model", cwd=str(root), - session_root=str(sessions), - cordis=str(cordis), - runtime_bin=str(executable), + dsh_bin=str(executable), + dsh_home=str(dsh_home), + patches=(str(patch),), + env={ + "DSH_PERMISSION_MODE": "danger-full-access", + "DSH_TELEMETRY_DISABLED": "1", + }, api_key="sk-keyless-smoke", base_url=base_url, request_timeout_seconds=60, @@ -1155,18 +1310,22 @@ def smoke_sdk_restart_snapshot(base_url: str, executable: Path, update_snapshots def smoke_direct(base_url: str, executable: Path) -> None: with tempfile.TemporaryDirectory(prefix="dsh-direct-") as temporary: root = Path(temporary).resolve() - sessions = root / "sessions" - cordis = root / "cordis.yml" - cordis.write_text(CUSTOM_CORDIS) + dsh_home = root / "home" + sessions = dsh_home / "sessions" + patch = write_profile_patch(root, "direct.patch.yml", sessions, []) environment = { **os.environ, - "DSH_CORDIS_CONFIG": str(cordis), - "DSH_SESSION_ROOT": str(sessions), - "DSH_CWD": str(root), + "DSH_HOME": str(dsh_home), + "DSH_PERMISSION_MODE": "danger-full-access", + "DSH_TELEMETRY_DISABLED": "1", "DEEPSEEK_API_KEY": "sk-keyless-smoke", "DEEPSEEK_BASE_URL": base_url, } - peer = RuntimePeer([str(executable)], root, environment) + peer = RuntimePeer( + [str(executable), "--profile", "sdk", "--patch", str(patch)], + root, + environment, + ) try: peer.send({"jsonrpc": "2.0", "id": "initialize", "method": "initialize", "params": {"cwd": str(root), "provider": "deepseek-official", "model": "smoke-model"}}) peer.read_until(lambda message: message.get("id") == "initialize") @@ -1332,9 +1491,9 @@ def build_minimal_snapshot_files( Every assembled system prompt, advertised tool schema, and system or user message is kept verbatim: they carry what the deployment actually shows the model, so a plugin that contributes an unintended system section or user message cannot pass unnoticed. - Assistant and tool payloads keep only their call identity, and the dynamic - runtime-context snapshot is dropped, because their text differs across the platforms - this expected output must replay on. + Assistant and tool payloads keep only their call identity because their text differs + across the platforms this expected output must replay on. The shipped profile omits + dynamic runtime context, so every message it emits is compared. """ snapshot = [] for body in requests: @@ -1346,21 +1505,11 @@ def build_minimal_snapshot_files( "messages": [ minimal_snapshot_message(message, cwd) for message in messages - if not is_runtime_context_message(message) ], }) return {"model-visible.json": json.dumps(snapshot, indent=2, ensure_ascii=False) + "\n"} -def is_runtime_context_message(message: object) -> bool: - """Identify the agent loop's dynamic runtime-context snapshot, current or cleared.""" - return ( - isinstance(message, dict) - and message.get("role") == "user" - and message_text(message.get("content")).startswith(RUNTIME_CONTEXT_PREFIX) - ) - - def minimal_snapshot_message(message: object, cwd: Path) -> dict[str, object]: """Reduce one model-visible message to its stable, behavior-carrying parts.""" if not isinstance(message, dict): @@ -1419,7 +1568,6 @@ def build_snapshot_files( {"method": notification.method, "payload": notification.payload} for notification in result.notifications ], - "session_root": result.session_root, } normalized_result = normalize_snapshot_value(result_value, replacements) files = { @@ -1461,7 +1609,6 @@ def build_restart_snapshot_files( "finish_reason": result.finish_reason, "eventTypes": [event.get("type") for event in result.events], "notificationMethods": [notification.method for notification in result.notifications], - "session_root": result.session_root, } for result in (first, second) ] diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/result.json b/scripts/snapshots/python-sdk-single-exe/advanced/result.json index 0889fd6b9f..c9dc0735ea 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/result.json +++ b/scripts/snapshots/python-sdk-single-exe/advanced/result.json @@ -4,7 +4,7 @@ "events": [ { "type": "agent/inbox/spliced", - "seq": 0, + "seq": 3, "time": 0, "data": { "target": "next-turn", @@ -28,7 +28,7 @@ }, { "type": "turn/start", - "seq": 1, + "seq": 4, "time": 0, "data": { "turn": 1 @@ -36,7 +36,7 @@ }, { "type": "agent/inbox/spliced", - "seq": 2, + "seq": 5, "time": 0, "data": { "target": "next-turn", @@ -47,7 +47,7 @@ }, { "type": "step/start", - "seq": 3, + "seq": 6, "time": 0, "data": { "turn": 1, @@ -56,7 +56,7 @@ }, { "type": "user/message", - "seq": 4, + "seq": 7, "time": 0, "data": { "content": [ @@ -73,14 +73,45 @@ }, "surfaceOp": "append" }, + { + "type": "user/message", + "seq": 8, + "time": 0, + "data": { + "content": [ + { + "type": "text", + "text": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)." + } + ], + "source": { + "kind": "plugin", + "plugin": "@deepseek-ai/dsh-system-prompt", + "form": "snapshot", + "sections": [ + { + "name": "sandbox:policy", + "text": "Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations." + }, + { + "name": "approval:policy", + "text": "Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)." + } + ] + }, + "role": "user", + "id": "{{messageId}}" + }, + "surfaceOp": "append" + }, { "type": "session/title", - "seq": 5, + "seq": 9, "time": 0, "data": { "title": "Run the advanced packaged-runtime snapsh", "messageSeqs": [ - 4 + 7 ], "source": { "kind": "fallback" @@ -89,7 +120,7 @@ }, { "type": "request/header", - "seq": 6, + "seq": 10, "time": 0, "data": { "header": { @@ -125,7 +156,7 @@ }, { "type": "request/context", - "seq": 7, + "seq": 11, "time": 0, "data": { "provider": "deepseek-official", @@ -135,16 +166,16 @@ }, { "type": "session-log-deepseek/delivery-accepted", - "seq": 8, + "seq": 12, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 7 + "throughSeq": 11 } }, { "type": "assistant/chunk", - "seq": 9, + "seq": 13, "time": 0, "data": { "turn": 1, @@ -158,7 +189,7 @@ }, { "type": "assistant/chunk", - "seq": 10, + "seq": 14, "time": 0, "data": { "turn": 1, @@ -174,7 +205,7 @@ }, { "type": "assistant/chunk", - "seq": 11, + "seq": 15, "time": 0, "data": { "turn": 1, @@ -193,7 +224,7 @@ }, { "type": "assistant/chunk", - "seq": 12, + "seq": 16, "time": 0, "data": { "turn": 1, @@ -209,7 +240,7 @@ }, { "type": "assistant/chunk", - "seq": 13, + "seq": 17, "time": 0, "data": { "turn": 1, @@ -224,7 +255,7 @@ }, { "type": "assistant/message", - "seq": 14, + "seq": 18, "time": 0, "data": { "turn": 1, @@ -252,17 +283,17 @@ } }, "sourceEventSeqs": [ - 9, - 10, - 11, - 12, - 13 + 13, + 14, + 15, + 16, + 17 ], "surfaceOp": "append" }, { "type": "tool/call", - "seq": 15, + "seq": 19, "time": 0, "data": { "turn": 1, @@ -274,7 +305,7 @@ }, { "type": "tool/result", - "seq": 16, + "seq": 20, "time": 0, "data": { "turn": 1, @@ -306,13 +337,13 @@ } }, "sourceEventSeqs": [ - 15 + 19 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 17, + "seq": 21, "time": 0, "data": { "turn": 1, @@ -321,7 +352,7 @@ }, { "type": "step/start", - "seq": 18, + "seq": 22, "time": 0, "data": { "turn": 1, @@ -330,16 +361,16 @@ }, { "type": "session-log-deepseek/delivery-accepted", - "seq": 19, + "seq": 23, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 18 + "throughSeq": 22 } }, { "type": "assistant/chunk", - "seq": 20, + "seq": 24, "time": 0, "data": { "turn": 1, @@ -353,7 +384,7 @@ }, { "type": "assistant/chunk", - "seq": 21, + "seq": 25, "time": 0, "data": { "turn": 1, @@ -369,7 +400,7 @@ }, { "type": "assistant/chunk", - "seq": 22, + "seq": 26, "time": 0, "data": { "turn": 1, @@ -388,7 +419,7 @@ }, { "type": "assistant/chunk", - "seq": 23, + "seq": 27, "time": 0, "data": { "turn": 1, @@ -404,7 +435,7 @@ }, { "type": "assistant/chunk", - "seq": 24, + "seq": 28, "time": 0, "data": { "turn": 1, @@ -419,7 +450,7 @@ }, { "type": "assistant/message", - "seq": 25, + "seq": 29, "time": 0, "data": { "turn": 1, @@ -447,17 +478,17 @@ } }, "sourceEventSeqs": [ - 20, - 21, - 22, - 23, - 24 + 24, + 25, + 26, + 27, + 28 ], "surfaceOp": "append" }, { "type": "tool/call", - "seq": 26, + "seq": 30, "time": 0, "data": { "turn": 1, @@ -469,7 +500,7 @@ }, { "type": "tool/result", - "seq": 27, + "seq": 31, "time": 0, "data": { "turn": 1, @@ -502,13 +533,13 @@ } }, "sourceEventSeqs": [ - 26 + 30 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 28, + "seq": 32, "time": 0, "data": { "turn": 1, @@ -517,7 +548,7 @@ }, { "type": "step/start", - "seq": 29, + "seq": 33, "time": 0, "data": { "turn": 1, @@ -526,7 +557,7 @@ }, { "type": "request/header", - "seq": 30, + "seq": 34, "time": 0, "data": { "header": { @@ -563,16 +594,16 @@ }, { "type": "session-log-deepseek/delivery-accepted", - "seq": 31, + "seq": 35, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 30 + "throughSeq": 34 } }, { "type": "assistant/chunk", - "seq": 32, + "seq": 36, "time": 0, "data": { "turn": 1, @@ -586,7 +617,7 @@ }, { "type": "assistant/chunk", - "seq": 33, + "seq": 37, "time": 0, "data": { "turn": 1, @@ -602,7 +633,7 @@ }, { "type": "assistant/chunk", - "seq": 34, + "seq": 38, "time": 0, "data": { "turn": 1, @@ -621,7 +652,7 @@ }, { "type": "assistant/chunk", - "seq": 35, + "seq": 39, "time": 0, "data": { "turn": 1, @@ -637,7 +668,7 @@ }, { "type": "assistant/chunk", - "seq": 36, + "seq": 40, "time": 0, "data": { "turn": 1, @@ -652,7 +683,7 @@ }, { "type": "assistant/message", - "seq": 37, + "seq": 41, "time": 0, "data": { "turn": 1, @@ -680,17 +711,17 @@ } }, "sourceEventSeqs": [ - 32, - 33, - 34, - 35, - 36 + 36, + 37, + 38, + 39, + 40 ], "surfaceOp": "append" }, { "type": "tool/call", - "seq": 38, + "seq": 42, "time": 0, "data": { "turn": 1, @@ -702,7 +733,7 @@ }, { "type": "tool/code-dispatch-start", - "seq": 39, + "seq": 43, "time": 0, "data": { "rootCallId": "advanced-code", @@ -716,7 +747,7 @@ }, { "type": "tool/code-dispatch", - "seq": 40, + "seq": 44, "time": 0, "data": { "rootCallId": "advanced-code", @@ -737,7 +768,7 @@ }, { "type": "tool/result", - "seq": 41, + "seq": 45, "time": 0, "data": { "turn": 1, @@ -765,13 +796,13 @@ } }, "sourceEventSeqs": [ - 38 + 42 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 42, + "seq": 46, "time": 0, "data": { "turn": 1, @@ -780,7 +811,7 @@ }, { "type": "step/start", - "seq": 43, + "seq": 47, "time": 0, "data": { "turn": 1, @@ -789,16 +820,16 @@ }, { "type": "session-log-deepseek/delivery-accepted", - "seq": 44, + "seq": 48, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 43 + "throughSeq": 47 } }, { "type": "assistant/chunk", - "seq": 45, + "seq": 49, "time": 0, "data": { "turn": 1, @@ -812,7 +843,7 @@ }, { "type": "assistant/chunk", - "seq": 46, + "seq": 50, "time": 0, "data": { "turn": 1, @@ -828,7 +859,7 @@ }, { "type": "assistant/chunk", - "seq": 47, + "seq": 51, "time": 0, "data": { "turn": 1, @@ -847,7 +878,7 @@ }, { "type": "assistant/chunk", - "seq": 48, + "seq": 52, "time": 0, "data": { "turn": 1, @@ -863,7 +894,7 @@ }, { "type": "assistant/chunk", - "seq": 49, + "seq": 53, "time": 0, "data": { "turn": 1, @@ -878,7 +909,7 @@ }, { "type": "assistant/message", - "seq": 50, + "seq": 54, "time": 0, "data": { "turn": 1, @@ -906,17 +937,17 @@ } }, "sourceEventSeqs": [ - 45, - 46, - 47, - 48, - 49 + 49, + 50, + 51, + 52, + 53 ], "surfaceOp": "append" }, { "type": "tool/call", - "seq": 51, + "seq": 55, "time": 0, "data": { "turn": 1, @@ -928,7 +959,7 @@ }, { "type": "tool/result", - "seq": 52, + "seq": 56, "time": 0, "data": { "turn": 1, @@ -956,13 +987,13 @@ } }, "sourceEventSeqs": [ - 51 + 55 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 53, + "seq": 57, "time": 0, "data": { "turn": 1, @@ -971,7 +1002,7 @@ }, { "type": "step/start", - "seq": 54, + "seq": 58, "time": 0, "data": { "turn": 1, @@ -980,16 +1011,16 @@ }, { "type": "session-log-deepseek/delivery-accepted", - "seq": 55, + "seq": 59, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 54 + "throughSeq": 58 } }, { "type": "assistant/chunk", - "seq": 56, + "seq": 60, "time": 0, "data": { "turn": 1, @@ -1003,7 +1034,7 @@ }, { "type": "assistant/chunk", - "seq": 57, + "seq": 61, "time": 0, "data": { "turn": 1, @@ -1019,7 +1050,7 @@ }, { "type": "assistant/chunk", - "seq": 58, + "seq": 62, "time": 0, "data": { "turn": 1, @@ -1038,7 +1069,7 @@ }, { "type": "assistant/chunk", - "seq": 59, + "seq": 63, "time": 0, "data": { "turn": 1, @@ -1054,7 +1085,7 @@ }, { "type": "assistant/chunk", - "seq": 60, + "seq": 64, "time": 0, "data": { "turn": 1, @@ -1069,7 +1100,7 @@ }, { "type": "assistant/message", - "seq": 61, + "seq": 65, "time": 0, "data": { "turn": 1, @@ -1097,17 +1128,17 @@ } }, "sourceEventSeqs": [ - 56, - 57, - 58, - 59, - 60 + 60, + 61, + 62, + 63, + 64 ], "surfaceOp": "append" }, { "type": "tool/call", - "seq": 62, + "seq": 66, "time": 0, "data": { "turn": 1, @@ -1119,7 +1150,7 @@ }, { "type": "tool-workflow/run-start", - "seq": 63, + "seq": 67, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -1128,7 +1159,7 @@ }, { "type": "tool-workflow/agent-start", - "seq": 64, + "seq": 68, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -1140,7 +1171,7 @@ }, { "type": "tool-workflow/agent-end", - "seq": 65, + "seq": 69, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -1150,7 +1181,7 @@ }, { "type": "tool-workflow/run-end", - "seq": 66, + "seq": 70, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -1159,7 +1190,7 @@ }, { "type": "tool/result", - "seq": 67, + "seq": 71, "time": 0, "data": { "turn": 1, @@ -1187,13 +1218,13 @@ } }, "sourceEventSeqs": [ - 62 + 66 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 68, + "seq": 72, "time": 0, "data": { "turn": 1, @@ -1202,7 +1233,7 @@ }, { "type": "step/start", - "seq": 69, + "seq": 73, "time": 0, "data": { "turn": 1, @@ -1211,16 +1242,16 @@ }, { "type": "session-log-deepseek/delivery-accepted", - "seq": 70, + "seq": 74, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 69 + "throughSeq": 73 } }, { "type": "assistant/chunk", - "seq": 71, + "seq": 75, "time": 0, "data": { "turn": 1, @@ -1234,7 +1265,7 @@ }, { "type": "assistant/chunk", - "seq": 72, + "seq": 76, "time": 0, "data": { "turn": 1, @@ -1250,7 +1281,7 @@ }, { "type": "assistant/chunk", - "seq": 73, + "seq": 77, "time": 0, "data": { "turn": 1, @@ -1269,7 +1300,7 @@ }, { "type": "assistant/chunk", - "seq": 74, + "seq": 78, "time": 0, "data": { "turn": 1, @@ -1285,7 +1316,7 @@ }, { "type": "assistant/chunk", - "seq": 75, + "seq": 79, "time": 0, "data": { "turn": 1, @@ -1300,7 +1331,7 @@ }, { "type": "assistant/message", - "seq": 76, + "seq": 80, "time": 0, "data": { "turn": 1, @@ -1328,17 +1359,17 @@ } }, "sourceEventSeqs": [ - 71, - 72, - 73, - 74, - 75 + 75, + 76, + 77, + 78, + 79 ], "surfaceOp": "append" }, { "type": "tool/call", - "seq": 77, + "seq": 81, "time": 0, "data": { "turn": 1, @@ -1350,7 +1381,7 @@ }, { "type": "tool/result", - "seq": 78, + "seq": 82, "time": 0, "data": { "turn": 1, @@ -1378,13 +1409,13 @@ } }, "sourceEventSeqs": [ - 77 + 81 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 79, + "seq": 83, "time": 0, "data": { "turn": 1, @@ -1393,7 +1424,7 @@ }, { "type": "step/start", - "seq": 80, + "seq": 84, "time": 0, "data": { "turn": 1, @@ -1402,7 +1433,7 @@ }, { "type": "request/header", - "seq": 81, + "seq": 85, "time": 0, "data": { "header": { @@ -1438,16 +1469,16 @@ }, { "type": "session-log-deepseek/delivery-accepted", - "seq": 82, + "seq": 86, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 81 + "throughSeq": 85 } }, { "type": "assistant/chunk", - "seq": 83, + "seq": 87, "time": 0, "data": { "turn": 1, @@ -1461,7 +1492,7 @@ }, { "type": "assistant/chunk", - "seq": 84, + "seq": 88, "time": 0, "data": { "turn": 1, @@ -1475,7 +1506,7 @@ }, { "type": "assistant/chunk", - "seq": 85, + "seq": 89, "time": 0, "data": { "turn": 1, @@ -1492,7 +1523,7 @@ }, { "type": "assistant/chunk", - "seq": 86, + "seq": 90, "time": 0, "data": { "turn": 1, @@ -1508,7 +1539,7 @@ }, { "type": "assistant/chunk", - "seq": 87, + "seq": 91, "time": 0, "data": { "turn": 1, @@ -1523,7 +1554,7 @@ }, { "type": "assistant/message", - "seq": 88, + "seq": 92, "time": 0, "data": { "turn": 1, @@ -1549,17 +1580,17 @@ } }, "sourceEventSeqs": [ - 83, - 84, - 85, - 86, - 87 + 87, + 88, + 89, + 90, + 91 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 89, + "seq": 93, "time": 0, "data": { "turn": 1, @@ -1568,7 +1599,7 @@ }, { "type": "turn/end", - "seq": 90, + "seq": 94, "time": 0, "data": { "turn": 1, @@ -1585,7 +1616,7 @@ "sessionId": "{{parent}}", "event": { "type": "agent/inbox/spliced", - "seq": 0, + "seq": 3, "time": 0, "data": { "target": "next-turn", @@ -1622,7 +1653,7 @@ "sessionId": "{{parent}}", "event": { "type": "turn/start", - "seq": 1, + "seq": 4, "time": 0, "data": { "turn": 1 @@ -1636,7 +1667,7 @@ "sessionId": "{{parent}}", "event": { "type": "agent/inbox/spliced", - "seq": 2, + "seq": 5, "time": 0, "data": { "target": "next-turn", @@ -1653,7 +1684,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 3, + "seq": 6, "time": 0, "data": { "turn": 1, @@ -1668,7 +1699,7 @@ "sessionId": "{{parent}}", "event": { "type": "user/message", - "seq": 4, + "seq": 7, "time": 0, "data": { "content": [ @@ -1687,18 +1718,55 @@ } } }, + { + "method": "session.event", + "payload": { + "sessionId": "{{parent}}", + "event": { + "type": "user/message", + "seq": 8, + "time": 0, + "data": { + "content": [ + { + "type": "text", + "text": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)." + } + ], + "source": { + "kind": "plugin", + "plugin": "@deepseek-ai/dsh-system-prompt", + "form": "snapshot", + "sections": [ + { + "name": "sandbox:policy", + "text": "Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations." + }, + { + "name": "approval:policy", + "text": "Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)." + } + ] + }, + "role": "user", + "id": "{{messageId}}" + }, + "surfaceOp": "append" + } + } + }, { "method": "session.event", "payload": { "sessionId": "{{parent}}", "event": { "type": "session/title", - "seq": 5, + "seq": 9, "time": 0, "data": { "title": "Run the advanced packaged-runtime snapsh", "messageSeqs": [ - 4 + 7 ], "source": { "kind": "fallback" @@ -1713,7 +1781,7 @@ "sessionId": "{{parent}}", "event": { "type": "request/header", - "seq": 6, + "seq": 10, "time": 0, "data": { "header": { @@ -1755,7 +1823,7 @@ "sessionId": "{{parent}}", "event": { "type": "request/context", - "seq": 7, + "seq": 11, "time": 0, "data": { "provider": "deepseek-official", @@ -1771,11 +1839,11 @@ "sessionId": "{{parent}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 8, + "seq": 12, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 7 + "throughSeq": 11 } } } @@ -1786,7 +1854,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 9, + "seq": 13, "time": 0, "data": { "turn": 1, @@ -1806,7 +1874,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 10, + "seq": 14, "time": 0, "data": { "turn": 1, @@ -1828,7 +1896,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 11, + "seq": 15, "time": 0, "data": { "turn": 1, @@ -1853,7 +1921,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 12, + "seq": 16, "time": 0, "data": { "turn": 1, @@ -1875,7 +1943,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 13, + "seq": 17, "time": 0, "data": { "turn": 1, @@ -1896,7 +1964,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 14, + "seq": 18, "time": 0, "data": { "turn": 1, @@ -1924,11 +1992,11 @@ } }, "sourceEventSeqs": [ - 9, - 10, - 11, - 12, - 13 + 13, + 14, + 15, + 16, + 17 ], "surfaceOp": "append" } @@ -1940,7 +2008,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 15, + "seq": 19, "time": 0, "data": { "turn": 1, @@ -1958,7 +2026,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 16, + "seq": 20, "time": 0, "data": { "turn": 1, @@ -1990,7 +2058,7 @@ } }, "sourceEventSeqs": [ - 15 + 19 ], "surfaceOp": "append" } @@ -2002,7 +2070,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 17, + "seq": 21, "time": 0, "data": { "turn": 1, @@ -2017,7 +2085,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 18, + "seq": 22, "time": 0, "data": { "turn": 1, @@ -2032,11 +2100,11 @@ "sessionId": "{{parent}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 19, + "seq": 23, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 18 + "throughSeq": 22 } } } @@ -2047,7 +2115,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 20, + "seq": 24, "time": 0, "data": { "turn": 1, @@ -2067,7 +2135,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 21, + "seq": 25, "time": 0, "data": { "turn": 1, @@ -2089,7 +2157,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 22, + "seq": 26, "time": 0, "data": { "turn": 1, @@ -2114,7 +2182,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 23, + "seq": 27, "time": 0, "data": { "turn": 1, @@ -2136,7 +2204,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 24, + "seq": 28, "time": 0, "data": { "turn": 1, @@ -2157,7 +2225,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 25, + "seq": 29, "time": 0, "data": { "turn": 1, @@ -2185,11 +2253,11 @@ } }, "sourceEventSeqs": [ - 20, - 21, - 22, - 23, - 24 + 24, + 25, + 26, + 27, + 28 ], "surfaceOp": "append" } @@ -2201,7 +2269,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 26, + "seq": 30, "time": 0, "data": { "turn": 1, @@ -2219,7 +2287,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 27, + "seq": 31, "time": 0, "data": { "turn": 1, @@ -2252,7 +2320,7 @@ } }, "sourceEventSeqs": [ - 26 + 30 ], "surfaceOp": "append" } @@ -2264,7 +2332,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 28, + "seq": 32, "time": 0, "data": { "turn": 1, @@ -2279,7 +2347,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 29, + "seq": 33, "time": 0, "data": { "turn": 1, @@ -2294,7 +2362,7 @@ "sessionId": "{{parent}}", "event": { "type": "request/header", - "seq": 30, + "seq": 34, "time": 0, "data": { "header": { @@ -2337,11 +2405,11 @@ "sessionId": "{{parent}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 31, + "seq": 35, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 30 + "throughSeq": 34 } } } @@ -2352,7 +2420,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 32, + "seq": 36, "time": 0, "data": { "turn": 1, @@ -2372,7 +2440,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 33, + "seq": 37, "time": 0, "data": { "turn": 1, @@ -2394,7 +2462,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 34, + "seq": 38, "time": 0, "data": { "turn": 1, @@ -2419,7 +2487,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 35, + "seq": 39, "time": 0, "data": { "turn": 1, @@ -2441,7 +2509,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 36, + "seq": 40, "time": 0, "data": { "turn": 1, @@ -2462,7 +2530,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 37, + "seq": 41, "time": 0, "data": { "turn": 1, @@ -2490,11 +2558,11 @@ } }, "sourceEventSeqs": [ - 32, - 33, - 34, - 35, - 36 + 36, + 37, + 38, + 39, + 40 ], "surfaceOp": "append" } @@ -2506,7 +2574,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 38, + "seq": 42, "time": 0, "data": { "turn": 1, @@ -2524,7 +2592,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/code-dispatch-start", - "seq": 39, + "seq": 43, "time": 0, "data": { "rootCallId": "advanced-code", @@ -2544,7 +2612,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/code-dispatch", - "seq": 40, + "seq": 44, "time": 0, "data": { "rootCallId": "advanced-code", @@ -2571,7 +2639,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 41, + "seq": 45, "time": 0, "data": { "turn": 1, @@ -2599,7 +2667,7 @@ } }, "sourceEventSeqs": [ - 38 + 42 ], "surfaceOp": "append" } @@ -2611,7 +2679,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 42, + "seq": 46, "time": 0, "data": { "turn": 1, @@ -2626,7 +2694,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 43, + "seq": 47, "time": 0, "data": { "turn": 1, @@ -2641,11 +2709,11 @@ "sessionId": "{{parent}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 44, + "seq": 48, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 43 + "throughSeq": 47 } } } @@ -2656,7 +2724,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 45, + "seq": 49, "time": 0, "data": { "turn": 1, @@ -2676,7 +2744,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 46, + "seq": 50, "time": 0, "data": { "turn": 1, @@ -2698,7 +2766,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 47, + "seq": 51, "time": 0, "data": { "turn": 1, @@ -2723,7 +2791,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 48, + "seq": 52, "time": 0, "data": { "turn": 1, @@ -2745,7 +2813,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 49, + "seq": 53, "time": 0, "data": { "turn": 1, @@ -2766,7 +2834,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 50, + "seq": 54, "time": 0, "data": { "turn": 1, @@ -2794,11 +2862,11 @@ } }, "sourceEventSeqs": [ - 45, - 46, - 47, - 48, - 49 + 49, + 50, + 51, + 52, + 53 ], "surfaceOp": "append" } @@ -2810,7 +2878,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 51, + "seq": 55, "time": 0, "data": { "turn": 1, @@ -2829,13 +2897,27 @@ "childSessionId": "{{child-1}}" } }, + { + "method": "session.event", + "payload": { + "sessionId": "{{child-1}}", + "event": { + "type": "permission/preset", + "seq": 2, + "time": 0, + "data": { + "preset": "danger-full-access" + } + } + } + }, { "method": "session.event", "payload": { "sessionId": "{{child-1}}", "event": { "type": "agent/inbox/spliced", - "seq": 0, + "seq": 3, "time": 0, "data": { "target": "next-turn", @@ -2872,7 +2954,7 @@ "sessionId": "{{child-1}}", "event": { "type": "turn/start", - "seq": 1, + "seq": 4, "time": 0, "data": { "turn": 1 @@ -2886,7 +2968,7 @@ "sessionId": "{{child-1}}", "event": { "type": "agent/inbox/spliced", - "seq": 2, + "seq": 5, "time": 0, "data": { "target": "next-turn", @@ -2903,10 +2985,10 @@ "sessionId": "{{child-1}}", "event": { "type": "subagent/descriptor", - "seq": 3, + "seq": 6, "time": 0, "data": { - "version": 2, + "version": 3, "mode": "one-shot", "provider": "spawn", "label": "Check direct child" @@ -2920,7 +3002,7 @@ "sessionId": "{{child-1}}", "event": { "type": "step/start", - "seq": 4, + "seq": 7, "time": 0, "data": { "turn": 1, @@ -2935,7 +3017,7 @@ "sessionId": "{{child-1}}", "event": { "type": "user/message", - "seq": 5, + "seq": 8, "time": 0, "data": { "content": [ @@ -2960,13 +3042,13 @@ "sessionId": "{{child-1}}", "event": { "type": "user/message", - "seq": 6, + "seq": 9, "time": 0, "data": { "content": [ { "type": "text", - "text": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it." + "text": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it." } ], "source": { @@ -2974,6 +3056,14 @@ "plugin": "@deepseek-ai/dsh-system-prompt", "form": "snapshot", "sections": [ + { + "name": "sandbox:policy", + "text": "Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations." + }, + { + "name": "approval:policy", + "text": "Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)." + }, { "name": "subagent:delegation", "text": "You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it." @@ -2993,12 +3083,12 @@ "sessionId": "{{child-1}}", "event": { "type": "session/title", - "seq": 7, + "seq": 10, "time": 0, "data": { "title": "Reply with exactly DIRECT_CHILD_OK and", "messageSeqs": [ - 5 + 8 ], "source": { "kind": "fallback" @@ -3013,18 +3103,17 @@ "sessionId": "{{child-1}}", "event": { "type": "request/header", - "seq": 8, + "seq": 11, "time": 0, "data": { "header": { "config": { "provider": "deepseek-official", "model": "smoke-model", - "maxTokens": 256000, - "reasoningEffort": "high" + "reasoningEffort": "high", + "maxTokens": 256000 }, "adapterDefaults": { - "reasoningEffort": true, "maxTokens": true }, "system": "{{system}}", @@ -3056,7 +3145,7 @@ "sessionId": "{{child-1}}", "event": { "type": "request/context", - "seq": 9, + "seq": 12, "time": 0, "data": { "provider": "deepseek-official", @@ -3072,11 +3161,11 @@ "sessionId": "{{child-1}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 10, + "seq": 13, "time": 0, "data": { "sessionId": "{{child-1}}", - "throughSeq": 9 + "throughSeq": 12 } } } @@ -3087,7 +3176,7 @@ "sessionId": "{{child-1}}", "event": { "type": "assistant/chunk", - "seq": 11, + "seq": 14, "time": 0, "data": { "turn": 1, @@ -3107,7 +3196,7 @@ "sessionId": "{{child-1}}", "event": { "type": "assistant/chunk", - "seq": 12, + "seq": 15, "time": 0, "data": { "turn": 1, @@ -3127,7 +3216,7 @@ "sessionId": "{{child-1}}", "event": { "type": "assistant/chunk", - "seq": 13, + "seq": 16, "time": 0, "data": { "turn": 1, @@ -3150,7 +3239,7 @@ "sessionId": "{{child-1}}", "event": { "type": "assistant/chunk", - "seq": 14, + "seq": 17, "time": 0, "data": { "turn": 1, @@ -3172,7 +3261,7 @@ "sessionId": "{{child-1}}", "event": { "type": "assistant/chunk", - "seq": 15, + "seq": 18, "time": 0, "data": { "turn": 1, @@ -3193,7 +3282,7 @@ "sessionId": "{{child-1}}", "event": { "type": "assistant/message", - "seq": 16, + "seq": 19, "time": 0, "data": { "turn": 1, @@ -3219,11 +3308,11 @@ } }, "sourceEventSeqs": [ - 11, - 12, - 13, 14, - 15 + 15, + 16, + 17, + 18 ], "surfaceOp": "append" } @@ -3235,7 +3324,7 @@ "sessionId": "{{child-1}}", "event": { "type": "step/end", - "seq": 17, + "seq": 20, "time": 0, "data": { "turn": 1, @@ -3250,7 +3339,7 @@ "sessionId": "{{child-1}}", "event": { "type": "turn/end", - "seq": 18, + "seq": 21, "time": 0, "data": { "turn": 1, @@ -3291,7 +3380,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 52, + "seq": 56, "time": 0, "data": { "turn": 1, @@ -3319,7 +3408,7 @@ } }, "sourceEventSeqs": [ - 51 + 55 ], "surfaceOp": "append" } @@ -3331,7 +3420,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 53, + "seq": 57, "time": 0, "data": { "turn": 1, @@ -3346,7 +3435,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 54, + "seq": 58, "time": 0, "data": { "turn": 1, @@ -3361,11 +3450,11 @@ "sessionId": "{{parent}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 55, + "seq": 59, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 54 + "throughSeq": 58 } } } @@ -3376,7 +3465,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 56, + "seq": 60, "time": 0, "data": { "turn": 1, @@ -3396,7 +3485,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 57, + "seq": 61, "time": 0, "data": { "turn": 1, @@ -3418,7 +3507,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 58, + "seq": 62, "time": 0, "data": { "turn": 1, @@ -3443,7 +3532,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 59, + "seq": 63, "time": 0, "data": { "turn": 1, @@ -3465,7 +3554,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 60, + "seq": 64, "time": 0, "data": { "turn": 1, @@ -3486,7 +3575,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 61, + "seq": 65, "time": 0, "data": { "turn": 1, @@ -3514,11 +3603,11 @@ } }, "sourceEventSeqs": [ - 56, - 57, - 58, - 59, - 60 + 60, + 61, + 62, + 63, + 64 ], "surfaceOp": "append" } @@ -3530,7 +3619,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 62, + "seq": 66, "time": 0, "data": { "turn": 1, @@ -3548,7 +3637,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool-workflow/run-start", - "seq": 63, + "seq": 67, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -3564,13 +3653,27 @@ "childSessionId": "{{child-2}}" } }, + { + "method": "session.event", + "payload": { + "sessionId": "{{child-2}}", + "event": { + "type": "permission/preset", + "seq": 2, + "time": 0, + "data": { + "preset": "danger-full-access" + } + } + } + }, { "method": "session.event", "payload": { "sessionId": "{{child-2}}", "event": { "type": "agent/inbox/spliced", - "seq": 0, + "seq": 3, "time": 0, "data": { "target": "next-turn", @@ -3607,7 +3710,7 @@ "sessionId": "{{child-2}}", "event": { "type": "turn/start", - "seq": 1, + "seq": 4, "time": 0, "data": { "turn": 1 @@ -3621,7 +3724,7 @@ "sessionId": "{{child-2}}", "event": { "type": "agent/inbox/spliced", - "seq": 2, + "seq": 5, "time": 0, "data": { "target": "next-turn", @@ -3632,16 +3735,34 @@ } } }, + { + "method": "session.event", + "payload": { + "sessionId": "{{parent}}", + "event": { + "type": "tool-workflow/agent-start", + "seq": 68, + "time": 0, + "data": { + "runId": "{{workflow-run}}", + "seq": 1, + "label": "workflow-child", + "phase": "Delegate", + "childId": "{{child-2}}" + } + } + } + }, { "method": "session.event", "payload": { "sessionId": "{{child-2}}", "event": { "type": "subagent/descriptor", - "seq": 3, + "seq": 6, "time": 0, "data": { - "version": 2, + "version": 3, "mode": "one-shot", "provider": "spawn" } @@ -3654,7 +3775,7 @@ "sessionId": "{{child-2}}", "event": { "type": "step/start", - "seq": 4, + "seq": 7, "time": 0, "data": { "turn": 1, @@ -3669,7 +3790,7 @@ "sessionId": "{{child-2}}", "event": { "type": "user/message", - "seq": 5, + "seq": 8, "time": 0, "data": { "content": [ @@ -3694,13 +3815,13 @@ "sessionId": "{{child-2}}", "event": { "type": "user/message", - "seq": 6, + "seq": 9, "time": 0, "data": { "content": [ { "type": "text", - "text": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it." + "text": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it." } ], "source": { @@ -3708,6 +3829,14 @@ "plugin": "@deepseek-ai/dsh-system-prompt", "form": "snapshot", "sections": [ + { + "name": "sandbox:policy", + "text": "Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations." + }, + { + "name": "approval:policy", + "text": "Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)." + }, { "name": "subagent:delegation", "text": "You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it." @@ -3727,12 +3856,12 @@ "sessionId": "{{child-2}}", "event": { "type": "session/title", - "seq": 7, + "seq": 10, "time": 0, "data": { "title": "Reply with exactly WORKFLOW_CHILD_OK and", "messageSeqs": [ - 5 + 8 ], "source": { "kind": "fallback" @@ -3747,18 +3876,17 @@ "sessionId": "{{child-2}}", "event": { "type": "request/header", - "seq": 8, + "seq": 11, "time": 0, "data": { "header": { "config": { "provider": "deepseek-official", "model": "smoke-model", - "maxTokens": 256000, - "reasoningEffort": "high" + "reasoningEffort": "high", + "maxTokens": 256000 }, "adapterDefaults": { - "reasoningEffort": true, "maxTokens": true }, "system": "{{system}}", @@ -3790,7 +3918,7 @@ "sessionId": "{{child-2}}", "event": { "type": "request/context", - "seq": 9, + "seq": 12, "time": 0, "data": { "provider": "deepseek-official", @@ -3800,35 +3928,17 @@ } } }, - { - "method": "session.event", - "payload": { - "sessionId": "{{parent}}", - "event": { - "type": "tool-workflow/agent-start", - "seq": 64, - "time": 0, - "data": { - "runId": "{{workflow-run}}", - "seq": 1, - "label": "workflow-child", - "phase": "Delegate", - "childId": "{{child-2}}" - } - } - } - }, { "method": "session.event", "payload": { "sessionId": "{{child-2}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 10, + "seq": 13, "time": 0, "data": { "sessionId": "{{child-2}}", - "throughSeq": 9 + "throughSeq": 12 } } } @@ -3839,7 +3949,7 @@ "sessionId": "{{child-2}}", "event": { "type": "assistant/chunk", - "seq": 11, + "seq": 14, "time": 0, "data": { "turn": 1, @@ -3859,7 +3969,7 @@ "sessionId": "{{child-2}}", "event": { "type": "assistant/chunk", - "seq": 12, + "seq": 15, "time": 0, "data": { "turn": 1, @@ -3879,7 +3989,7 @@ "sessionId": "{{child-2}}", "event": { "type": "assistant/chunk", - "seq": 13, + "seq": 16, "time": 0, "data": { "turn": 1, @@ -3902,7 +4012,7 @@ "sessionId": "{{child-2}}", "event": { "type": "assistant/chunk", - "seq": 14, + "seq": 17, "time": 0, "data": { "turn": 1, @@ -3924,7 +4034,7 @@ "sessionId": "{{child-2}}", "event": { "type": "assistant/chunk", - "seq": 15, + "seq": 18, "time": 0, "data": { "turn": 1, @@ -3945,7 +4055,7 @@ "sessionId": "{{child-2}}", "event": { "type": "assistant/message", - "seq": 16, + "seq": 19, "time": 0, "data": { "turn": 1, @@ -3971,11 +4081,11 @@ } }, "sourceEventSeqs": [ - 11, - 12, - 13, 14, - 15 + 15, + 16, + 17, + 18 ], "surfaceOp": "append" } @@ -3987,7 +4097,7 @@ "sessionId": "{{child-2}}", "event": { "type": "step/end", - "seq": 17, + "seq": 20, "time": 0, "data": { "turn": 1, @@ -4002,7 +4112,7 @@ "sessionId": "{{child-2}}", "event": { "type": "turn/end", - "seq": 18, + "seq": 21, "time": 0, "data": { "turn": 1, @@ -4043,7 +4153,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool-workflow/agent-end", - "seq": 65, + "seq": 69, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -4059,7 +4169,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool-workflow/run-end", - "seq": 66, + "seq": 70, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -4074,7 +4184,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 67, + "seq": 71, "time": 0, "data": { "turn": 1, @@ -4102,7 +4212,7 @@ } }, "sourceEventSeqs": [ - 62 + 66 ], "surfaceOp": "append" } @@ -4114,7 +4224,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 68, + "seq": 72, "time": 0, "data": { "turn": 1, @@ -4129,7 +4239,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 69, + "seq": 73, "time": 0, "data": { "turn": 1, @@ -4144,11 +4254,11 @@ "sessionId": "{{parent}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 70, + "seq": 74, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 69 + "throughSeq": 73 } } } @@ -4159,7 +4269,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 71, + "seq": 75, "time": 0, "data": { "turn": 1, @@ -4179,7 +4289,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 72, + "seq": 76, "time": 0, "data": { "turn": 1, @@ -4201,7 +4311,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 73, + "seq": 77, "time": 0, "data": { "turn": 1, @@ -4226,7 +4336,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 74, + "seq": 78, "time": 0, "data": { "turn": 1, @@ -4248,7 +4358,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 75, + "seq": 79, "time": 0, "data": { "turn": 1, @@ -4269,7 +4379,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 76, + "seq": 80, "time": 0, "data": { "turn": 1, @@ -4297,11 +4407,11 @@ } }, "sourceEventSeqs": [ - 71, - 72, - 73, - 74, - 75 + 75, + 76, + 77, + 78, + 79 ], "surfaceOp": "append" } @@ -4313,7 +4423,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 77, + "seq": 81, "time": 0, "data": { "turn": 1, @@ -4331,7 +4441,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 78, + "seq": 82, "time": 0, "data": { "turn": 1, @@ -4359,7 +4469,7 @@ } }, "sourceEventSeqs": [ - 77 + 81 ], "surfaceOp": "append" } @@ -4371,7 +4481,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 79, + "seq": 83, "time": 0, "data": { "turn": 1, @@ -4386,7 +4496,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 80, + "seq": 84, "time": 0, "data": { "turn": 1, @@ -4401,7 +4511,7 @@ "sessionId": "{{parent}}", "event": { "type": "request/header", - "seq": 81, + "seq": 85, "time": 0, "data": { "header": { @@ -4443,11 +4553,11 @@ "sessionId": "{{parent}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 82, + "seq": 86, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 81 + "throughSeq": 85 } } } @@ -4458,7 +4568,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 83, + "seq": 87, "time": 0, "data": { "turn": 1, @@ -4478,7 +4588,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 84, + "seq": 88, "time": 0, "data": { "turn": 1, @@ -4498,7 +4608,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 85, + "seq": 89, "time": 0, "data": { "turn": 1, @@ -4521,7 +4631,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 86, + "seq": 90, "time": 0, "data": { "turn": 1, @@ -4543,7 +4653,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 87, + "seq": 91, "time": 0, "data": { "turn": 1, @@ -4564,7 +4674,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 88, + "seq": 92, "time": 0, "data": { "turn": 1, @@ -4590,11 +4700,11 @@ } }, "sourceEventSeqs": [ - 83, - 84, - 85, - 86, - 87 + 87, + 88, + 89, + 90, + 91 ], "surfaceOp": "append" } @@ -4606,7 +4716,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 89, + "seq": 93, "time": 0, "data": { "turn": 1, @@ -4621,7 +4731,7 @@ "sessionId": "{{parent}}", "event": { "type": "turn/end", - "seq": 90, + "seq": 94, "time": 0, "data": { "turn": 1, @@ -4639,6 +4749,5 @@ "status": "idle" } } - ], - "session_root": "{{cwd}}/sessions" + ] } 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 6e2f62bfe1..39ac30a965 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.1.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.1.jsonl @@ -1,20 +1,23 @@ {"type":"session","version":0,"id":"{{child-1}}","createdAt":0,"cwd":"{{cwd}}","parentSession":"{{parent}}","origin":"subagent","delegationDepth":1} +{"type":"sandbox/mode","data":{"mode":"danger-full-access","source":"delegation"}} +{"type":"approval/policy","data":{"policy":"never","source":"delegation"}} +{"type":"permission/preset","data":{"preset":"danger-full-access"}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} -{"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Check direct child"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn","label":"Check direct child"}} {"type":"step/start","data":{"turn":1,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} -{"type":"session/title","data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[5],"source":{"kind":"fallback"}}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"initial"}} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} +{"type":"session/title","data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[8],"source":{"kind":"fallback"}}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","reasoningEffort":"high","maxTokens":256000},"adapterDefaults":{"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{child-1}}","throughSeq":9}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{child-1}}","throughSeq":12}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"DIRECT_CHILD_OK"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DIRECT_CHILD_OK"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[11,12,13,14,15],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[14,15,16,17,18],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","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 e11cec5548..7593678f03 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.2.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.2.jsonl @@ -1,20 +1,23 @@ {"type":"session","version":0,"id":"{{child-2}}","createdAt":0,"cwd":"{{cwd}}","parentSession":"{{parent}}","origin":"subagent","delegationDepth":1} +{"type":"sandbox/mode","data":{"mode":"danger-full-access","source":"delegation"}} +{"type":"approval/policy","data":{"policy":"never","source":"delegation"}} +{"type":"permission/preset","data":{"preset":"danger-full-access"}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} -{"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"spawn"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn"}} {"type":"step/start","data":{"turn":1,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} -{"type":"session/title","data":{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[5],"source":{"kind":"fallback"}}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"initial"}} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} +{"type":"session/title","data":{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[8],"source":{"kind":"fallback"}}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","reasoningEffort":"high","maxTokens":256000},"adapterDefaults":{"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{child-2}}","throughSeq":9}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{child-2}}","throughSeq":12}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"WORKFLOW_CHILD_OK"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"WORKFLOW_CHILD_OK"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"WORKFLOW_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[11,12,13,14,15],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"WORKFLOW_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[14,15,16,17,18],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","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 2e65037b37..a361560a35 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.jsonl @@ -1,92 +1,96 @@ {"type":"session","version":0,"id":"{{parent}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0} +{"type":"permission/preset","data":{"preset":"danger-full-access"}} +{"type":"sandbox/mode","data":{"mode":"danger-full-access"}} +{"type":"approval/policy","data":{"policy":"never"}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Run the advanced packaged-runtime snapshot scenario."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":1,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Run the advanced packaged-runtime snapshot scenario."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} -{"type":"session/title","data":{"title":"Run the advanced packaged-runtime snapsh","messageSeqs":[4],"source":{"kind":"fallback"}}} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} +{"type":"session/title","data":{"title":"Run the advanced packaged-runtime snapsh","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":7}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":11}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-define","name":"cordis_define","argumentsDelta":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"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","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"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","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"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-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"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-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[13,14,15,16,17],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"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","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-define"},"content":[{"type":"tool-result","toolCallId":"advanced-define","content":[{"type":"text","text":"Defined snap-1/pkg-1 (Snapshot Double); it is not running yet. Use cordis_run to activate this Package."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1"}},"sourceEventSeqs":[15],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-define"},"content":[{"type":"tool-result","toolCallId":"advanced-define","content":[{"type":"text","text":"Defined snap-1/pkg-1 (Snapshot Double); it is not running yet. Use cordis_run to activate this Package."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1"}},"sourceEventSeqs":[19],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":18}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":22}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-run","name":"cordis_run","argumentsDelta":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[20,21,22,23,24],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[24,25,26,27,28],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}} -{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-run"},"content":[{"type":"tool-result","toolCallId":"advanced-run","content":[{"type":"text","text":"snap-1/pkg-1 is running (run-1)."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1","pluginRunId":"run-1"}},"sourceEventSeqs":[26],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-run"},"content":[{"type":"tool-result","toolCallId":"advanced-run","content":[{"type":"text","text":"snap-1/pkg-1 is running (run-1)."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1","pluginRunId":"run-1"}},"sourceEventSeqs":[30],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"change"}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":30}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":34}} {"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-code","name":"run_code","argumentsDelta":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}} {"type":"assistant/chunk","data":{"turn":1,"step":3,"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","data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"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-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[32,33,34,35,36],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":3,"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-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[36,37,38,39,40],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":3,"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","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"snapshot_double","arguments":{"value":21}}} {"type":"tool/code-dispatch","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"snapshot_double","arguments":{"value":21},"isError":false,"content":[{"type":"text","text":"42"}]}} -{"type":"tool/result","data":{"turn":1,"step":3,"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":[38],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":3,"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":[42],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} {"type":"step/start","data":{"turn":1,"step":4}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":43}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":47}} {"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-direct-child","name":"subagent","argumentsDelta":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}} {"type":"assistant/chunk","data":{"turn":1,"step":4,"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","data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[45,46,47,48,49],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[49,50,51,52,53],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":4,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}} -{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[51],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[55],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":4}} {"type":"step/start","data":{"turn":1,"step":5}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":54}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":58}} {"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-workflow","name":"workflow","argumentsDelta":"{\"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","data":{"turn":1,"step":5,"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","data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[56,57,58,59,60],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[60,61,62,63,64],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":5,"callId":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}} {"type":"tool-workflow/run-start","data":{"runId":"{{workflow-run}}","name":"advanced-exe-snapshot"}} {"type":"tool-workflow/agent-start","data":{"runId":"{{workflow-run}}","seq":1,"label":"workflow-child","phase":"Delegate","childId":"{{child-2}}"}} {"type":"tool-workflow/agent-end","data":{"runId":"{{workflow-run}}","seq":1,"outcome":"completed"}} {"type":"tool-workflow/run-end","data":{"runId":"{{workflow-run}}","stopReason":"completed"}} -{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[62],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[66],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":5}} {"type":"step/start","data":{"turn":1,"step":6}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":69}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":73}} {"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-undefine","name":"cordis_undefine","argumentsDelta":"{\"pluginId\": \"snap-1\"}"}}} {"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[71,72,73,74,75],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[75,76,77,78,79],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":6,"callId":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}} -{"type":"tool/result","data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[77],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[81],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":6}} {"type":"step/start","data":{"turn":1,"step":7}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"change"}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":81}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":85}} {"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"text-delta","index":0,"text":"ADVANCED_EXECUTABLE_OK"}}} {"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[83,84,85,86,87],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[87,88,89,90,91],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":7}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/scripts/snapshots/python-sdk-single-exe/restart/requests.json b/scripts/snapshots/python-sdk-single-exe/restart/requests.json index dd5f925294..a7efe0f6ce 100644 --- a/scripts/snapshots/python-sdk-single-exe/restart/requests.json +++ b/scripts/snapshots/python-sdk-single-exe/restart/requests.json @@ -9,6 +9,10 @@ { "role": "user", "content": "Complete the first isolated Python SDK process turn." + }, + { + "role": "user", + "content": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)." } ], "toolNames": [ @@ -37,6 +41,10 @@ { "role": "user", "content": "Complete the second isolated Python SDK process turn." + }, + { + "role": "user", + "content": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)." } ], "toolNames": [ diff --git a/scripts/snapshots/python-sdk-single-exe/restart/result.json b/scripts/snapshots/python-sdk-single-exe/restart/result.json index 32911ef466..d1aac3e127 100644 --- a/scripts/snapshots/python-sdk-single-exe/restart/result.json +++ b/scripts/snapshots/python-sdk-single-exe/restart/result.json @@ -9,51 +9,6 @@ "agent/inbox/spliced", "step/start", "user/message", - "session/title", - "request/header", - "request/context", - "session-log-deepseek/delivery-accepted", - "assistant/chunk", - "assistant/chunk", - "assistant/chunk", - "assistant/chunk", - "assistant/chunk", - "assistant/message", - "step/end", - "turn/end" - ], - "notificationMethods": [ - "session.event", - "session.status", - "session.event", - "session.event", - "session.event", - "session.event", - "session.event", - "session.event", - "session.event", - "session.event", - "session.event", - "session.event", - "session.event", - "session.event", - "session.event", - "session.event", - "session.event", - "session.event", - "session.status" - ], - "session_root": "{{sessions}}" - }, - { - "session_id": "{{session-2}}", - "final_response": "PROCESS_TWO_OK", - "finish_reason": "completed", - "eventTypes": [ - "agent/inbox/spliced", - "turn/start", - "agent/inbox/spliced", - "step/start", "user/message", "session/title", "request/header", @@ -87,8 +42,55 @@ "session.event", "session.event", "session.event", + "session.event", "session.status" + ] + }, + { + "session_id": "{{session-2}}", + "final_response": "PROCESS_TWO_OK", + "finish_reason": "completed", + "eventTypes": [ + "agent/inbox/spliced", + "turn/start", + "agent/inbox/spliced", + "step/start", + "user/message", + "user/message", + "session/title", + "request/header", + "request/context", + "session-log-deepseek/delivery-accepted", + "assistant/chunk", + "assistant/chunk", + "assistant/chunk", + "assistant/chunk", + "assistant/chunk", + "assistant/message", + "step/end", + "turn/end" ], - "session_root": "{{sessions}}" + "notificationMethods": [ + "session.event", + "session.status", + "session.event", + "session.event", + "session.event", + "session.event", + "session.event", + "session.event", + "session.event", + "session.event", + "session.event", + "session.event", + "session.event", + "session.event", + "session.event", + "session.event", + "session.event", + "session.event", + "session.event", + "session.status" + ] } ] diff --git a/scripts/snapshots/python-sdk-single-exe/restart/session.1.jsonl b/scripts/snapshots/python-sdk-single-exe/restart/session.1.jsonl index 38e5f98ff6..babaec2193 100644 --- a/scripts/snapshots/python-sdk-single-exe/restart/session.1.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/restart/session.1.jsonl @@ -1,18 +1,22 @@ {"type":"session","version":0,"id":"{{session-1}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0} +{"type":"permission/preset","data":{"preset":"danger-full-access"}} +{"type":"sandbox/mode","data":{"mode":"danger-full-access"}} +{"type":"approval/policy","data":{"policy":"never"}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Complete the first isolated Python SDK process turn."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":1,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Complete the first isolated Python SDK process turn."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} -{"type":"session/title","data":{"title":"Complete the first isolated Python","messageSeqs":[4],"source":{"kind":"fallback"}}} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} +{"type":"session/title","data":{"title":"Complete the first isolated Python","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session-1}}","throughSeq":7}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session-1}}","throughSeq":11}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"PROCESS_ONE_OK"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PROCESS_ONE_OK"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"PROCESS_ONE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"PROCESS_ONE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[13,14,15,16,17],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/scripts/snapshots/python-sdk-single-exe/restart/session.2.jsonl b/scripts/snapshots/python-sdk-single-exe/restart/session.2.jsonl index 523f784dfa..afd9753b4a 100644 --- a/scripts/snapshots/python-sdk-single-exe/restart/session.2.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/restart/session.2.jsonl @@ -1,18 +1,22 @@ {"type":"session","version":0,"id":"{{session-2}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0} +{"type":"permission/preset","data":{"preset":"danger-full-access"}} +{"type":"sandbox/mode","data":{"mode":"danger-full-access"}} +{"type":"approval/policy","data":{"policy":"never"}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Complete the second isolated Python SDK process turn."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":1,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Complete the second isolated Python SDK process turn."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} -{"type":"session/title","data":{"title":"Complete the second isolated Python","messageSeqs":[4],"source":{"kind":"fallback"}}} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} +{"type":"session/title","data":{"title":"Complete the second isolated Python","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session-2}}","throughSeq":7}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session-2}}","throughSeq":11}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"PROCESS_TWO_OK"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PROCESS_TWO_OK"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"PROCESS_TWO_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"PROCESS_TWO_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[13,14,15,16,17],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/scripts/translation-pairing.spec.ts b/scripts/translation-pairing.spec.ts index c4dda468fe..e36b19815d 100644 --- a/scripts/translation-pairing.spec.ts +++ b/scripts/translation-pairing.spec.ts @@ -310,7 +310,7 @@ describe('translation scope discovery', () => { 'packages/example/node_modules/dependency/README.md', 'packages/example/lib/README.md', 'coverage/report/README.md', - 'python/sdk-runtime/src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-macos-arm64/README.md', + 'python/sdk-runtime/src/deepseek_harness_runtime/runtime/deepseek-harness-sdk-runtime-macos-arm64/README.md', 'python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/README.md', ])('excludes non-source or non-README path %s', (file) => { expect(isTranslationScopeFile(file)).toBe(false) diff --git a/scripts/translation-pairing.ts b/scripts/translation-pairing.ts index 95b70c48b1..e4c0919a2f 100644 --- a/scripts/translation-pairing.ts +++ b/scripts/translation-pairing.ts @@ -165,7 +165,7 @@ export const TRANSLATION_SCOPE_GLOB_EXCLUDES = [ '**/.pytest_cache/**', 'apps/web/dist/**', '.artifacts/**', - 'python/sdk-runtime/src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-*/**', + 'python/sdk-runtime/src/deepseek_harness_runtime/runtime/deepseek-harness-sdk-runtime-*/**', 'python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/**', 'vendor/**', ] @@ -177,7 +177,7 @@ function isTranslationSourceExcluded(file: string): boolean { || segment.startsWith('.doc-typecheck-') || segment.startsWith('.node-next-types-')) || file.startsWith('apps/web/dist/') - || file.startsWith('python/sdk-runtime/src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-') + || file.startsWith('python/sdk-runtime/src/deepseek_harness_runtime/runtime/deepseek-harness-sdk-runtime-') || file.startsWith('python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/') } diff --git a/scripts/verify-application-entrypoints.spec.ts b/scripts/verify-application-entrypoints.spec.ts index 02ba9a2196..ee590de033 100644 --- a/scripts/verify-application-entrypoints.spec.ts +++ b/scripts/verify-application-entrypoints.spec.ts @@ -74,12 +74,14 @@ describe('application entrypoints', () => { ]) }) - it('accepts the temporary private Python carrier source without an npm bin', () => { + it('rejects a private Python application carrier outside dsh', () => { const root = fixture() - write(root, 'packages/sdk/python-runtime/package.json', JSON.stringify({ private: true })) - write(root, 'packages/sdk/python-runtime/src/packaged-bin.ts', '#!/usr/bin/env node\n') + write(root, 'packages/sdk/rogue-python-runtime/package.json', JSON.stringify({ private: true })) + write(root, 'packages/sdk/rogue-python-runtime/src/bin.ts', '#!/usr/bin/env node\n') - expect(applicationEntrypointViolations(root)).toEqual([]) + expect(applicationEntrypointViolations(root)).toEqual([ + 'packages/sdk/rogue-python-runtime/src/bin.ts: executable source has no application/build/test classification', + ]) }) it('rejects a classified demo wrapper that launches a package entry', () => { diff --git a/scripts/verify-application-entrypoints.ts b/scripts/verify-application-entrypoints.ts index 7dec320e79..b273d6aed6 100644 --- a/scripts/verify-application-entrypoints.ts +++ b/scripts/verify-application-entrypoints.ts @@ -1,7 +1,7 @@ /** * Enforce dsh profiles as the only supported Node application launcher. - * Vendor CLIs, build tools, test tools, and the temporary private Python - * runtime carrier are explicit classifications rather than implicit holes. + * Vendor CLIs, build tools, and test tools are explicit classifications + * rather than implicit holes. */ import { existsSync, globSync, readFileSync } from 'node:fs' @@ -43,7 +43,6 @@ const EXECUTABLE_SOURCE_ALLOWLIST = new Map([ ['packages/experimental/webworker-packer/bin.js', 'private build-only wrapper'], ['packages/experimental/webworker-packer/src/bin.ts', 'private build-only implementation'], ['packages/sdk/client/tests/fake-runtime.ts', 'test-only SDK runtime peer'], - ['packages/sdk/python-runtime/src/packaged-bin.ts', 'temporary private Python runtime carrier'], ['packages/test-support/llm-mock-server/src/bin.ts', 'test-only model server'], ]) @@ -194,6 +193,6 @@ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(resolve(p for (const failure of failures) console.error(` ${failure}`) process.exitCode = 1 } else { - console.log('verify-application-entrypoints: dsh is the only supported Node application launcher; the private Python carrier remains the temporary exception.') + console.log('verify-application-entrypoints: dsh is the only supported Node application launcher.') } } diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts index 1fb184af82..f83b621965 100644 --- a/scripts/verify-package-readme-model-experience.ts +++ b/scripts/verify-package-readme-model-experience.ts @@ -167,7 +167,6 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly> = { 'packages/jobs/jobs-local': { kind: 'indirect', reason: 'The registry backend delegates model rendering to producer plugins and dsh-tool-jobs.' }, 'packages/boot/app-boot': { kind: 'indirect', reason: 'Only the loaded plugin tree contributes model context.' }, 'packages/boot/cmdline': { kind: 'none', reason: 'Resolves the process command line before any session exists; configured rows own every model-visible consequence.' }, - 'packages/sdk/python-runtime': { kind: 'indirect', reason: 'Only the externally configured plugin tree contributes model context.' }, 'packages/interaction/permission-presets': { kind: 'indirect', reason: 'The service writes mechanism events rendered by dsh-user-approval and dsh-tool-bash.' }, 'packages/interaction/user-questions': { kind: 'indirect', reason: 'Model-facing consumers render provider answers and seam errors.' }, 'packages/util/timeout': { kind: 'indirect', reason: 'Only timeout consumers render timeout outcomes.' }, diff --git a/tsconfig.base.json b/tsconfig.base.json index 2c64da3ce6..8a8bc37baa 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -108,6 +108,7 @@ "@deepseek-ai/dsh-tools/presentation": ["./packages/core/tools/src/presentation.ts"], "@deepseek-ai/dsh-tools/types": ["./packages/core/tools/src/types.ts"], "@deepseek-ai/dsh-tool-subagent-control/list-agents": ["./packages/subagent/tool-subagent-control/src/list-agents.ts"], + "@deepseek-ai/dsh-tool-subagent/model-selection-settings": ["./packages/subagent/tool-subagent/src/model-selection-settings.ts"], "@deepseek-ai/dsh-user-approval/types": ["./packages/interaction/user-approval/src/types.ts"], "@deepseek-ai/dsh-user-questions/types": ["./packages/interaction/user-questions/src/types.ts"], "@deepseek-ai/dsh-agent/types": ["./packages/core/agent/src/types.ts"], diff --git a/tsconfig.host.json b/tsconfig.host.json index b64992cdf1..0bcbe50df3 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -263,11 +263,11 @@ { "path": "./packages/bundle/base" }, { "path": "./packages/bundle/headless" }, { "path": "./packages/bundle/sdk-app" }, + { "path": "./packages/bundle/sdk-minimal" }, { "path": "./packages/bundle/web-app" }, { "path": "./packages/boot/app-boot" }, { "path": "./packages/boot/cmdline" }, { "path": "./packages/sdk/server" }, - { "path": "./packages/sdk/python-runtime" }, { "path": "./packages/test-support/llm-replay" }, { "path": "./packages/typert/generator" }, { "path": "./packages/test-support/acp-snapshot" },