Merge remote-tracking branch 'origin/master' into worktree/attachment-alignment-2

This commit is contained in:
creatixchu
2026-08-12 16:14:56 +08:00
128 changed files with 2013 additions and 285 deletions
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md
2026-06-20-generic-long-running-tool-runtime.md: c34d0708ee88b94fcf9b9294fe002a9c3e081cfd
2026-06-20-generic-long-running-tool-runtime.zh.md: 311fd271ccbd139f7d82270eb56e2471a34a4bb3
2026-06-20-generic-long-running-tool-runtime.md: 1edc3422c253e06178a5c8ebf68dfd4ef1289e31
2026-06-20-generic-long-running-tool-runtime.zh.md: 8cfc2b2ec7ecce1a358b0be63ca52e9319bf362a
@@ -25,6 +25,8 @@ Long-running tools are producers. `dsh-tool-bash` adapts a `BashProcess` into in
The literal types live on the [tasks subsystem page](../../../../docs/subsystems/tasks.md). A producer calls `ctx.tasks.start()` with a kind, label, optional owning `Agent`, optional positive `outputLimitBytes`, and a `run()` function. The runtime completes all failable preflight work before calling `run()` and invokes it once. After `run()` returns hooks, registration commits without another failable step; a producer cannot start work that lacks a collectable task id.
The process-local provider also owns bounded admission, whose rationale is recorded in the [bounded background task admission decision](../bug-fix/2026-08-11-bounded-background-task-admission.md). Its positive-safe-integer `maxConcurrentTasksPerOwner` config defaults to `10`; `start()` derives each exact `Agent` object's active count from `running` and `stopping` records, while every unowned task shares one service bucket. Capacity rejection occurs before `run()` and id allocation, and producer `done` settlement is the only event that releases a stopping task's place. The provider does not queue, preempt, or retain a second mutable count.
`outputLimitBytes` is producer-owned presentation policy, not a registry buffer. The registry validates and projects it unchanged into `TaskSnapshot`; generic control APIs apply the cap to complete model-facing output after adding their own status or notice metadata. Omitting it preserves the existing controller behavior, so the runtime does not impose a hidden default on unrelated producer families.
A model-facing producer exposes that committed id in its canonical success value, normally `{ kind: 'background', taskId }`; Native rendering may keep human-readable prose. A pre-aborted background call fails rather than returning a no-op because no task exists to satisfy the promised handle. Once registration publishes the id, cancellation belongs to the task's own controller and the task runtime: later cancellation of the producing tool call must not kill the published task. `task_kill`, owner disposal, and service teardown request cancellation; foreground execution remains coupled to the call's `exec.signal`.
@@ -55,7 +57,7 @@ For contract-compliant producers, `AgentHandle.dispose()` resolves only after ow
`TaskService` provides:
- `start(spec)` for preflighted, atomic registration.
- `start(spec)` for preflighted, provider-admitted, atomic registration.
- `get(id, caller?)` and `list(caller?)` for non-consuming snapshots.
- `read(id, caller?)` for a consuming stream delta or an idempotent final result.
- `kill(id, caller?, reason?)` for cancellation.
@@ -125,10 +127,12 @@ Authorization, not unguessability, is the access boundary, and ids do not derive
## Testing
Unit coverage pins preflight atomicity, per-kind ids, output-limit validation and projection, complete UTF-8 result bounds, stream and final reads, wait timeout and abort races, cancellation, first-wins settlement, listener containment, notice suppression, owner isolation, stale owner instances, owner cleanup, service teardown, and the no-controller fence. Producer tests cover bash process mapping, subagent startup cancellation, terminal mapping, and disposal. Snapshot coverage pins the control-tool schemas and prompt guidance.
Unit coverage pins preflight atomicity, per-kind ids, per-exact-owner and unowned-bucket admission, `stopping` occupancy, terminal release, output-limit validation and projection, complete UTF-8 result bounds, stream and final reads, wait timeout and abort races, cancellation, first-wins settlement, listener containment, notice suppression, owner isolation, stale owner instances, owner cleanup, service teardown, and the no-controller fence. Producer tests cover bash process mapping, subagent startup cancellation, terminal mapping, and disposal. Snapshot coverage pins the control-tool schemas, prompt guidance, and an assembled ACP path where the configured limit rejects a second real background Bash task with a `task_kill` recovery action.
## Consequences
Bash commands and subagents share one id vocabulary, listing, notice format, prompt habit, and set of control tools. New long-running producers implement execution hooks instead of another registry and tool family. The [tool cookbook](../../../../docs/cookbook/adding-a-tool.md) points producers to this contract.
One exact owner cannot grow process-local Task-backed work without bound, and another owner does not consume its allowance. A cancellation request keeps capacity occupied until the producer actually releases its resource, so replacing slow-stopping work cannot exceed the configured live-resource budget.
Owned background bash now stops with its agent instead of surviving it. Background processes have no executor timeout; callers must kill irrelevant work or rely on owner/service disposal. Stream reads support one consuming reader, and a producer that returns from `cancel` without settling `done` can still stall teardown. Durable jobs, independent observation cursors, and foreground promotion remain separate designs.
@@ -25,6 +25,8 @@ Status: implemented
字面类型见[任务子系统页面](../../../../docs/subsystems/tasks.md)。生产方调用 `ctx.tasks.start()`,传入 kind、label、可选的所属 `Agent`、可选的正数 `outputLimitBytes` 与一个 `run()` 函数。运行时会在调用 `run()` 前完成所有可能失败的预检工作,并且只调用一次。`run()` 返回钩子后,注册过程不会再执行可能失败的步骤而直接提交;生产方无法启动没有可收集 task id 的工作。
进程内 Service provider 还拥有有界准入,其理由记录在[有界后台任务准入决策](../bug-fix/2026-08-11-bounded-background-task-admission.md)中。它的 `maxConcurrentTasksPerOwner` 配置必须是正的安全整数,默认值为 `10``start()``running``stopping` 记录派生每个确切 `Agent` 对象的活动数量,而全部无 owner 任务共享一个服务级桶。容量拒绝发生在 `run()` 与 id 分配之前,处于 stopping 的任务只有在生产方 `done` 结算时才释放名额。Service provider 不排队或抢占任务,也不保留第二份可变计数。
`outputLimitBytes` 是生产方拥有的呈现策略,而非注册表缓冲区。注册表校验该值,并将其原样投影到 `TaskSnapshot`;通用任务控制器添加自身的状态或通知元数据后,再将该上限应用于完整的面向模型输出。省略该值时保持现有控制器行为,因此运行时不会向无关的生产方类别施加隐式默认值。
面向模型的生产方会在规范成功值中暴露已提交的 id,通常为 `{ kind: 'background', taskId }`;Native 渲染仍可保留便于人类阅读的行文。预先被中止的后台调用会失败,而不是返回空操作,因为不存在可履行所承诺句柄的任务。一旦注册过程发布 id,取消就归任务自身的控制器与任务运行时所有:随后取消生产工具调用不得终止已发布的任务。`task_kill`、所有者资源释放和服务拆除会请求取消;前台执行仍与调用的 `exec.signal` 耦合。
@@ -55,7 +57,7 @@ task id 在运行时全局可见且可预测,因此注册表会授权每次访
`TaskService` 提供:
- `start(spec)`:经过预检的原子注册。
- `start(spec)`:经过预检与 Service provider 准入的原子注册。
- `get(id, caller?)``list(caller?)`:非消费式快照。
- `read(id, caller?)`:消费式流增量或幂等的最终结果。
- `kill(id, caller?, reason?)`:取消。
@@ -125,10 +127,12 @@ bash seam 暴露 `resolve`、`run` 和 `start`。`start(spec)` 返回一个 `Bas
## 测试
单元覆盖固定预检原子性、按 kind 分配的 id、输出上限的校验与投影、完整结果的 UTF-8 字节上限、流式与最终读取、等待超时与中止竞态、取消、首次结果优先的结算、监听器隔离、通知压制、所有者隔离、陈旧的所有者实例、所有者清理、服务资源销毁和无控制器防线。生产方测试覆盖 bash 进程映射、subagent 启动取消、终止映射与释放。快照覆盖固定控制工具 schema提示词指导。
单元覆盖固定预检原子性、按 kind 分配的 id、按确切 owner 与无 owner 桶执行的准入、`stopping` 占位、终态释放、输出上限的校验与投影、完整结果的 UTF-8 字节上限、流式与最终读取、等待超时与中止竞态、取消、首次结果优先的结算、监听器隔离、通知压制、所有者隔离、陈旧的所有者实例、所有者清理、服务资源销毁和无控制器防线。生产方测试覆盖 bash 进程映射、subagent 启动取消、终止映射与释放。快照覆盖固定控制工具 schema提示词指导,以及一条组合完整的 ACP 路径:配置上限会拒绝第二个真实后台 Bash 任务,并给出 `task_kill` 恢复动作
## 后果
bash 命令与 subagent 共享一套 id 词汇、列表、通知格式、提示词习惯和控制工具。新的长时间运行生产方只需实现执行钩子,而不必再实现一套注册表与工具族。[工具实操手册](../../../../docs/cookbook/adding-a-tool.md)将生产方指向本约定。
单个确切 owner 无法再无限增加进程内由 Task 承载的工作,另一个 owner 也不会消耗它的额度。取消请求会继续占用容量,直到生产方真正释放资源,因此用新工作替换缓慢停止的任务不会突破已配置的实时资源预算。
有所属后台 bash 会随其 agent 一起停止,不再比 agent 存活更久。后台进程没有执行器超时;调用方必须终止无关工作,或依赖所有者/服务释放。流式读取只支持一个消费方;生产方的 `cancel` 返回后如果未使 `done` 完成,仍可能阻塞资源销毁。持久任务、独立观察游标和前台提升仍属于单独设计。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent 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: 01081f1e0b8027420fedbc599f99c67e67a4639b
2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: fadfc8cdcb0651665b965126ab0b25ed4218a533
2026-07-10-single-file-executable-sdk-runtime-distribution.md: 4fe9ea3c4073e249a8c961634be9d329b35a3f4e
2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: d7a4b91a48175edcc624e6af44efa3e4c7676554
@@ -40,15 +40,15 @@ The deploy root is [`python/sdk-runtime/package.json`](../../../../python/sdk-ru
### Build pipeline and artifacts
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-jsonrpc-agent-pkg deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → 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-jsonrpc-demo/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-<platform>-<arch>` land in `dist-exe/` and are copied back into the runtime directory. Linux installs build `pty.node` from source, so the builder copies it from the root install into the staged closure because legacy deploy omits that side-effect directory; macOS uses its target prebuild and emits the required `-spawn-helper` beside the executable. CI treats these products as intermediate test inputs and retains their platform wheels. All four deploy flags are grounded in measurement: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted 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-jsonrpc-agent-pkg deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → 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-jsonrpc-demo/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-<platform>-<arch>` 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. macOS uses its target prebuild and emits the required `-spawn-helper` beside the executable. CI treats these products as intermediate test inputs and retains their platform wheels. All four deploy flags are grounded in measurement: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted 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), triggered explicitly only — `workflow_dispatch`, or the `build-exe` label on a pull request; native builds on the three platforms linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64, with `~/.pkg-cache` cached; macOS ad-hoc signing is handled by pkg. Each leg drives a mock SSE model through the SDK with the default config and a custom `cordis.yml`, drives the exe directly over NDJSON JSON-RPC, verifies the JSONL and final response, and installs release-shaped wheels into a clean venv without `runtime_bin`; Linux additionally inspects GLIBC requirements and runs in a manylinux 2.28 container. A full three-target run retains four artifacts, each containing one release file: the platform-independent SDK wheel and three native runtime wheels; a subset dispatch retains the SDK wheel and selected runtime wheels. Bare executables and source bundles remain intermediate test inputs. [`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) accepts only `python-vX.Y.Z` tag pipelines whose version matches the root `package.json`, builds one SDK wheel and three native runtime wheels, then a single serialized job checks and publishes all four to the project PyPI registry. Windows is a non-goal.
CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml), triggered explicitly by `workflow_dispatch`, the `build-exe` label on a pull request, or the [public publication workflow](../process/2026-08-11-python-publication-workflow.md); 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 drives a mock SSE model through the SDK with the default config and a custom `cordis.yml`, drives the exe directly over NDJSON JSON-RPC, verifies the JSONL and final response, and installs release-shaped wheels into a clean venv without `runtime_bin`; Linux additionally inspects both the executable and native addon's GLIBC requirements and runs in a manylinux 2.28 container, 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<repository-version>` 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 and optional helper, and the build-injected `runtime/node/` closure tree. `resolve_bundled_launch_args()` automatic resolution **finds the exe only**; the node carrier is enabled only by an explicit `DSH_RUNTIME_MODE=node` (running `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/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.
[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) reads the authoritative stable `X.Y.Z` from the repository root `package.json` and stages both packages at that version, with `deepseek-harness-sdk` depending exactly on `deepseek-harness-runtime-bin==X.Y.Z`. An optional `python-vX.Y.Z` release tag is a consistency assertion and is rejected when it differs from the repository version; the source `pyproject.toml` development sentinel never determines a release version. The SDK is a `py3-none-any` wheel; each wheel-only runtime package contains one exe, and the macOS wheel also contains its architecture-matched helper. Runtime wheels use one of `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, or `py3-none-macosx_11_0_arm64`; the Hatch hook rejects sdists, universal tags, mixed-platform payloads, missing or extra helpers, and unsupported platforms.
[`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<repository-version>` 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 the macOS wheel also contains its architecture-matched helper. Runtime wheels use one of `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, or 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 helpers, and unsupported platforms.
The exe's "must be explicitly configured" hard semantic is unchanged; the zero-config experience is restored by the wrapper: when the caller gave no `cordis`, named no explicit runtime, and the environment has no `DSH_CORDIS_CONFIG`, the client explicitly injects the checked-in default `cordis.yml` (agent-core + preloaded llm-deepseek + JSONL persistence + bash-local + the `dsh-jsonrpc` serving entry, with `!!js` environment-variable fallbacks) via `DSH_CORDIS_CONFIG`.
@@ -40,15 +40,15 @@ exe 的 VFS 内是**构建产物形态的真实包树**(各包的 `lib/` + 真
### 构建管线与产物
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-jsonrpc-agent-pkg deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → 恢复被 legacy deploy 提升回源 manifest 的 `node_modules` 下的任何直接工作区包,同时省略其包内依赖树,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的每个符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 注入 pkg 配置(`bin` 指向闭包内的 `node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/packaged-bin.js``assets` 使用全量 glob,因为动态 `import()` 对 pkg 静态分析不可见,必须显式打入全部内容)→ 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 可执行文件 `dsh-jsonrpc-agent-pkg-<platform>-<arch>` 写入 `dist-exe/`,并拷回运行时目录。Linux 安装会从源码构建 `pty.node`,而 `--legacy` 部署会省略副作用目录,因此构建器会把它从根安装目录复制到暂存闭包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-jsonrpc-agent-pkg deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → 恢复被 legacy deploy 提升回源 manifest 的 `node_modules` 下的任何直接工作区包,同时省略其包内依赖树,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的每个符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 注入 pkg 配置(`bin` 指向闭包内的 `node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/packaged-bin.js``assets` 使用全量 glob,因为动态 `import()` 对 pkg 静态分析不可见,必须显式打入全部内容)→ 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 可执行文件 `dsh-jsonrpc-agent-pkg-<platform>-<arch>` 写入 `dist-exe/`,并拷回运行时目录。Linux 安装会从源码构建 `pty.node`;CI 会在打包前进入匹配架构的 manylinux 2.28 容器重新构建该 addon,而 `--legacy` 部署会省略这一副作用目录,因此构建器会把它从根安装目录复制到暂存闭包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 不会从注册表解析这些未发布名称。
CI 使用 [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml)且只允许显式触发:手动派发 `workflow_dispatch`,或给 PRPull Request添加 `build-exe` 标签。linux-x64、linux-arm64`ubuntu-24.04-arm`)和 macos-arm64 三个平台分别进行原生构建,并缓存 `~/.pkg-cache`macOS 的 ad-hoc 签名由 pkg 处理。每个平台都使用 mock SSEServer-Sent Events)模型,分别通过默认配置和自定义 `cordis.yml` 驱动 SDK,再通过 NDJSON JSON-RPC 直接驱动 exe,校验 JSONL 与最终响应;最后把发布形态的 wheel 包安装到干净的 venv 中,并在不传 `runtime_bin` 的情况下运行。Linux 还会检查 GLIBC 依赖,并在 manylinux 2.28 容器中运行。完整构建三个目标时保留 4 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 3 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-vX.Y.Z` 标签流水线,构建一个 SDK wheel 包和 3 个原生运行时 wheel 包,再由单个串行任务校验并将这 4 个文件发布到项目的 PyPI 注册表。Windows 不在目标范围内。
CI 使用 [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml)手动派发 `workflow_dispatch`PRPull Request `build-exe` 标签或[公开发布工作流](../process/2026-08-11-python-publication-workflow.md)显式触发。linux-x64、linux-arm64`ubuntu-24.04-arm`)和 macos-arm64 三个平台分别进行原生构建,并缓存 `~/.pkg-cache`macOS 的 ad-hoc 签名由 pkg 处理。每个平台都使用 mock SSEServer-Sent Events)模型,分别通过默认配置和自定义 `cordis.yml` 驱动 SDK,再通过 NDJSON JSON-RPC 直接驱动 exe,校验 JSONL 与最终响应;最后把发布形态的 wheel 包安装到干净的 venv 中,并在不传 `runtime_bin` 的情况下运行。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<repository-version>` 标签流水线,构建一个 SDK wheel 包和 3 个原生运行时 wheel 包,再由单个串行任务校验并将这 4 个文件发布到项目的 PyPI 注册表。Windows 不在目标范围内。
### Python SDK 分发:双载体,exe 用于生产,`node` 用于开发
Python SDK 位于 [`python/`](../../../../python/README.md)`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含检入的默认 `runtime/cordis.yml`、构建注入的平台 exe 与可选 helper,以及构建注入的 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 的自动解析**只查找 exe**`node` 载体仅在显式设置 `DSH_RUNTIME_MODE=node` 时启用(运行 `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/packaged-bin.js`,需要系统 Node ≥22.19),定位为本仓库成员的开发验证通道,不随 wheel 包分发。
[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) 从仓库根目录的 `package.json` 读取权威的稳定版本 `X.Y.Z`,以该版本暂存两个包,`deepseek-harness-sdk` 精确依赖 `deepseek-harness-runtime-bin==X.Y.Z`。可选的 `python-vX.Y.Z` 发布标签只是一项一致性断言,与仓库版本不同时会被拒绝;源码 `pyproject.toml` 中的开发占位版本从不决定发布版本。SDK 是 `py3-none-any` wheel 包;每个只提供 wheel 包的运行时包都包含一个 exe,macOS wheel 包还包含与其架构匹配的 helper。运行时 wheel 包使用 `py3-none-manylinux_2_28_x86_64``py3-none-manylinux_2_28_aarch64` `py3-none-macosx_11_0_arm64` 三种标签之一Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、helper 缺失或多余,以及不支持的平台。
[`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<repository-version>` 发布标签只是一项一致性断言,与仓库版本不同时会被拒绝;源码 `pyproject.toml` 中的开发占位版本从不决定发布版本。暂存过程还会把仓库许可证放入两个 wheel 包,并把第三方声明放入内置运行时 wheel 包。SDK 是 `py3-none-any` wheel 包;每个只提供 wheel 包的运行时包都包含一个 exe,macOS wheel 包还包含与其架构匹配的 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、通用标签、混合平台载荷、helper 缺失或多余,以及不支持的平台。
exe「必须显式配置」的硬语义不变;零配置体验由包装层恢复:调用方没有提供 `cordis`、没有显式指定运行时,且环境中没有 `DSH_CORDIS_CONFIG` 时,客户端将检入的默认 `cordis.yml``agent-core` + 预载的 `llm-deepseek` + JSONL 持久化 + `bash-local` + `dsh-jsonrpc` 对外服务条目,并通过 `!!js` 使用环境变量兜底)显式注入 `DSH_CORDIS_CONFIG`
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent 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-26-task-registry-seam.md
2026-07-26-task-registry-seam.md: d5864d86577839c77ab27d70c9dc1c6a79685d56
2026-07-26-task-registry-seam.zh.md: 096cf41c614d1e9e15b6421412e9dc8160e38099
2026-07-26-task-registry-seam.md: b3cbae94c2d90b0834fd3221a153808ffc763258
2026-07-26-task-registry-seam.zh.md: 2551b503069abe237d1b95610531af256f23b508
@@ -12,8 +12,8 @@ The [background-task runtime](2026-06-20-generic-long-running-tool-runtime.md) s
`tasks/` is now a three-package capability family in the bash-trio shape:
- **`@deepseek-ai/dsh-tasks` (Service Definition)** — the abstract `TaskService extends Service` owning `ctx.tasks`, the eight-method contract (`start`, `list`, `get`, `read`, `kill`, `wait`, `onTaskDone`, `attachController`), all vocabulary types (`TaskId`, `TaskKindMap`, `TaskStart`, `TaskHooks`, `TaskOutcome`, `TaskSnapshot`, `TaskRead`, `TaskDoneListener`), and the snapshot invariant companion. The class-level JSDoc states the semantics every Service provider owes: registrations outlive producer and controller fibers, owned access is session-fenced, settlement is first-wins with contained listeners, and `start` refuses work while no attached task controller serves the spec's owner (controllers and listeners are scope-layered, so one process-wide registry answers both questions per owner).
- **`@deepseek-ai/dsh-tasks-local` (Service provider)** — `LocalTaskService`, the process-local registry moved verbatim: the in-memory store, per-kind counters, waiter bookkeeping, `TASK_WAIT_TIMEOUT` deadline code, owner-cleanup effects, and force-fail teardown. The `dsh-timeout` dependency moves here with it; the Service Definition package has no provider dependencies.
- **`@deepseek-ai/dsh-tasks` (Service Definition)** — the abstract `TaskService extends Service` owning `ctx.tasks`, the nine-method contract (`start`, `list`, `get`, `read`, `kill`, `wait`, `onTaskDone`, `onTasksChanged`, `attachController`), all vocabulary types (`TaskId`, `TaskKindMap`, `TaskStart`, `TaskHooks`, `TaskOutcome`, `TaskSnapshot`, `TaskRead`, `TaskDoneListener`), and the snapshot invariant companion. The class-level JSDoc states the semantics every Service provider owes: registrations outlive producer and controller fibers, owned access is session-fenced, settlement is first-wins with contained listeners, and `start` refuses work while no attached task controller serves the spec's owner (controllers and listeners are scope-layered, so one process-wide registry answers both questions per owner).
- **`@deepseek-ai/dsh-tasks-local` (Service provider)** — `LocalTaskService`, the process-local registry: the in-memory store, per-kind id counters, waiter bookkeeping, `TASK_WAIT_TIMEOUT` deadline code, owner-cleanup effects, force-fail teardown, and the default-10 configurable admission policy. Admission derives `running` plus `stopping` capacity from the same records per exact owner, with one unowned bucket; it adds no public count or second state owner. The `dsh-timeout` dependency and Schemastery-owned provider config live here; the Service Definition package has no provider dependencies.
- **`@deepseek-ai/dsh-tool-tasks` (Consumer)** — unchanged; it injects `'tasks'` and never imports provider types.
Compositions load `dsh-tasks-local` where they previously loaded `dsh-tasks` (the CLI cordis.yml row, `agent-spine-demo`, test harnesses, the tool-catalog generator boot). Producer misconfiguration diagnostics ("background tasks unavailable: load …") name `dsh-tasks` — the Service Definition package that declares the absent `ctx.tasks` service — and the Service Definition package's own APIs (its README and the direct-mount fence) point at Service providers, so the producer message stays correct when another backend becomes the recommended default. Producers, `TaskKindMap` declaration merges, and the controller keep importing `@deepseek-ai/dsh-tasks` only.
@@ -22,7 +22,7 @@ The seam keeps the in-process contract semantics unchanged: `TaskStart.run()` st
## Alternatives considered
**Keep the concrete service until a second backend exists (status quo).** This was the original runtime note's position: extracting a Service Definition before a second provider risks freezing the wrong boundary. It lost because the boundary is no longer speculative — the eight service methods and their semantics have been stable across every producer integration since introduction, they are exactly the API `dsh-tool-tasks` and the producers already program against, and the repository convention treats swappable capabilities as three packages by default. The residual risk (a durable backend needing contract changes) is unchanged by the split: those changes would land in the Service Definition package either way, and today they would also churn every Consumer's provider dependency.
**Keep the concrete service until a second backend exists (status quo).** This was the original runtime note's position: extracting a Service Definition before a second provider risks freezing the wrong boundary. It lost because the boundary is no longer speculative — the nine service methods and their semantics have been stable across every producer integration since introduction, they are exactly the API `dsh-tool-tasks` and the producers already program against, and the repository convention treats swappable capabilities as three packages by default. The residual risk (a durable backend needing contract changes) is unchanged by the split: those changes would land in the Service Definition package either way, and today they would also churn every Consumer's provider dependency.
**Service-Definition-only extraction inside one package (export an abstract class beside the concrete one).** Rejected because it separates nothing operationally: Consumers still depend on the package that carries the provider and its dependencies, and a replacement backend still cannot ship without the local one in its graph. The package boundary is the unit of independent evolution here.
@@ -30,6 +30,6 @@ The seam keeps the in-process contract semantics unchanged: `TaskStart.run()` st
## Consequences
Bought: the task registry now matches the repository-wide seam shape; a durable, remote, or instrumented registry is a sibling Service provider implementing eight abstract methods, and no producer, controller, or `TaskKindMap` extender changes when one lands. The Service Definition README states the contract; the provider README owns the lifecycle bookkeeping facts. The registry behavior suite (owner cleanup, settlement, waits, teardown) lives with `dsh-tasks-local`; the Service Definition package keeps a stub-subclass test pinning registration under `ctx.tasks` and single-service duplication behavior, plus the probe-based invariant suite.
Bought: the task registry now matches the repository-wide seam shape; a durable, remote, or instrumented registry is a sibling Service provider implementing nine abstract methods, and no producer, controller, or `TaskKindMap` extender changes when one lands. The Service Definition README states the contract; the provider README owns the lifecycle bookkeeping facts. The registry behavior suite (owner cleanup, settlement, waits, teardown) lives with `dsh-tasks-local`; the Service Definition package keeps a stub-subclass test pinning registration under `ctx.tasks` and single-service duplication behavior, plus the probe-based invariant suite.
Cost: one more package (manifest, tsconfig, README, invariant companion), and compositions must name the Service provider package. `abstract` erases at runtime and this package name used to be the mountable registry, so the Service Definition constructor fails loudly when mounted directly — a stale composition row gets "load a Service provider such as @deepseek-ai/dsh-tasks-local" at load time instead of a half-registered `ctx.tasks` failing far from the misconfiguration.
@@ -12,8 +12,8 @@ Status: implemented
`tasks/` 如今是一个 bash 三件套形态的三包能力家族:
- **`@deepseek-ai/dsh-tasks`Service Definition**——抽象的 `TaskService extends Service`,拥有 `ctx.tasks`个方法的约定(`start``list``get``read``kill``wait``onTaskDone``attachController`)、全部词汇类型(`TaskId``TaskKindMap``TaskStart``TaskHooks``TaskOutcome``TaskSnapshot``TaskRead``TaskDoneListener`),以及快照不变式配套插件。类级 JSDoc 陈述了每个 Service provider 都必须兑现的语义:注册的存续期长于生产方与控制器的 fiber,有所有者的访问以会话为界,结算遵循首次结果优先且监听器错误被隔离,并且当没有任何已附加的任务控制器服务于 spec 的所有者时 `start` 拒绝启动工作(控制器与监听器按 scope 分层,因此一个进程级注册表能逐所有者地回答这两个问题)。
- **`@deepseek-ai/dsh-tasks-local`Service provider**——`LocalTaskService`,即原样迁移的进程内注册表:内存存储、按 kind 划分的计数器、等待方簿记、`TASK_WAIT_TIMEOUT` deadline 代码、所有者清理 effect,以及强制失败的拆除`dsh-timeout` 依赖随之迁入此包;Service Definition 包不含任何提供方依赖。
- **`@deepseek-ai/dsh-tasks`Service Definition**——抽象的 `TaskService extends Service`,拥有 `ctx.tasks`个方法的约定(`start``list``get``read``kill``wait``onTaskDone``onTasksChanged``attachController`)、全部词汇类型(`TaskId``TaskKindMap``TaskStart``TaskHooks``TaskOutcome``TaskSnapshot``TaskRead``TaskDoneListener`),以及快照不变式配套插件。类级 JSDoc 陈述了每个 Service provider 都必须兑现的语义:注册的存续期长于生产方与控制器的 fiber,有所有者的访问以会话为界,结算遵循首次结果优先且监听器错误被隔离,并且当没有任何已附加的任务控制器服务于 spec 的所有者时 `start` 拒绝启动工作(控制器与监听器按 scope 分层,因此一个进程级注册表能逐所有者地回答这两个问题)。
- **`@deepseek-ai/dsh-tasks-local`Service provider**——`LocalTaskService`,即进程内注册表:内存存储、按 kind 划分的 id 计数器、等待方簿记、`TASK_WAIT_TIMEOUT` deadline 代码、所有者清理 effect强制失败的拆除,以及默认值为 10 且可配置的准入策略。准入从同一组记录中按确切 owner 派生 `running``stopping` 容量,并为无 owner 任务使用一个共享桶;它不新增公开计数或第二个状态 owner。`dsh-timeout` 依赖与由 Schemastery 管理的 Service provider 配置都位于此包;Service Definition 包不含任何提供方依赖。
- **`@deepseek-ai/dsh-tool-tasks`(Consumer)**——保持不变;它注入 `'tasks'`,从不导入提供方类型。
各组合在原先加载 `dsh-tasks` 的位置改为加载 `dsh-tasks-local`CLI(命令行界面)的 cordis.yml 配置项、`agent-spine-demo`、各测试 harness,以及工具目录生成器的启动流程。生产方的配置错误诊断信息(「background tasks unavailable: load …」)点名 `dsh-tasks`——即声明缺失的 `ctx.tasks` 服务的 Service Definition 包;Service Definition 包自身的 API(其 README 与直接挂载防线)会指向各 Service provider,因此当另一个后端日后成为推荐默认时,生产方的消息依旧正确。生产方、`TaskKindMap` 声明合并和控制器仍然只导入 `@deepseek-ai/dsh-tasks`
@@ -22,7 +22,7 @@ Status: implemented
## 曾考虑的替代方案
**在第二个后端出现之前保持具体服务(维持现状)。**这正是运行时 Agent Note 当初的立场:在第二个 Service provider 出现前抽取 Service Definition,可能固化错误的边界。该方案落选,因为这条边界已不再是臆测:个服务方法及其语义自引入以来在每一次生产方集成中都保持稳定,它们正是 `dsh-tool-tasks` 与各生产方已经面向编程的那套接口,而且仓库约定默认将可替换能力拆成三个包。剩余风险(持久化后端可能需要变更约定)不因这次拆分而改变:无论拆分与否,这类变更都会落在 Service Definition 包里;而若维持现状,它们今天还会连带搅动每个 Consumer 的提供方依赖。
**在第二个后端出现之前保持具体服务(维持现状)。**这正是运行时 Agent Note 当初的立场:在第二个 Service provider 出现前抽取 Service Definition,可能固化错误的边界。该方案落选,因为这条边界已不再是臆测:个服务方法及其语义自引入以来在每一次生产方集成中都保持稳定,它们正是 `dsh-tool-tasks` 与各生产方已经面向编程的那套接口,而且仓库约定默认将可替换能力拆成三个包。剩余风险(持久化后端可能需要变更约定)不因这次拆分而改变:无论拆分与否,这类变更都会落在 Service Definition 包里;而若维持现状,它们今天还会连带搅动每个 Consumer 的提供方依赖。
**在单个包内仅抽取 Service Definition(在具体类旁导出一个抽象类)。**否决,因为它在运作层面并未分离任何东西:Consumer 依然依赖携带 Service provider 及其依赖项的那个包,而替换后端若不把本地 Service provider 纳入自身依赖图,就仍然无法发布。在这里,包边界才是独立演进的单位。
@@ -30,6 +30,6 @@ Status: implemented
## 后果
换来的是:任务注册表如今与全仓库通行的 seam 形态一致;持久化、远程或带插桩的注册表将是一个实现个抽象方法的同级 Service provider,这样的注册表落地时,任何生产方、控制器或 `TaskKindMap` 扩展方都无需改动。Service Definition 的 README 陈述约定;生命周期簿记方面的事实归 Service provider 的 README 所有。注册表行为测试套件(所有者清理、结算、等待、拆除)随 `dsh-tasks-local` 存放;Service Definition 包保留一个桩子类(stub subclass)测试,固定 `ctx.tasks` 下的注册行为与单一服务的重复注册行为,外加基于探针的不变式测试套件。
换来的是:任务注册表如今与全仓库通行的 seam 形态一致;持久化、远程或带插桩的注册表将是一个实现个抽象方法的同级 Service provider,这样的注册表落地时,任何生产方、控制器或 `TaskKindMap` 扩展方都无需改动。Service Definition 的 README 陈述约定;生命周期簿记方面的事实归 Service provider 的 README 所有。注册表行为测试套件(所有者清理、结算、等待、拆除)随 `dsh-tasks-local` 存放;Service Definition 包保留一个桩子类(stub subclass)测试,固定 `ctx.tasks` 下的注册行为与单一服务的重复注册行为,外加基于探针的不变式测试套件。
代价是:多出一个包,即多一份 manifest(元数据清单)、tsconfig、README 与不变式配套插件;同时各组合必须点名 Service provider 包。`abstract` 在运行时会被擦除,而这个包名过去正是可挂载的具体注册表,因此直接挂载 Service Definition 时,其构造函数会明确报错——一条陈旧的组合配置行会在加载时得到「load a Service provider such as @deepseek-ai/dsh-tasks-local」,而不是一个未完整注册的 `ctx.tasks` 在远离错误配置处才失败。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md
2026-07-29-dsh-source-launch-tsx-esm.md: b2428602a780f2880f0f803ba59b16a76b39790e
2026-07-29-dsh-source-launch-tsx-esm.zh.md: 866bc8886725788e6e619f9507f5c8ccab63f82d
2026-07-29-dsh-source-launch-tsx-esm.md: effa61d8e0f54023d6971f73149214683d79b8d6
2026-07-29-dsh-source-launch-tsx-esm.zh.md: 055b374af20559a79bd912918f2e9e0e1cf26a24
@@ -14,7 +14,7 @@ Startup latency also mattered: the off-thread `module.register()` hooks worker s
## Decision
The `dsh` TUI, Web, and headless source launches run `node --import tsx/esm`: tsx's ESM-only hook owns both TypeScript transformation and tsconfig `paths` projection. The root `dsh` script completes the repository build, then uses that vector from the repository root. The CJS hook stays off because the CLI source graph is ESM-only; measured runtime launch to the TUI banner is ~0.7s versus ~1.1s under the full tsx default and ~0.75s under the removed native chain.
The `dsh` TUI, Web, and headless source launches run `node --import tsx/esm`: tsx's ESM-only hook owns both TypeScript transformation and tsconfig `paths` projection. The root `dsh` script uses that vector directly from the repository root; artifact generation is a separate operation under the [source-launch/build separation decision](../simplification/2026-08-12-separate-source-launch-from-build.md). The CJS hook stays off because the CLI source graph is ESM-only; measured runtime launch to the TUI banner is ~0.7s versus ~1.1s under the full tsx default and ~0.75s under the removed native chain.
`scripts/tspath-loader.ts` and `apps/cli/src/tsconfig-paths-loader.ts` are deleted. With them went the loader's runtime rule of mapping a workspace import only for declared runtime dependencies — tsx applies the `paths` map unconditionally. Declaration completeness now rests on the static gates alone: `verify-cordis-config` for configured bare plugins, and workspace constraints for manifests. (That runtime rule found real bugs: `dsh-plan-mode` and `dsh-tool-tasks` imported `@deepseek-ai/dsh-llm` while declaring it only in devDependencies; since fixed.)
@@ -14,7 +14,7 @@ Status: implemented
## 决策
`dsh` 的 TUI、Web 与无头源码启动运行 `node --import tsx/esm`:由 tsx 的 ESM-only 钩子同时负责 TypeScript 转换与 tsconfig `paths` 投影。根目录的 `dsh` 脚本先完成仓库构建,然后从仓库根目录使用同一启动方式。CJS 钩子保持关闭,因为 CLI(命令行界面)源码图是纯 ESM;实测运行时启动至 TUI banner 耗时约 0.7s,对比完整 tsx 默认形态约 1.1s、已移除的原生链约 0.75s。
`dsh` 的 TUI、Web 与无头源码启动运行 `node --import tsx/esm`:由 tsx 的 ESM-only 钩子同时负责 TypeScript 转换与 tsconfig `paths` 投影。根目录的 `dsh` 脚本直接从仓库根目录使用同一启动方式;产物生成是独立操作,由[源码启动与构建分离决策](../simplification/2026-08-12-separate-source-launch-from-build.md)规定。CJS 钩子保持关闭,因为 CLI(命令行界面)源码图是纯 ESM;实测运行时启动至 TUI banner 耗时约 0.7s,对比完整 tsx 默认形态约 1.1s、已移除的原生链约 0.75s。
`scripts/tspath-loader.ts``apps/cli/src/tsconfig-paths-loader.ts` 已删除。随之消失的还有该 loader「仅为已声明运行时依赖映射 workspace import」的运行时规则——tsx 无条件应用 `paths` 映射。声明完整性现在仅由静态门禁保障:配置的裸插件走 `verify-cordis-config`manifest(元数据清单)走 workspace constraints。(该运行时规则确实发现过真实缺陷:`dsh-plan-mode``dsh-tool-tasks` 导入 `@deepseek-ai/dsh-llm` 却只声明在 devDependencies;后已修复。)
@@ -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-11-bounded-background-task-admission.md
2026-08-11-bounded-background-task-admission.md: 24512a87f554cd2d775fe76c5a6e5a700a51f2e4
2026-08-11-bounded-background-task-admission.zh.md: dc3abaf4a64a4dc5fe5cacaabd3c29e278874646
@@ -0,0 +1,55 @@
# Agent Note: Bounded background task admission
Status: implemented
English | [中文](2026-08-11-bounded-background-task-admission.zh.md)
## Problem
A model can start background Bash, PowerShell, PTY operations, and one-shot subagents in separate tool calls and later turns. The agent loop's `maxParallelToolCalls` limits only calls still executing inside one step; each background producer returns a task id immediately, so repeated starts can grow live processes or child work without bound.
The process-local task registry already owns the exact task owner and the authoritative lifecycle state, but it retained terminal history beside live records and had no admission policy. Releasing capacity when cancellation was requested would also be incorrect: a `stopping` producer may still own its process, PTY, or child until `TaskHooks.done` settles.
## Decision
`LocalTaskService` owns a `maxConcurrentTasksPerOwner` configuration field. It accepts positive safe integers, defaults to `10`, and is available through the provider's Cordis schema, the typed `agent-spine-demo` bundle, and the ACP app configuration. The bundle transports the value; the process-local provider owns its meaning.
The [generic task runtime decision](../architecture/2026-06-20-generic-long-running-tool-runtime.md) owns the shared Task lifecycle and control API; this note owns the process-local admission policy.
`start()` performs admission after the existing task-controller, task-field, and live-owner checks and before `TaskStart.run()`. It derives the active count from the registry's current records instead of storing another counter:
| Record | Occupies capacity | Release fact |
|---|---:|---|
| `running` | yes | producer `done` settles |
| `stopping` | yes | producer `done` settles |
| `completed`, `killed`, or `failed` | no | already terminal |
Owned tasks are bucketed by exact `Agent` object identity, matching owner cleanup. Replacement agents that reuse a session id receive an independent bucket. Tasks without an owner share one service-level bucket, so omitting ownership is not an unlimited bypass.
When the bucket is full, `start()` throws before producer execution and task-id allocation. The diagnostic includes the current limit and tells the model to use `task_kill`, wait until the task finishes stopping, and retry. Rejection creates no execution resource, queue entry, reservation, or public task record; a later successful start receives the next ordinary per-kind id.
Owner and service disposal keep their existing order: request cancellation, retain `stopping` occupancy while producers release resources, await settlement, then remove records. The admission policy therefore follows the same lifecycle fact used by reads, notices, and cleanup rather than treating a cancellation request as resource release.
Continuable background subagents remain outside this budget. They own durable child sessions and live Activations rather than Task records, so limiting them requires a separate result and lifecycle contract. This decision also adds no Task snapshot, session-log, wire, persistence, process-wide CPU or memory budget, queue, priority, preemption, or automatic oldest-task termination.
## Verification
The task-provider suite covers the default and explicit limits, producer-before rejection, unchanged id counters, `stopping` occupancy, every terminal release state, exact-owner isolation, same-session replacement objects, the shared unowned bucket, invalid configuration, owner cleanup, and service teardown. Spine and ACP composition tests pin typed forwarding. A keyless ACP replay boots the real Loader composition with a limit of one, starts one real background Bash process, observes the second start's actionable error, stops the first task by its returned id, and verifies that the rejected producer's marker file was never created.
## Alternatives considered
**Rely on `maxParallelToolCalls`.** Rejected because a background tool call releases its step slot as soon as it returns a task id; the setting cannot bound work that remains live across later steps and turns.
**Release capacity when `task_kill` succeeds.** Rejected because successful cancellation only changes the task to `stopping`. The producer may still hold the resource until `done` settles, so admitting a replacement immediately would exceed the configured live-resource bound.
**Use one global process bucket.** Rejected because one busy agent would deny unrelated sessions, while unowned host work still needs an explicit bounded bucket. Exact owner identity already defines the cleanup lifecycle and supplies the correct partition.
**Queue, preempt, or terminate the oldest task.** Rejected because each policy adds ordering, ownership, and cancellation behavior beyond the requested fail-closed limit. An explicit rejection lets the model decide which work is no longer needed through the existing `task_kill` control.
**Maintain a mutable active-count map.** Rejected because the registry already holds the authoritative records and statuses. A second count would require rollback and settlement synchronization while providing no user result that a direct derivation lacks.
## Consequences
One exact owner cannot keep creating Task-backed live resources indefinitely, and unrelated owners retain independent allowances. A slow stop keeps a bucket full until `done` settles, which is deliberate: the configured number bounds work that may still own resources, not cancellation requests. A producer whose `cancel` returns but whose `done` never settles holds one slot for the rest of the service lifetime and can stall teardown because the registry cannot safely infer resource release.
Admission scans the process-local registry on each start. The cost grows with retained Task history, accepted in exchange for one state authority and a default limit small enough to bound the common live set. Terminal history remains available to existing reads and listings without consuming capacity.
@@ -0,0 +1,55 @@
# Agent Note: 有界后台任务准入
Status: implemented
[English](2026-08-11-bounded-background-task-admission.md) | 中文
## 问题
模型可以在不同工具调用和后续回合中启动后台 Bash、PowerShell、PTY 操作与一次性 subagent。agent loop 的 `maxParallelToolCalls` 只限制单个步骤中尚未返回的调用;每个后台生产方会立即返回 task id,因此反复启动会让仍存活的进程或子工作无限增长。
进程内任务注册表已经拥有确切任务 owner 与权威生命周期状态,但终止历史和实时记录保存在一起,且没有准入策略。在请求取消时立即释放容量也不正确:处于 `stopping` 的生产方仍可能拥有进程、PTY 或子任务,直到 `TaskHooks.done` 结算。
## 决策
`LocalTaskService` 拥有 `maxConcurrentTasksPerOwner` 配置字段。它只接受正的安全整数,默认值为 `10`,并通过 Service provider 的 Cordis schema、typed `agent-spine-demo` 组合包与 ACP 应用配置提供。组合包只传输该值;其含义归进程内 Service provider 所有。
[通用任务运行时决策](../architecture/2026-06-20-generic-long-running-tool-runtime.md)拥有共享 Task 生命周期与控制 API;本记录只拥有进程内准入策略。
`start()` 在现有任务控制器、任务字段与存活 owner 检查之后、`TaskStart.run()` 之前执行准入。它从注册表当前记录派生活动数量,而不保存另一份计数:
| 记录 | 占用容量 | 释放事实 |
|---|---:|---|
| `running` | 是 | 生产方 `done` 结算 |
| `stopping` | 是 | 生产方 `done` 结算 |
| `completed``killed``failed` | 否 | 已经终止 |
有 owner 的任务按确切 `Agent` 对象身份分桶,与 owner 清理保持一致。复用同一会话 id 的替代 agent 获得独立桶。无 owner 的任务共享一个服务级桶,因此省略 owner 不会成为无界旁路。
桶已满时,`start()` 会在生产方执行和 task id 分配前抛出异常。诊断包含当前上限,并告诉模型使用 `task_kill`、等待任务完全停稳后再重试。拒绝不会创建执行资源、排队项、预留或公开任务记录;后续成功启动仍会取得按 kind 正常递增的下一个 id。
owner 与服务释放保留现有顺序:请求取消,在生产方释放资源期间继续让 `stopping` 占位,等待结算,然后移除记录。因此,准入策略遵循读取、通知与清理共同使用的同一生命周期事实,而不会把取消请求误当成资源释放。
可继续后台 subagent 仍不纳入此预算。它们拥有持久 child session 与实时 Activation,而不是 Task 记录;限制它们需要独立的用户结果与生命周期约定。本决策也不会新增 Task 快照、会话日志、wire、持久化、进程级 CPU 或内存预算、队列、优先级、抢占或自动终止最旧任务。
## 验证
任务 Service provider 测试覆盖默认与显式上限、生产方执行前拒绝、id 计数器不变、`stopping` 占位、每种终态释放、确切 owner 隔离、同会话替代对象、共享无 owner 桶、非法配置、owner 清理和服务拆除。spine 与 ACP 组合测试固定 typed 转发。一条 keyless ACP 回放以 1 为上限启动真实 Loader 组合,启动一个真实后台 Bash 进程,观察第二次启动返回可操作错误,按返回的 task id 停止第一个任务,并验证被拒绝生产方的标记文件从未生成。
## 曾考虑的替代方案
**依赖 `maxParallelToolCalls`。**否决,因为后台工具调用一返回 task id 就会释放其步骤槽位;该设置无法限制在后续步骤和回合中继续存活的工作。
**在 `task_kill` 成功时释放容量。**否决,因为取消成功只会把任务改为 `stopping`。生产方在 `done` 结算前仍可能持有资源,立即准入替代任务会突破已配置的实时资源上限。
**使用一个全局进程桶。**否决,因为一个繁忙 agent 会拒绝无关会话,而无 owner 的宿主工作仍需要一个明确的有界桶。确切 owner 身份已经定义清理生命周期,并提供正确分区。
**排队、抢占或终止最旧任务。**否决,因为每种策略都会增加超出 fail-closed 上限要求的顺序、所有权和取消行为。显式拒绝让模型通过现有 `task_kill` 控制自行决定哪些工作不再需要。
**维护一张可变活动计数表。**否决,因为注册表已经保存权威记录与状态。第二份计数需要回滚和结算同步,却无法提供直接派生所缺少的用户结果。
## 后果
单个确切 owner 无法再无限创建由 Task 承载的实时资源,无关 owner 则保留独立额度。缓慢停止会让桶保持满载直到 `done` 结算,这是有意行为:配置值限制的是仍可能拥有资源的工作,而不是取消请求。如果生产方的 `cancel` 返回后始终不结算 `done`,它会在服务剩余生命周期内持续占用一个名额并阻塞销毁,因为注册表无法安全推断资源已经释放。
每次启动都会扫描进程内注册表。成本随保留的 Task 历史增长;为了保持单一状态权威,并利用足以约束常见实时集合的较小默认值,接受这一代价。终止历史仍可供现有读取与列表使用,但不消耗容量。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.md
2026-07-20-dsh-cli-personal-config.md: bc2aff322de01bb9c6beebb1679b2ff9909d1fe3
2026-07-20-dsh-cli-personal-config.zh.md: 507a7188a4a77d904e3204499290f3ed22abab2c
2026-07-20-dsh-cli-personal-config.md: 58eba652b2dc8617c15313f45ed4af0a08678b08
2026-07-20-dsh-cli-personal-config.zh.md: 1c4daaec96c37fef5b5996098530ed484a5c117f
@@ -14,7 +14,7 @@ The entry modes and the personal file's name and location below are superseded b
Two coupled pieces, aligned with the `apps/` assembly tier proposed by the `dsh web` PR (#443):
**The `dsh` CLI (`apps/cli`, npm name `@deepseek-ai/dsh`).** `apps/*` is the product-assembly tier over `packages/*` libraries. One bin dispatches the default interactive TUI, `-p`/`--prompt` headless turns, and the `web` surface. The TUI boots `examples/tui-agent/cordis.yml` (or `--config`) with the invoking directory as the workspace. From a source checkout, the root `pnpm dsh` script builds the repository and runs the same entry with tsx's ESM hook; the [source-launch decision](../architecture/2026-07-29-dsh-source-launch-tsx-esm.md) owns that contract.
**The `dsh` CLI (`apps/cli`, npm name `@deepseek-ai/dsh`).** `apps/*` is the product-assembly tier over `packages/*` libraries. One bin dispatches the default interactive TUI, `-p`/`--prompt` headless turns, and the `web` surface. The TUI boots `examples/tui-agent/cordis.yml` (or `--config`) with the invoking directory as the workspace. From a source checkout, the root `pnpm dsh` script runs the same entry with tsx's ESM hook without building; the [source-launch decision](../architecture/2026-07-29-dsh-source-launch-tsx-esm.md) owns the runtime vector and the [source-launch/build separation decision](../simplification/2026-08-12-separate-source-launch-from-build.md) owns artifact generation.
**Personal config (`dsh-app-boot`).** The personal overlay lives in the Harness home — `$DSH_HOME`, else `~/.dsh` — resolved by the shared [`resolveDshHome`](../architecture/2026-07-24-single-harness-home-resolver.md) (`@deepseek-ai/dsh-paths`), the same single root skills and AGENTS.md resolve against. The dsh TUI, Web, and headless surfaces consume its two optional files; the demo bins boot their committed trees verbatim:
@@ -14,7 +14,7 @@ Status: implemented
两个耦合的部分,与 `dsh web` PR#443)提出的 `apps/` 装配层对齐:
**`dsh` CLI(命令行界面;`apps/cli`npm 名 `@deepseek-ai/dsh`)。** `apps/*` 是位于 `packages/*` 库之上的产品组装层。一个 bin 负责分发默认交互式 TUI、`-p`/`--prompt` 无头轮次和 `web` 界面。TUI 以调用目录为 workspace,启动 `examples/tui-agent/cordis.yml`(或 `--config` 指定的配置)。在源码检出中,根目录的 `pnpm dsh` 脚本先构建仓库,再使用 tsx 的 ESM hook 运行同一入口;该约定由[源码启动决策](../architecture/2026-07-29-dsh-source-launch-tsx-esm.md)维护
**`dsh` CLI(命令行界面;`apps/cli`npm 名 `@deepseek-ai/dsh`)。** `apps/*` 是位于 `packages/*` 库之上的产品组装层。一个 bin 负责分发默认交互式 TUI、`-p`/`--prompt` 无头轮次和 `web` 界面。TUI 以调用目录为 workspace,启动 `examples/tui-agent/cordis.yml`(或 `--config` 指定的配置)。在源码检出中,根目录的 `pnpm dsh` 脚本不执行构建,直接使用 tsx 的 ESM hook 运行同一入口;运行方式由[源码启动决策](../architecture/2026-07-29-dsh-source-launch-tsx-esm.md)规定,产物生成由[源码启动与构建分离决策](../simplification/2026-08-12-separate-source-launch-from-build.md)规定
**个人配置(`dsh-app-boot`)。** 个人 overlay 存放在 Harness home——`$DSH_HOME`,否则 `~/.dsh`——由共享的 [`resolveDshHome`](../architecture/2026-07-24-single-harness-home-resolver.md)`@deepseek-ai/dsh-paths`)解析,与 skills、AGENTS.md 解析所依据的单一根目录相同。dsh 的 TUI、Web 和无头界面使用其中两个可选文件;各示例 bin 仍然逐字节按已提交的配置树启动:
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-11-python-publication-workflow.md
2026-08-11-python-publication-workflow.md: 870db08e1d59ad7840fa9acf822915f83ecbd31b
2026-08-11-python-publication-workflow.zh.md: 0b2b4a71b909a510bc5a7f52132dbb0ba2bf3e67
@@ -0,0 +1,51 @@
# Agent Note: Python public publication workflow
Status: implemented
English | [中文](2026-08-11-python-publication-workflow.zh.md)
## Problem
The Python SDK comprises one platform-independent client wheel and three native runtime wheels that must carry one version and become installable as a set. Public PyPI uploads expose package metadata and files immediately, cannot replace an uploaded filename, and create a temporarily unusable SDK if its exact runtime dependency has not arrived. The private repository needs to exercise the complete native build and validation sequence without publishing any artifact externally.
## Decision
The `Release (Python)` GitHub workflow exposes credential-free validation to pull requests labeled `python-release-dry-run` and to manual runs with `publish=false`. Both paths call the native wheel builder for all three platforms, install the Linux release set on Python 3.10 and 3.14, download the four resulting artifacts, verify their exact filenames and package metadata, enforce PyPI's default per-file size limit, record SHA-256 hashes, and retain one aggregate release candidate. These jobs have only repository read permission and no registry credential or OIDC permission, and pull request events cannot enter either publication job.
A run with `publish=true` must use the `python-v<repository-version>` tag in the private automation repository, match that repository's `github.repository` to its repository-scoped `PYPI_PUBLISHER_REPOSITORY` variable, find `PUBLIC_PYPI_RELEASE_ENABLED=true`, and receive approval from the `pypi-runtime` and `pypi` GitHub environments for runtime and SDK publication, respectively. The read-only public mirror supplies the package metadata URLs but does not run release Actions. Only the two publication jobs receive `id-token: write`; PyPI Trusted Publishing exchanges the private repository identity for short-lived project credentials, so the repository stores no PyPI token.
Publication consumes the aggregate artifact produced and checked in the same workflow run. Each publication job verifies the retained `SHA256SUMS` before selecting its upload set. A runtime job uploads all three platform wheels before a dependent job uploads the SDK wheel because PyPI uploads are not atomic and the SDK pins the runtime distribution at the exact same version. Neither job checks out source or rebuilds a wheel. Separating them lets GitHub's failed-job retry resume an SDK failure without attempting to replace immutable runtime files.
Both publication actions disable public attestations. The action still uses Trusted Publishing for authentication, while omitting provenance that would disclose the private publisher repository instead of the public source mirror.
Repository versions may be stable or use the supported prerelease spellings. Tags retain the repository spelling, while wheel filenames, metadata, dependency pins, and artifact lookup use the normalized PEP 440 spelling.
The runtime package's `platforms.json` is the source of truth for native wheel tags and executable names. The repository release builder and the isolated Hatch build hook validate and load that file independently. GitHub Actions and GitLab CI call one repository-owned macOS deployment-target check for both the runtime executable and its required spawn helper, so every Mach-O file in the wheel must fit the declared platform tag.
Both Python build-system requirements pin Hatchling 1.30.1. The next available Hatchling release emits Core Metadata 2.5, which the pinned Twine 6.2.0 validator rejects; keeping the builder exact makes local, GitHub, and GitLab output agree until the validation toolchain supports that metadata version.
## Alternatives considered
**TestPyPI rehearsal.** TestPyPI is a public index, so uploading there would expose package names, metadata, and wheel contents before the repository opens. The credential-free aggregate artifact and the existing private GitLab package registry cover validation and upload-protocol rehearsal without that disclosure.
**A long-lived PyPI API token.** A stored token gives unrelated workflow steps a reusable secret and needs manual rotation. Trusted Publishing limits the credential to the registered repository, workflow, and environment and mints it only for each protected publication job.
**Building again inside the publication job.** A second build can differ from the candidate that passed native smoke tests. Publication downloads the same retained bytes and checks no source out.
**Uploading the SDK before its runtime carriers.** The SDK would become visible while its exact dependency remained unavailable if a later upload failed. Runtime-first ordering leaves partial failures without an installable client that points at missing files.
**Publishing from the public mirror.** The public mirror is a read-only source projection and does not run release Actions. Binding the PyPI publisher to it would leave no workload capable of presenting the registered OIDC identity.
**Publishing public attestations.** The default action behavior makes the Trusted Publisher repository identity publicly verifiable. That provenance identifies the private automation repository rather than the package's public source mirror, so the publication jobs disable it.
## Consequences
The complete release candidate and the public release both run from the private automation repository. Selecting `publish=true` fails before the protected publication jobs unless the publisher-repository variable, release switch, and tag identify an intentional public release. Mirroring code does not copy those private repository settings, so the read-only public mirror cannot satisfy the authorization checks.
The private automation repository owner and name, workflow filename, and each job's environment (`pypi-runtime` for runtime and `pypi` for SDK) are part of the Trusted Publisher identity. A source-repository transfer, workflow rename, or environment rename requires updating the affected PyPI publishers and the publisher-repository variable when the repository identity changes. Changing the read-only public mirror changes package metadata URLs instead, not the publishing identity.
PyPI publication remains non-atomic across the two distribution projects. Runtime-first ordering narrows the visible failure mode, while separate publication jobs and checksum verification let a failed SDK upload resume with the exact checked bytes; an uploaded filename is never replaced.
Disabling public attestations gives up public cryptographic provenance for the upload identity. Trusted Publishing still authenticates each upload, and the retained aggregate artifact keeps the checked wheel hashes inside the private release workflow.
Upgrading Hatchling now requires validating the emitted Core Metadata version with the release pipeline's pinned Twine version before changing both package build requirements together.
@@ -0,0 +1,51 @@
# Agent Note: Python 公开发布工作流
Status: implemented
[English](2026-08-11-python-publication-workflow.md) | 中文
## 问题
Python SDK 由一个平台无关的客户端 wheel 包和三个原生运行时 wheel 包组成,它们必须使用同一版本,并作为一组可安装。public PyPI 上传会立即公开包元数据和文件,无法替换已上传的同名文件;如果精确版本的运行时依赖尚未到达,还会产生暂时不可用的 SDK。私有仓库需要在不向外发布任何产物的情况下,执行完整的原生构建与验证流程。
## 决策
GitHub 的 `Release (Python)` 工作流为带有 `python-release-dry-run` 标签的拉取请求和设置 `publish=false` 的手动运行提供无凭据验证。两条路径都会为全部三个平台调用原生 wheel 包构建器,在 Python 3.10 和 3.14 上安装 Linux 发行集合,下载所得四份产物,验证其精确文件名和包元数据,执行 PyPI 默认单文件大小限制,记录 SHA-256 哈希,并保留一份汇总候选发行版。这些作业只有仓库读取权限,没有注册表凭据或 OIDC 权限,拉取请求事件无法进入任何发布作业。
设置 `publish=true` 时,运行必须在私有自动化仓库使用 `python-v<repository-version>` 标签,将该仓库的 `github.repository` 与其仓库级 `PYPI_PUBLISHER_REPOSITORY` 变量匹配,找到 `PUBLIC_PYPI_RELEASE_ENABLED=true`,并分别获得 GitHub `pypi-runtime``pypi` 环境对运行时与 SDK 发布的批准。只读公开镜像提供包元数据 URL,但不运行发布 Actions。只有两个发布作业获得 `id-token: write`PyPI Trusted Publishing 会把私有仓库身份换成短期项目凭据,因此仓库不保存 PyPI token。
发布过程使用同一次工作流运行中生成并检查过的汇总产物。每个发布作业都会在选择上传文件前验证保留的 `SHA256SUMS`。一个运行时作业先上传全部三个平台 wheel 包,再由依赖它的作业上传 SDK wheel 包,因为 PyPI 上传不是原子操作,而 SDK 会把运行时分发包固定到完全相同的版本。两个作业都不会检出源码,也不会重新构建 wheel 包。将它们拆开后,GitHub 的失败作业重试可以在 SDK 上传失败时继续执行,而不会尝试替换不可变的运行时文件。
两个发布 action 都会禁用公开 attestation。action 仍使用 Trusted Publishing 进行身份认证,同时不上传会披露私有发布仓库而非公开源码镜像的 provenance。
仓库版本可以是稳定版,也可以使用受支持的预发布写法。标签保留仓库写法,wheel 包文件名、元数据、依赖版本固定和产物查找则使用规范化的 PEP 440 写法。
运行时包的 `platforms.json` 是原生 wheel 包标签和可执行文件名的事实来源。仓库发行构建器与隔离 Hatch 构建钩子会分别校验并加载该文件。GitHub Actions 与 GitLab CI 对运行时可执行文件及其必需的 spawn helper 调用同一个仓库自有的 macOS 部署目标检查,因此 wheel 包中的每个 Mach-O 文件都必须符合声明的平台标签。
两个 Python 构建系统依赖都固定使用 Hatchling 1.30.1。下一个可用的 Hatchling 版本会生成 Core Metadata 2.5,而固定使用的 Twine 6.2.0 校验器会拒绝该版本;精确固定构建器后,本地、GitHub 与 GitLab 的输出会保持一致,直到校验工具链支持该元数据版本。
## 考虑过的替代方案
**使用 TestPyPI 演练。** TestPyPI 是公开索引,上传会在仓库开放前暴露包名、元数据和 wheel 包内容。无凭据的汇总产物与既有私有 GitLab 包注册表可以覆盖验证和上传协议演练,而不会造成这种披露。
**使用长期 PyPI API token。** 保存的 token 会让无关工作流步骤接触可复用的密钥,并需要人工轮换。Trusted Publishing 把凭据限制到已登记的仓库、工作流和环境,并且只为每个受保护的发布作业生成凭据。
**在发布作业中重新构建。** 第二次构建可能与通过原生冒烟测试的候选产物不同。发布过程下载并使用同一批已保留文件,且不检出任何源码。
**先上传 SDK,再上传运行时载体。** 如果后续上传失败,SDK 会先公开,而其精确依赖仍不可用。运行时优先的顺序使部分失败不会产生指向缺失文件的可安装客户端。
**从公开镜像发布。** 公开镜像是只读源码投影,不运行发布 Actions。将 PyPI Publisher 绑定到该镜像后,没有工作负载能够提供已登记的 OIDC 身份。
**发布公开 attestation。** action 默认行为会让 Trusted Publisher 仓库身份可公开验证。该 provenance 标识私有自动化仓库而非包的公开源码镜像,因此发布作业将其禁用。
## 后果
完整候选发行版与公开发布都从私有自动化仓库运行。选择 `publish=true` 后,只有发布仓库变量、发布开关和标签都能标识一次有意的公开发布,工作流才会进入受保护的发布作业,否则会提前失败。镜像代码不会复制这些私有仓库设置,因此只读公开镜像无法满足授权检查。
私有自动化仓库 owner 和仓库名、工作流文件名以及每个作业的环境(运行时使用 `pypi-runtime`SDK 使用 `pypi`)都是 Trusted Publisher 身份的一部分。源码仓库转移、工作流改名或环境改名后,必须更新受影响的 PyPI Publisher;仓库身份变化时还必须更新发布仓库变量。只读公开镜像发生变化时,需要修改的是包元数据 URL,而不是发布身份。
两个分发项目之间的 PyPI 发布仍然不是原子操作。运行时优先的顺序会缩小可见的失败状态;独立的发布作业和校验和验证则让失败的 SDK 上传能够从经过检查的精确文件继续执行,并且绝不替换已上传的同名文件。
禁用公开 attestation 会放弃上传身份的公开密码学 provenance。Trusted Publishing 仍会认证每次上传,而保留的汇总产物会在私有发布工作流内部保存经过检查的 wheel 包哈希。
升级 Hatchling 时,必须先使用发布流水线固定的 Twine 版本验证其生成的 Core Metadata 版本,再同时修改两个包的构建依赖。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-10-source-run-without-managed-installer.md
2026-08-10-source-run-without-managed-installer.md: ac506b72acab0dd6c92ce6111487d091b2bf4a73
2026-08-10-source-run-without-managed-installer.zh.md: 86e44a90a9e7b34ad37b6a4bfd3ad14de40868f5
2026-08-10-source-run-without-managed-installer.md: ce618c88327fc11ad0cad9042e171800a5492663
2026-08-10-source-run-without-managed-installer.zh.md: e54ec93b8bd13a36b7e8b6813899d5a1e27b788c
@@ -12,7 +12,7 @@ That lifecycle is not required to run or develop DeepSeek Harness from a source
## Decision
The repository supports source execution through its root `pnpm` scripts. The `dsh` entry in `package.json` runs `pnpm run build`, then launches `apps/cli/src/bin.ts` through `node --import tsx/esm`; build output remains visible before the CLI output. The package script forwards arguments and inherits the caller's environment, including `NODE_USE_ENV_PROXY=1` when a supporting Node version must honor `HTTP_PROXY` and `HTTPS_PROXY`. Users select Web with `pnpm dsh web` and headless execution with `pnpm dsh --profile headless "task"`. The independent ACP example remains available through `pnpm run demo:acp`.
The repository supports source execution through its root `pnpm` scripts. The `dsh` entry in `package.json` launches `apps/cli/src/bin.ts` directly through `node --import tsx/esm`; artifact generation is the separate `pnpm run build` operation defined by the [source-launch/build separation decision](2026-08-12-separate-source-launch-from-build.md). The package script forwards arguments and inherits the caller's environment, including `NODE_USE_ENV_PROXY=1` when a supporting Node version must honor `HTTP_PROXY` and `HTTPS_PROXY`. Users select Web with `pnpm dsh web` and headless execution with `pnpm dsh --profile headless "task"`. The independent ACP example remains available through `pnpm run demo:acp`.
The repository does not distribute a source installer, an installer test suite, or skills that assume a managed `current` symlink and timestamped staging worktrees. Users own source checkout placement, Git updates, and any launcher they create outside the repository.
@@ -28,4 +28,4 @@ The repository does not distribute a source installer, an installer test suite,
Source users invoke repository scripts rather than an installed `dsh` command. The repository provides no atomic upgrade cutover or preserved staging rollback checkout, and it does not automate the integration or upstream publication of personal source modifications. A future distribution mechanism must justify its ownership of installation and upgrade state, define recovery behavior, and add tests and user documentation without making the source-run path depend on it. Any future publication workflow must isolate one approved feature and obtain explicit approval before its first push and draft PR.
Verification covers repository-wide references to the removed entry points, documentation links, generated third-party-notice freshness, the build-first `package.json` command, and a source CLI smoke through the exact `node --import tsx/esm` runtime vector.
Verification covers repository-wide references to the removed entry points, documentation links, generated third-party-notice freshness, the direct `package.json` source command, and a source CLI smoke through the exact `node --import tsx/esm` runtime vector.
@@ -12,7 +12,7 @@ Status: implemented
## 决策
仓库通过根目录的 `pnpm` 脚本支持从源码运行。`package.json` 中的 `dsh`先执行 `pnpm run build`,再通过 `node --import tsx/esm` 启动 `apps/cli/src/bin.ts`构建输出会显示在 CLI(命令行界面)输出之前。该包脚本会转发参数并继承调用方环境;当支持环境代理的 Node 版本必须遵循 `HTTP_PROXY``HTTPS_PROXY` 时,调用方可设置 `NODE_USE_ENV_PROXY=1`。用户使用 `pnpm dsh web` 选择 Web,使用 `pnpm dsh --profile headless "task"` 选择无头执行。独立的 ACPAgent Client Protocol)示例仍可通过 `pnpm run demo:acp` 运行。
仓库通过根目录的 `pnpm` 脚本支持从源码运行。`package.json` 中的 `dsh` 项通过 `node --import tsx/esm` 直接启动 `apps/cli/src/bin.ts`产物生成是独立的 `pnpm run build` 操作,由[源码启动与构建分离决策](2026-08-12-separate-source-launch-from-build.md)规定。该包脚本会转发参数并继承调用方环境;当支持环境代理的 Node 版本必须遵循 `HTTP_PROXY``HTTPS_PROXY` 时,调用方可设置 `NODE_USE_ENV_PROXY=1`。用户使用 `pnpm dsh web` 选择 Web,使用 `pnpm dsh --profile headless "task"` 选择无头执行。独立的 ACPAgent Client Protocol)示例仍可通过 `pnpm run demo:acp` 运行。
仓库不分发源码安装器、安装器测试套件,也不分发依赖受管理的 `current` 符号链接和带时间戳 staging worktree 的 skill。源码检出的存放位置、Git 更新,以及用户在仓库外创建的任何启动器均由用户负责。
@@ -28,4 +28,4 @@ Status: implemented
源码用户通过仓库脚本运行程序,而非使用已安装的 `dsh` 命令。仓库不提供原子升级切换,也不保留 staging 回滚检出;仓库同样不会自动集成个人源码修改或将其发布到上游。未来的分发机制必须说明为何应由其管理安装和升级状态,定义恢复行为,并补充测试与用户文档,同时不得让源码运行路径依赖该机制。未来任何发布工作流都必须隔离出一项获批功能,并在首次推送和创建草稿 PR(Pull Request)前取得明确批准。
验证范围包括仓库内对已移除入口点的所有引用、文档链接、生成的第三方声明文件的新鲜度、`package.json` 中的先构建后启动命令,以及通过准确的 `node --import tsx/esm` 运行方式对源码 CLI 进行的冒烟测试。
验证范围包括仓库内对已移除入口点的所有引用、文档链接、生成的第三方声明文件的新鲜度、`package.json` 中的直接源码启动命令,以及通过准确的 `node --import tsx/esm` 运行方式对源码 CLI 进行的冒烟测试。
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-12-separate-source-launch-from-build.md
2026-08-12-separate-source-launch-from-build.md: d3f2d21c9bf74c699c1468f99074d88ab2138cb4
2026-08-12-separate-source-launch-from-build.zh.md: 2cc2decd0a46bd1c30bd7113a665740dcf359947
@@ -0,0 +1,38 @@
# Agent Note: Separate source launch from repository build
Status: implemented
English | [中文](2026-08-12-separate-source-launch-from-build.zh.md)
## Problem
The TypeScript source launcher does not need a complete repository build for every invocation. The Web surface does need built frontend and client-plugin artifacts. Making one package script own both operations adds repository-wide build latency to repeated TUI, headless, and Web startup and obscures when browser artifacts are refreshed.
Source modules reached through tsx and browser modules reached through built bundles have different freshness behavior. Separating their commands requires explicit ownership of artifact production and an accurate failure model for missing and stale output.
## Decision
The root `dsh` script only runs `node --import tsx/esm apps/cli/src/bin.ts`. `pnpm run build` remains the separate operation that generates package and frontend artifacts. Source users run the build before the first production-like launch and whenever frontend or client-plugin artifacts need refreshing.
Missing TypeRT host artifacts fail profile boot through module-resolution errors without a build instruction. Once those host artifacts exist, missing frontend and client-plugin artifacts fail at startup with diagnostics that direct the user to `pnpm run build`. The launcher does not validate artifact freshness: existing stale frontend or client-plugin bundles are accepted and can run older browser code until the next build. After package Node halves have been built once, `pnpm run dev:web` rebuilds only packages that declare `dsh.client`; it keeps client-plugin bundles current and activates their hot-reload path, but does not rebuild the frontend shell.
This decision owns build scheduling only. The [tsx ESM source-launch decision](../architecture/2026-07-29-dsh-source-launch-tsx-esm.md) owns TypeScript transformation and workspace resolution, the [source-run decision](2026-08-10-source-run-without-managed-installer.md) owns repository scripts as the supported checkout entry points, and the [personal-config decision](../feature/2026-07-20-dsh-cli-personal-config.md) owns the machine-level configuration layer.
## Alternatives considered
**Build before every source launch.** This provides the strongest default freshness guarantee, but charges every invocation for repository-wide artifact generation even when the relevant outputs are already current.
**Build only when an artifact is missing.** This avoids some startup work but leaves stale output undetected while making build behavior implicit and dependent on the current filesystem contents.
**Start the Web artifact watcher from `pnpm dsh`.** This keeps client-plugin bundles current but changes a one-shot launcher into an owner of another long-lived process. The explicit `pnpm run dev:web` command already owns that development lifecycle.
## Consequences
- Repeated source launches do not wait for a complete repository build, and build output is not mixed with CLI output.
- Source users own artifact freshness. Missing artifacts stop startup, but only frontend and client-plugin failures direct users to `pnpm run build`; existing stale frontend and client-plugin bundles can silently serve older browser code.
- TUI, Web, and headless selection, argument forwarding, environment inheritance, and the tsx ESM launch vector remain unchanged.
- The root onboarding and CLI reference show build and launch as separate commands and document the stale-artifact behavior.
## Verification
`apps/cli/tests/source-launch.compat.spec.ts` pins the exact root package command and exercises the production source-launch vector. `packages/bundle/web-app/tests/web-app.spec.ts` and `packages/client/modules/tests/node-half.client.spec.ts` pin the missing-artifact diagnostics.
@@ -0,0 +1,38 @@
# Agent Note: 将源码启动与仓库构建分离
Status: implemented
[English](2026-08-12-separate-source-launch-from-build.md) | 中文
## 问题
TypeScript 源码启动器无需在每次调用前完成整个仓库的构建。Web 界面则需要已构建的前端与 Client plugin 产物。由同一个包脚本同时负责这两项操作,会让重复启动 TUI、无头模式和 Web 时都承担全仓库构建延迟,也会掩盖浏览器产物何时刷新。
经由 tsx 加载的源码模块与经由已构建组合包加载的浏览器模块具有不同的新鲜度表现。将两条命令分离后,需要明确产物生成的责任,并准确说明产物缺失与过期时的失败模式。
## 决策
根目录的 `dsh` 脚本只运行 `node --import tsx/esm apps/cli/src/bin.ts``pnpm run build` 仍是生成包与前端产物的独立操作。源码用户在首次进行类生产启动前运行构建,并在前端或 Client plugin 产物需要刷新时再次运行。
TypeRT Host 产物缺失时,profile 启动会因不含构建指引的模块解析错误而失败。这些 Host 产物存在后,如果前端或 Client plugin 产物缺失,启动会失败,诊断信息会指示用户运行 `pnpm run build`。启动器不会验证产物是否为最新:已有的陈旧前端或 Client plugin 组合包仍会被接受,并可能继续运行旧版浏览器代码,直至下次构建。各包的 Node 半侧至少构建过一次后,`pnpm run dev:web` 只重建声明了 `dsh.client` 的包;它会保持 Client plugin 组合包为最新状态并启用其热重载路径,但不会重建前端 shell。
本决策仅规定构建调度。[tsx ESM 源码启动决策](../architecture/2026-07-29-dsh-source-launch-tsx-esm.md)规定 TypeScript 转换与 workspace 解析,[源码运行决策](2026-08-10-source-run-without-managed-installer.md)规定以仓库脚本作为受支持的检出入口,[个人配置决策](../feature/2026-07-20-dsh-cli-personal-config.md)规定机器级配置层。
## 考虑过的备选方案
**每次源码启动前都执行构建。**这样可提供最强的默认新鲜度保证,但即使相关产物已经是最新状态,每次调用仍要承担全仓库产物生成的开销。
**仅在产物缺失时执行构建。**这样可避免部分启动开销,但无法发现过期产物,还会让构建行为变成由当前文件系统内容决定的隐式策略。
**由 `pnpm dsh` 启动 Web 产物 watcher。**这样可保持 Client plugin 组合包为最新状态,却会让一次性启动器负责另一个长时间运行的进程。显式的 `pnpm run dev:web` 命令已经负责这套开发生命周期。
## 影响
- 重复的源码启动无需等待完整的仓库构建,构建输出也不会与 CLI 输出混在一起。
- 源码用户负责产物新鲜度。产物缺失会阻止启动,但只有前端与 Client plugin 产物缺失的错误会指示用户运行 `pnpm run build`;已有的过期前端与 Client plugin 组合包可能静默提供旧版浏览器代码。
- TUI、Web 与无头模式选择、参数转发、环境继承,以及 tsx ESM 启动方式保持不变。
- 根目录上手指南与 CLI 参考将构建和启动列为独立命令,并说明过期产物行为。
## 验证
`apps/cli/tests/source-launch.compat.spec.ts` 固定根目录包命令的准确内容,并执行生产源码启动方式。`packages/bundle/web-app/tests/web-app.spec.ts``packages/client/modules/tests/node-half.client.spec.ts` 固定产物缺失诊断。
+76 -13
View File
@@ -4,10 +4,22 @@ name: Build single-exe
# .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md.
# A full target run retains one SDK wheel and three runtime wheels; subset
# dispatch retains the SDK wheel and selected runtime wheels. Bare executables
# and source closures are test inputs. Run manually or label a PR
# `build-exe` (remove and reapply to rerun). Checkout uses the triggering ref,
# so dispatch needs no separate ref input.
# and source closures are test inputs. Run manually, label a PR `build-exe`
# (remove and reapply to rerun), or call it from the Python release workflow.
# Checkout uses the triggering ref, so dispatch needs no separate ref input.
on:
workflow_call:
inputs:
targets:
description: Comma-separated pkg targets to build; empty builds all three.
type: string
required: false
default: ''
release:
description: Run as the native builder for the Python release workflow.
type: boolean
required: false
default: false
workflow_dispatch:
inputs:
targets:
@@ -22,7 +34,9 @@ on:
types: [labeled]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
# Keep the called workflow distinct from its caller's concurrency group;
# github.workflow identifies the caller inside a reusable workflow.
group: build-single-exe-${{ github.ref }}
cancel-in-progress: true
permissions:
@@ -38,12 +52,13 @@ jobs:
# construct the matrix before the dependent jobs.
plan:
name: plan targets
if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'build-exe'
if: inputs.release || github.event_name == 'workflow_dispatch' || github.event.label.name == 'build-exe'
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
matrix: ${{ steps.plan.outputs.matrix }}
version: ${{ steps.version.outputs.version }}
repository-version: ${{ steps.version.outputs.repository-version }}
steps:
- uses: actions/checkout@v6
@@ -51,12 +66,15 @@ jobs:
id: version
run: |
set -euo pipefail
version="$(jq -r '.version // empty' package.json)"
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
echo "::error::package.json version must be stable X.Y.Z, got '$version'"
exit 1
}
echo "version=$version" >> "$GITHUB_OUTPUT"
python3 - <<'PY' >> "$GITHUB_OUTPUT"
import runpy
release = runpy.run_path("scripts/build-python-release.py")
repository_version = release["repository_version"]()
wheel_version = release["pep440_version"](repository_version)
print(f"repository-version={repository_version}")
print(f"version={wheel_version}")
PY
- name: Compute matrix from targets input
id: plan
@@ -116,6 +134,7 @@ jobs:
name: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
path: dist-python/deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
if-no-files-found: error
retention-days: 7
build:
needs: [plan, sdk-wheel]
@@ -157,6 +176,41 @@ jobs:
- name: Install (immutable)
run: pnpm install --frozen-lockfile
- name: Rebuild Linux node-pty against manylinux 2.28
if: runner.os == 'Linux'
env:
RUNNER_ARCH: ${{ runner.arch }}
run: |
set -euo pipefail
case "$RUNNER_ARCH" in
X64) image=quay.io/pypa/manylinux_2_28_x86_64 ;;
ARM64) image=quay.io/pypa/manylinux_2_28_aarch64 ;;
*) echo "::error::Unsupported Linux runner architecture $RUNNER_ARCH"; exit 1 ;;
esac
addon_dir="$(realpath packages/subprocess/subprocess-local/node_modules/node-pty)"
addon="$addon_dir/build/Release/pty.node"
[ -f "$addon_dir/build/Makefile" ] || {
echo "::error::node-pty install did not generate $addon_dir/build/Makefile"
exit 1
}
docker run --rm \
--user "$(id -u):$(id -g)" \
-v "$PWD:$PWD" \
-v "$HOME/.cache/node-gyp:$HOME/.cache/node-gyp:ro" \
-v "$HOME/setup-pnpm:$HOME/setup-pnpm:ro" \
-w "$addon_dir" \
"$image" \
bash -euxo pipefail -c \
'rm -rf build/Release && make -C build -j2 BUILDTYPE=Release'
[ -f "$addon" ] || { echo "::error::$addon missing after manylinux rebuild"; exit 1; }
readelf --version-info "$addon" | tee node-pty-glibc-versions.txt
maximum="$(sed -n 's/.*Name: GLIBC_\([0-9.]*\).*/\1/p' node-pty-glibc-versions.txt | sort -V | tail -1)"
[ -n "$maximum" ] || { echo "::error::No GLIBC requirements found in $addon"; exit 1; }
dpkg --compare-versions "$maximum" le 2.28 || {
echo "::error::node-pty addon requires GLIBC_$maximum but wheel claims manylinux_2_28"
exit 1
}
- name: Build single-exe
run: pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets=${{ matrix.target }}
@@ -173,7 +227,7 @@ jobs:
case "$platform" in
linux-x64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_x86_64.whl ;;
linux-arm64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_aarch64.whl ;;
macos-arm64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-macosx_11_0_arm64.whl ;;
macos-arm64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-macosx_14_0_arm64.whl ;;
*) echo "::error::Unsupported runtime platform $platform"; exit 1 ;;
esac
echo "platform=$platform" >> "$GITHUB_OUTPUT"
@@ -224,6 +278,14 @@ jobs:
exit 1
}
- name: Check macOS deployment target
if: runner.os == 'macOS'
env:
EXE: ${{ steps.runtime.outputs.exe }}
run: >-
python3 scripts/check-macos-deployment-target.py
"$EXE" "$EXE-spawn-helper"
- name: Run wheel in a manylinux 2.28 container
if: runner.os == 'Linux'
env:
@@ -236,7 +298,7 @@ jobs:
ARM64) image=quay.io/pypa/manylinux_2_28_aarch64 ;;
*) echo "::error::Unsupported Linux runner architecture $RUNNER_ARCH"; exit 1 ;;
esac
docker run --rm -e VERSION -v "$PWD:/work" -w /work "$image" bash -euxo pipefail -c '
docker run --rm -e VERSION -e DSH_TELEMETRY_DISABLED -v "$PWD:/work" -w /work "$image" bash -euxo pipefail -c '
/opt/python/cp310-cp310/bin/python -m venv /tmp/dsh-sdk
/tmp/dsh-sdk/bin/python -m pip install --find-links /work/dist-python deepseek-harness-sdk=="$VERSION"
/tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-default
@@ -247,3 +309,4 @@ jobs:
name: ${{ steps.runtime.outputs.wheel }}
path: dist-python/${{ steps.runtime.outputs.wheel }}
if-no-files-found: error
retention-days: 7
+244
View File
@@ -0,0 +1,244 @@
name: Release (Python)
# A PR labeled python-release-dry-run or a manual run with publish=false builds
# and validates the complete release without registry credentials. Publication
# is accepted only from a manual run on the matching python-v* tag when the
# private publisher-repository identity and public-PyPI switch are configured.
on:
workflow_dispatch:
inputs:
publish:
description: Publish the validated wheels to public PyPI. Must run from a python-v* tag.
required: true
type: boolean
default: false
pull_request:
types: [labeled]
permissions:
contents: read
concurrency:
# Public runs stay globally serialized across tags. Dry runs remain isolated
# by ref so they do not block an intentional publication.
group: ${{ github.event_name == 'workflow_dispatch' && inputs.publish && 'python-publication' || format('{0}-{1}', github.workflow, github.ref) }}
cancel-in-progress: false
jobs:
build:
name: Build four wheels
if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'python-release-dry-run'
uses: ./.github/workflows/build-exe-for-python-sdk.yml
with:
targets: node24-linux-x64,node24-linux-arm64,node24-macos-arm64
release: true
python-compat:
name: Python ${{ matrix.python }} / installed SDK
needs: build
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
python: ['3.10', '3.14']
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-python@v6.3.0
with:
python-version: ${{ matrix.python }}
- uses: actions/download-artifact@v8
with:
pattern: deepseek_harness_*
path: dist
merge-multiple: true
- name: Resolve installed wheel version
id: compatibility-version
run: |
python - <<'PY' >> "$GITHUB_OUTPUT"
import runpy
release = runpy.run_path("scripts/build-python-release.py")
repository_version = release["repository_version"]()
print(f"version={release['pep440_version'](repository_version)}")
PY
- name: Install and run the published entry path
run: |
python -m pip install --find-links dist "deepseek-harness-sdk==${{ steps.compatibility-version.outputs.version }}"
python scripts/smoke-python-runtime.py --scenario sdk-default
validate:
name: Validate release candidate
needs: [build, python-compat]
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-python@v6.3.0
with:
python-version: '3.10'
- name: Resolve release version
id: version
run: |
python3 - <<'PY' >> "$GITHUB_OUTPUT"
import runpy
release = runpy.run_path("scripts/build-python-release.py")
repository_version = release["repository_version"]()
wheel_version = release["pep440_version"](repository_version)
print(f"repository-version={repository_version}")
print(f"version={wheel_version}")
PY
- name: Authorize publication request
env:
PUBLISH: ${{ github.event_name == 'workflow_dispatch' && inputs.publish }}
PUBLIC_PYPI_RELEASE_ENABLED: ${{ vars.PUBLIC_PYPI_RELEASE_ENABLED }}
PYPI_PUBLISHER_REPOSITORY: ${{ vars.PYPI_PUBLISHER_REPOSITORY }}
REPOSITORY: ${{ github.repository }}
REF_NAME: ${{ github.ref_name }}
REF_TYPE: ${{ github.ref_type }}
REPOSITORY_VERSION: ${{ steps.version.outputs.repository-version }}
run: |
set -euo pipefail
if [ "$PUBLISH" = true ]; then
[ -n "$PYPI_PUBLISHER_REPOSITORY" ] || {
echo "::error::Set the repository variable PYPI_PUBLISHER_REPOSITORY before publication."
exit 1
}
[ "$REPOSITORY" = "$PYPI_PUBLISHER_REPOSITORY" ] || {
echo "::error::This repository is not the configured PyPI publisher repository."
exit 1
}
[ "$PUBLIC_PYPI_RELEASE_ENABLED" = true ] || {
echo "::error::Set PUBLIC_PYPI_RELEASE_ENABLED=true before public publication."
exit 1
}
[ "$REF_TYPE" = tag ] && [ "$REF_NAME" = "python-v$REPOSITORY_VERSION" ] || {
echo "::error::Publication must run from tag python-v$REPOSITORY_VERSION."
exit 1
}
fi
- uses: actions/download-artifact@v8
with:
pattern: deepseek_harness_*
path: dist
merge-multiple: true
- name: Check release contents
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
set -euo pipefail
expected="$(mktemp)"
actual="$(mktemp)"
printf '%s\n' \
"deepseek_harness_runtime_bin-$VERSION-py3-none-macosx_14_0_arm64.whl" \
"deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_aarch64.whl" \
"deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_x86_64.whl" \
"deepseek_harness_sdk-$VERSION-py3-none-any.whl" > "$expected"
find dist -maxdepth 1 -type f -name '*.whl' -exec basename {} \; | sort > "$actual"
diff -u "$expected" "$actual"
while IFS= read -r wheel; do
size="$(stat -c '%s' "dist/$wheel")"
[ "$size" -lt 100000000 ] || {
echo "::error::$wheel is $size bytes; public PyPI accepts at most 100000000 bytes by default."
exit 1
}
done < "$actual"
- name: Validate package metadata
run: |
python -m pip install twine==6.2.0
python -m twine check dist/*.whl
- name: Record artifact hashes
run: |
cd dist
sha256sum *.whl | sort -k2 > SHA256SUMS
cat SHA256SUMS
- uses: actions/upload-artifact@v7
with:
name: python-release-${{ steps.version.outputs.version }}
path: dist/*
if-no-files-found: error
retention-days: 7
publish-runtime:
name: Publish runtime wheels to public PyPI
if: github.event_name == 'workflow_dispatch' && inputs.publish
needs: validate
runs-on: ubuntu-latest
timeout-minutes: 10
environment: pypi-runtime
permissions:
contents: read
id-token: write
steps:
- uses: actions/download-artifact@v8
with:
name: python-release-${{ needs.validate.outputs.version }}
path: dist
- name: Verify release artifact hashes
run: cd dist && sha256sum -c SHA256SUMS
- name: Select runtime wheels
run: |
mkdir -p dist/runtime
mv dist/deepseek_harness_runtime_bin-*.whl dist/runtime/
- name: Publish runtime wheels
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/runtime/
# Public attestations reveal the private publisher repository. OIDC
# authentication remains enabled without uploading that provenance.
attestations: false
# Keep the SDK in a dependent job. If its upload fails after the immutable
# runtime files arrive, "re-run failed jobs" resumes here without attempting
# to overwrite the runtime release.
publish-sdk:
name: Publish SDK wheel to public PyPI
if: github.event_name == 'workflow_dispatch' && inputs.publish
needs: [validate, publish-runtime]
runs-on: ubuntu-latest
timeout-minutes: 10
environment: pypi
permissions:
contents: read
id-token: write
steps:
- uses: actions/download-artifact@v8
with:
name: python-release-${{ needs.validate.outputs.version }}
path: dist
- name: Verify release artifact hashes
run: cd dist && sha256sum -c SHA256SUMS
- name: Select SDK wheel
run: |
mkdir -p dist/sdk
mv dist/deepseek_harness_sdk-*.whl dist/sdk/
- name: Publish SDK wheel
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/sdk/
attestations: false
+14 -8
View File
@@ -1,6 +1,6 @@
workflow:
rules:
- if: '$CI_COMMIT_TAG =~ /^python-v\d+\.\d+\.\d+$/'
- if: '$CI_COMMIT_TAG =~ /^python-v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$/'
- when: never
stages:
@@ -16,6 +16,7 @@ variables:
- python3 -m venv .ci-python
- . .ci-python/bin/activate
- export DSH_VERSION="$(python -c 'import json; print(json.load(open("package.json"))["version"])')"
- export DSH_WHEEL_VERSION="$(python -c 'import runpy; release = runpy.run_path("scripts/build-python-release.py"); print(release["pep440_version"](release["repository_version"]()))')"
- test "$CI_COMMIT_TAG" = "python-v$DSH_VERSION" || { echo "Tag $CI_COMMIT_TAG does not match package.json version $DSH_VERSION"; exit 1; }
- python -m pip install uv==0.11.23
@@ -42,7 +43,7 @@ sdk-wheel:
- 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"
- python -m venv .wheel-smoke
- .wheel-smoke/bin/python -m pip install --find-links "release/$PLATFORM" --find-links release/sdk deepseek-harness-sdk=="$DSH_VERSION"
- .wheel-smoke/bin/python -m pip install --find-links "release/$PLATFORM" --find-links release/sdk deepseek-harness-sdk=="$DSH_WHEEL_VERSION"
- .wheel-smoke/bin/python scripts/smoke-python-runtime.py --scenario sdk-default
- |
if [ "${PLATFORM#linux-}" != "$PLATFORM" ]; then
@@ -55,7 +56,11 @@ sdk-wheel:
linux-arm64) image=quay.io/pypa/manylinux_2_28_aarch64 ;;
*) echo "Unsupported Linux platform $PLATFORM"; exit 1 ;;
esac
docker run --rm -v "$PWD:/work" -w /work "$image" bash -euxo pipefail -c "/opt/python/cp310-cp310/bin/python -m venv /tmp/dsh-sdk && /tmp/dsh-sdk/bin/python -m pip install --find-links /work/release/$PLATFORM --find-links /work/release/sdk deepseek-harness-sdk==$DSH_VERSION && /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-default"
docker run --rm -v "$PWD:/work" -w /work "$image" bash -euxo pipefail -c "/opt/python/cp310-cp310/bin/python -m venv /tmp/dsh-sdk && /tmp/dsh-sdk/bin/python -m pip install --find-links /work/release/$PLATFORM --find-links /work/release/sdk deepseek-harness-sdk==$DSH_WHEEL_VERSION && /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-default"
fi
- |
if [ "$PLATFORM" = macos-arm64 ]; then
python3 scripts/check-macos-deployment-target.py "$EXE" "$EXE-spawn-helper"
fi
artifacts:
paths: [release/$PLATFORM/*.whl]
@@ -108,16 +113,17 @@ publish-python:
- python3 -m venv .ci-python
- . .ci-python/bin/activate
- export DSH_VERSION="$(python -c 'import json; print(json.load(open("package.json"))["version"])')"
- export DSH_WHEEL_VERSION="$(python -c 'import runpy; release = runpy.run_path("scripts/build-python-release.py"); print(release["pep440_version"](release["repository_version"]()))')"
- test "$CI_COMMIT_TAG" = "python-v$DSH_VERSION" || { echo "Tag $CI_COMMIT_TAG does not match package.json version $DSH_VERSION"; exit 1; }
- python -m pip install twine==6.2.0
script:
- test "$(find release -name '*.whl' | wc -l | tr -d ' ')" = 4
- test -f "release/sdk/deepseek_harness_sdk-${DSH_VERSION}-py3-none-any.whl"
- test -f "release/linux-x64/deepseek_harness_runtime_bin-${DSH_VERSION}-py3-none-manylinux_2_28_x86_64.whl"
- test -f "release/linux-arm64/deepseek_harness_runtime_bin-${DSH_VERSION}-py3-none-manylinux_2_28_aarch64.whl"
- test -f "release/macos-arm64/deepseek_harness_runtime_bin-${DSH_VERSION}-py3-none-macosx_11_0_arm64.whl"
- test -f "release/sdk/deepseek_harness_sdk-${DSH_WHEEL_VERSION}-py3-none-any.whl"
- test -f "release/linux-x64/deepseek_harness_runtime_bin-${DSH_WHEEL_VERSION}-py3-none-manylinux_2_28_x86_64.whl"
- test -f "release/linux-arm64/deepseek_harness_runtime_bin-${DSH_WHEEL_VERSION}-py3-none-manylinux_2_28_aarch64.whl"
- test -f "release/macos-arm64/deepseek_harness_runtime_bin-${DSH_WHEEL_VERSION}-py3-none-macosx_14_0_arm64.whl"
- python -m twine check release/*/*.whl
- export TWINE_USERNAME=gitlab-ci-token
- export TWINE_PASSWORD="$CI_JOB_TOKEN"
- export TWINE_REPOSITORY_URL="$CI_API_V4_URL/projects/$CI_PROJECT_ID/packages/pypi"
- python -m twine upload --non-interactive release/*/*.whl || { echo 'Publish failed. GitLab does not overwrite an existing version; create a new python-vX.Y.Z tag.'; exit 1; }
- python -m twine upload --non-interactive release/*/*.whl || { echo 'Publish failed. GitLab does not overwrite an existing version; create a new python-v<repository-version> tag.'; exit 1; }
+1 -1
View File
@@ -73,7 +73,7 @@ pnpm run hygiene # knip + publint + workspace constraints + NodeNext cons
pnpm run check:windows-wine # ONLY when diagnosing a known Windows failure (needs wine); CI owns this signal
pnpm run doc-sync # all documentation gates; leaf list in scripts/run-gates.ts
pnpm run website:build # VitePress build (doubles as dead-link check)
pnpm dsh --profile headless "task" # build, then run one task (needs DEEPSEEK_API_KEY)
pnpm dsh --profile headless "task" # run one task from source (needs DEEPSEEK_API_KEY)
pnpm run demo:cordis # the agent modifies its own runtime (needs key)
pnpm run demo:acp # ACP automation server (needs DEEPSEEK_API_KEY)
```
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write README.md
README.md: 785d7dd41cb64b0c0cbd6c23abcd2cdd6ba815db
README.zh.md: 82bc2eace173d4f56892f514e5a9eebc4f2079d8
README.md: 37c9bc77cde1e2259b4233d4dc37f9c8b1264a96
README.zh.md: e849c8cda6867c711e4fea4f464cfbacac5c4693
+2 -1
View File
@@ -32,10 +32,11 @@ To run a repository checkout instead:
git clone https://github.com/deepseek-harness/deepseek-harness.git
cd deepseek-harness
pnpm install
pnpm run build
pnpm dsh web
```
The last command builds the repository and opens the same Web UI path.
`pnpm run build` prepares the repository artifacts. `pnpm dsh web` starts the Web UI without rebuilding and opens the same path.
## Profiles and plugins
+2 -1
View File
@@ -32,10 +32,11 @@ npx @deepseek-ai/dsh web
git clone https://github.com/deepseek-harness/deepseek-harness.git
cd deepseek-harness
pnpm install
pnpm run build
pnpm dsh web
```
最后一条命令会构建仓库,并进入相同的 Web UI 路径。
`pnpm run build` 准备仓库产物。`pnpm dsh web` 启动 Web UI,不会重新构建,并进入相同的路径。
## Profile 与插件
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent 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: 98a856261bc632c97f350db8fc7bb0b10c22235d
README.zh.md: 283e54138e24202ed6b88d1d309538c58cd66b3e
README.md: 4fbd0692a2df403c6395235e096e193c994ea198
README.zh.md: 7861f5b4f8cb447aff01ec8b64b12fd298485b4b
+1 -1
View File
@@ -35,4 +35,4 @@ The [CLI behavior reference](reference/README.md) owns exact layer precedence, f
## Development
Production runs require built package and frontend artifacts. From the repository root, `pnpm dsh <args...>` builds those artifacts, runs the TypeScript entry, and forwards every argument; the [source-execution reference](reference/README.md#source-execution) owns the module-resolution contract.
Production runs require built package and frontend artifacts. From the repository root, run `pnpm run build` separately, then use `pnpm dsh <args...>` to run the TypeScript entry and forward every argument; the [source-execution reference](reference/README.md#source-execution) owns the module-resolution contract.
+1 -1
View File
@@ -35,4 +35,4 @@ profile 目录包含一个 `package.json`(树外插件依赖,加上 profile
## 开发
生产运行需要已构建的包与前端产物。仓库根目录运行 `pnpm dsh <args...>` 会先构建这些产物,再运行 TypeScript 入口并转发所有参数;模块解析约定由[源码执行参考](reference/README.md#source-execution)负责。
生产运行需要已构建的包与前端产物。请在仓库根目录单独运行 `pnpm run build`,然后使用 `pnpm dsh <args...>` 运行 TypeScript 入口并转发所有参数;模块解析约定由[源码执行参考](reference/README.md#source-execution)负责。
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent 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: 46ea3c241d6775ce90a89c7be58901375a0634a3
README.zh.md: f020f46260d6b04b87a4918a671ca6bcbed251d9
README.md: e8bc99f7b2b15f0679ec268a8e778e815758c00f
README.zh.md: c75c487caa7e2ffd7ca5c4b16f3c6243f0a86f3e
+1 -1
View File
@@ -81,4 +81,4 @@ Install external plugin bundles through `dsh plugin --profile <name> add <packag
## Source execution
From the repository root, use `pnpm dsh <args...>`. The `package.json` script runs the complete repository build, launches `apps/cli/src/bin.ts` with `node --import tsx/esm`, and forwards every argument. Build output appears before CLI output. The process inherits the launch environment; set `NODE_USE_ENV_PROXY=1` when a supporting Node version must honor `HTTP_PROXY` and `HTTPS_PROXY`. The installed form launches the built `apps/cli/lib/bin.js` without rebuilding the repository.
From the repository root, run `pnpm run build` separately after a fresh checkout and whenever artifacts need updating, then use `pnpm dsh <args...>`. The `package.json` script launches `apps/cli/src/bin.ts` with `node --import tsx/esm` without building and forwards every argument. Missing TypeRT host artifacts fail profile boot through module-resolution errors without a build instruction. Once those host artifacts exist, missing frontend or client-plugin bundles fail at startup with an instruction to run `pnpm run build`. The launcher does not check freshness, so existing stale bundles can run older browser code until rebuilt. The process inherits the launch environment; set `NODE_USE_ENV_PROXY=1` when a supporting Node version must honor `HTTP_PROXY` and `HTTPS_PROXY`. The installed form launches the built `apps/cli/lib/bin.js` without rebuilding the repository.
+1 -1
View File
@@ -81,4 +81,4 @@ dsh web --help
## 源码执行
仓库根目录使用 `pnpm dsh <args...>``package.json` 中的脚本会完成整个仓库的构建,通过 `node --import tsx/esm` 启动 `apps/cli/src/bin.ts`,并转发所有参数。构建输出会显示在 CLI 输出之前。该进程会继承启动环境;当支持环境代理的 Node 版本必须遵循 `HTTP_PROXY``HTTPS_PROXY` 时,请设置 `NODE_USE_ENV_PROXY=1`。安装形式会直接启动构建后的 `apps/cli/lib/bin.js`,不会重新构建仓库。
仓库根目录中,于全新 checkout 之后及产物需要更新时单独运行 `pnpm run build`,然后使用 `pnpm dsh <args...>``package.json` 中的脚本会构建,而是通过 `node --import tsx/esm` 启动 `apps/cli/src/bin.ts`,并转发所有参数。TypeRT Host 产物缺失时,profile 启动会因不含构建指引的模块解析错误而失败。这些 Host 产物存在后,如果前端或 Client plugin 组合包缺失,启动会失败并提示运行 `pnpm run build`。启动器不会检查产物是否为最新,因此已有的陈旧组合包可能继续运行旧版浏览器代码,直至重新构建。该进程会继承启动环境;当支持环境代理的 Node 版本必须遵循 `HTTP_PROXY``HTTPS_PROXY` 时,请设置 `NODE_USE_ENV_PROXY=1`。安装形式会直接启动构建后的 `apps/cli/lib/bin.js`,不会重新构建仓库。
+3 -3
View File
@@ -6,7 +6,7 @@ import { describe, expect, it } from 'vitest'
/**
* Keyless smoke for SOURCE `dsh` execution: run `apps/cli/src/bin.ts`
* with the exact production runtime vector (`node --import tsx/esm`, the
* vector the root `dsh` script invokes after building) and assert the
* vector the root `dsh` script invokes directly) and assert the
* required-config diagnostic. The Node compatibility matrix runs this
* WHOLE file, so a Node release changing module hooks or TypeScript handling
* breaks this gate instead of every developer's `pnpm dsh`; the built-bin
@@ -17,11 +17,11 @@ const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
const dshSourceBin = 'apps/cli/src/bin.ts'
describe('dsh SOURCE launcher (node --import tsx/esm)', () => {
it('builds before launching the source CLI', async () => {
it('launches the source CLI without building', async () => {
const rootPackage = JSON.parse(await readFile(new URL('../../../package.json', import.meta.url), 'utf8')) as {
readonly scripts?: Record<string, string>
}
expect(rootPackage.scripts?.dsh).toBe('pnpm run build && node --import tsx/esm apps/cli/src/bin.ts')
expect(rootPackage.scripts?.dsh).toBe('node --import tsx/esm apps/cli/src/bin.ts')
})
it('boots the source entry and requires a profile', async () => {
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent 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/api-gateway.md
api-gateway.md: c60793532d621585fc9878b6197497b45015856b
api-gateway.zh.md: daf46f6ad208f4cb959028ff59d98d9de7a072d6
api-gateway.md: e2878dce4611af562ebc4f30cbb2c5cb529dd61e
api-gateway.zh.md: 86e63c6e9a4166a38397a445424704dc9e61821f
+1 -1
View File
@@ -138,7 +138,7 @@ SRC solves only dispatch for a Host process running from source. The Client does
## Development mode
The repository `dsh` script completes the Host, Client, and Web build before starting the source Host. Web development runs that command and the Client plugin watcher in separate terminals:
Web development prepares current Host, Client, and Web artifacts with `pnpm run build`, then runs the source Host and the Client plugin watcher in separate terminals:
```sh
pnpm dsh web
+1 -1
View File
@@ -138,7 +138,7 @@ SRC 只解决 Host 源码进程的分发问题。Client 不会从运行中的 Ho
## 开发模式
仓库的 `dsh` 脚本会先完成 Host、Client 与 Web 构建,再启动源码 Host。Web 开发需要在两个终端中分别运行该命令和 Client plugin watcher
Web 开发先使用 `pnpm run build` 准备当前 Host、Client 与 Web 产物,然后在两个终端中分别运行源码 Host 和 Client plugin watcher
```sh
pnpm dsh web
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent 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: 620609b48a1e49a079b42d58e39899f5e2bbeb2c
config-catalog.zh.md: 11d7741a2e2c55f89a7f569cb410ac4699511aba
config-catalog.md: 415f4a305caf52121aa3ae9396bfce5442782f71
config-catalog.zh.md: dfe6b19dbdf2fdf09884e420a1f38eebfa0328c5
+25 -6
View File
@@ -69,6 +69,8 @@ export interface Config {
skills?: agentCore.SkillConfig
/** Model-facing bash tool config forwarded through agent-core. */
toolBash?: NonNullable<agentCore.Config['toolBash']>
/** Process-local background-task admission config forwarded through agent-core. */
tasks?: NonNullable<agentCore.Config['tasks']>
/** Generic background-task controls forwarded through agent-core; set false to omit their tools. */
toolTasks?: NonNullable<agentCore.Config['toolTasks']>
/** Persisted same-session goals; owner defaults enable them, or false disables the stack and tools. */
@@ -172,9 +174,10 @@ Source: [`packages/preset/agent-presets/src/preset.ts:52`](../packages/preset/ag
* `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
* workspace-context loader, and `toolBash`/`toolTasks` to the model-facing tool
* plugins this bundle owns. Provider adapters own their `retryPolicy`; this
* bundle always mounts its executor.
* workspace-context loader, `tasks` to the process-local task provider, and
* `toolBash`/`toolTasks` to the model-facing tool plugins this bundle owns.
* Provider adapters own their `retryPolicy`; this bundle always mounts its
* executor.
* `goals` opts into and configures the persisted goal domain plus its model tool
* and same-session driver; `invariants` configures global and package-filtered
* relational checks. Owner schemas supply defaults for optional input;
@@ -211,6 +214,8 @@ export interface Config {
skills?: SkillConfig
/** Model-facing bash tool config, or false when another plugin owns `bash`. */
toolBash?: toolBash.Config | false
/** Process-local background-task admission config. */
tasks?: TasksConfig
/** Generic background-task controls; set false to keep the task service without model-facing task tools. */
toolTasks?: toolTasks.Config | false
/** Global enablement and package-name filters for invariant companions. */
@@ -240,9 +245,9 @@ export interface GoalConfig {
}
```
Depends on: [`AgentLoopConfig`](#deepseek-aidsh-agent-loop) · [`GoalDomainConfig`](#deepseek-aidsh-goal) · [`InvariantConfig`](#deepseek-aidsh-invariants) · [`SessionTitleConfig`](#deepseek-aidsh-session-title) · [`SkillLocal`](../packages/skill/skill-local/src/index.ts) · [`SkillRegistryConfig`](#deepseek-aidsh-skill) · [`SystemPromptConfig`](#deepseek-aidsh-system-prompt) · [`toolBash`](../packages/bash/tool-bash/src/index.ts) · [`toolGoal`](../packages/goal/tool-goal/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools) · [`toolSkill`](../packages/skill/tool-skill/src/index.ts) · [`toolTasks`](../packages/tasks/tool-tasks/src/index.ts) · [`workspaceContext`](../packages/context/workspace-context/src/index.ts)
Depends on: [`AgentLoopConfig`](#deepseek-aidsh-agent-loop) · [`GoalDomainConfig`](#deepseek-aidsh-goal) · [`InvariantConfig`](#deepseek-aidsh-invariants) · [`SessionTitleConfig`](#deepseek-aidsh-session-title) · [`SkillLocal`](../packages/skill/skill-local/src/index.ts) · [`SkillRegistryConfig`](#deepseek-aidsh-skill) · [`SystemPromptConfig`](#deepseek-aidsh-system-prompt) · [`TasksConfig`](#deepseek-aidsh-tasks-local) · [`toolBash`](../packages/bash/tool-bash/src/index.ts) · [`toolGoal`](../packages/goal/tool-goal/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools) · [`toolSkill`](../packages/skill/tool-skill/src/index.ts) · [`toolTasks`](../packages/tasks/tool-tasks/src/index.ts) · [`workspaceContext`](../packages/context/workspace-context/src/index.ts)
Source: [`packages/examples/agent-spine-demo/src/index.ts:90`](../packages/examples/agent-spine-demo/src/index.ts)
Source: [`packages/examples/agent-spine-demo/src/index.ts:91`](../packages/examples/agent-spine-demo/src/index.ts)
## `@deepseek-ai/dsh-agent-tool-mode`
@@ -2034,6 +2039,21 @@ export interface Config {
Source: [`packages/core/system-prompt/src/index.ts:186`](../packages/core/system-prompt/src/index.ts)
## `@deepseek-ai/dsh-tasks-local`
```ts config-catalog
/** Configuration for the process-local task registry. */
export interface Config {
/**
* Maximum `running` plus `stopping` tasks per exact owner or in the shared unowned bucket;
* omission defaults to 10.
*/
maxConcurrentTasksPerOwner?: number
}
```
Source: [`packages/tasks/tasks-local/src/index.ts:31`](../packages/tasks/tasks-local/src/index.ts)
## `@deepseek-ai/dsh-time-context`
Requires: `agents`
@@ -2804,7 +2824,6 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
- `@deepseek-ai/dsh-storage` ([`packages/storage/storage/src/index.ts`](../packages/storage/storage/src/index.ts))
- `@deepseek-ai/dsh-subagent` ([`packages/subagent/subagent/src/index.ts`](../packages/subagent/subagent/src/index.ts))
- `@deepseek-ai/dsh-subprocess-local` ([`packages/subprocess/subprocess-local/src/index.ts`](../packages/subprocess/subprocess-local/src/index.ts))
- `@deepseek-ai/dsh-tasks-local` ([`packages/tasks/tasks-local/src/index.ts`](../packages/tasks/tasks-local/src/index.ts))
- `@deepseek-ai/dsh-timeout-policy` — requires `tools` ([`packages/guard/timeout-policy/src/index.ts`](../packages/guard/timeout-policy/src/index.ts))
- `@deepseek-ai/dsh-tool-ask-user` — requires `tools` · `userInteraction` ([`packages/interaction/tool-ask-user/src/index.ts`](../packages/interaction/tool-ask-user/src/index.ts))
- `@deepseek-ai/dsh-tool-schedule` — requires `agents` · `sessions` · `tools` · `sessionPersistence` ([`packages/schedule/tool-schedule/src/index.ts`](../packages/schedule/tool-schedule/src/index.ts))
+25 -6
View File
@@ -71,6 +71,8 @@ export interface Config {
skills?: agentCore.SkillConfig
/** Model-facing bash tool config forwarded through agent-core. */
toolBash?: NonNullable<agentCore.Config['toolBash']>
/** Process-local background-task admission config forwarded through agent-core. */
tasks?: NonNullable<agentCore.Config['tasks']>
/** Generic background-task controls forwarded through agent-core; set false to omit their tools. */
toolTasks?: NonNullable<agentCore.Config['toolTasks']>
/** Persisted same-session goals; owner defaults enable them, or false disables the stack and tools. */
@@ -174,9 +176,10 @@ export type PresetTrust = 'system' | 'user'
* `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
* workspace-context loader, and `toolBash`/`toolTasks` to the model-facing tool
* plugins this bundle owns. Provider adapters own their `retryPolicy`; this
* bundle always mounts its executor.
* workspace-context loader, `tasks` to the process-local task provider, and
* `toolBash`/`toolTasks` to the model-facing tool plugins this bundle owns.
* Provider adapters own their `retryPolicy`; this bundle always mounts its
* executor.
* `goals` opts into and configures the persisted goal domain plus its model tool
* and same-session driver; `invariants` configures global and package-filtered
* relational checks. Owner schemas supply defaults for optional input;
@@ -213,6 +216,8 @@ export interface Config {
skills?: SkillConfig
/** Model-facing bash tool config, or false when another plugin owns `bash`. */
toolBash?: toolBash.Config | false
/** Process-local background-task admission config. */
tasks?: TasksConfig
/** Generic background-task controls; set false to keep the task service without model-facing task tools. */
toolTasks?: toolTasks.Config | false
/** Global enablement and package-name filters for invariant companions. */
@@ -242,9 +247,9 @@ export interface GoalConfig {
}
```
依赖:[`AgentLoopConfig`](#deepseek-aidsh-agent-loop) · [`GoalDomainConfig`](#deepseek-aidsh-goal) · [`InvariantConfig`](#deepseek-aidsh-invariants) · [`SessionTitleConfig`](#deepseek-aidsh-session-title) · [`SkillLocal`](../packages/skill/skill-local/src/index.ts) · [`SkillRegistryConfig`](#deepseek-aidsh-skill) · [`SystemPromptConfig`](#deepseek-aidsh-system-prompt) · [`toolBash`](../packages/bash/tool-bash/src/index.ts) · [`toolGoal`](../packages/goal/tool-goal/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools) · [`toolSkill`](../packages/skill/tool-skill/src/index.ts) · [`toolTasks`](../packages/tasks/tool-tasks/src/index.ts) · [`workspaceContext`](../packages/context/workspace-context/src/index.ts)
依赖:[`AgentLoopConfig`](#deepseek-aidsh-agent-loop) · [`GoalDomainConfig`](#deepseek-aidsh-goal) · [`InvariantConfig`](#deepseek-aidsh-invariants) · [`SessionTitleConfig`](#deepseek-aidsh-session-title) · [`SkillLocal`](../packages/skill/skill-local/src/index.ts) · [`SkillRegistryConfig`](#deepseek-aidsh-skill) · [`SystemPromptConfig`](#deepseek-aidsh-system-prompt) · [`TasksConfig`](#deepseek-aidsh-tasks-local) · [`toolBash`](../packages/bash/tool-bash/src/index.ts) · [`toolGoal`](../packages/goal/tool-goal/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools) · [`toolSkill`](../packages/skill/tool-skill/src/index.ts) · [`toolTasks`](../packages/tasks/tool-tasks/src/index.ts) · [`workspaceContext`](../packages/context/workspace-context/src/index.ts)
来源:[`packages/examples/agent-spine-demo/src/index.ts:90`](../packages/examples/agent-spine-demo/src/index.ts)
来源:[`packages/examples/agent-spine-demo/src/index.ts:91`](../packages/examples/agent-spine-demo/src/index.ts)
## `@deepseek-ai/dsh-agent-tool-mode`
@@ -2036,6 +2041,21 @@ export interface Config {
来源:[`packages/core/system-prompt/src/index.ts:186`](../packages/core/system-prompt/src/index.ts)
## `@deepseek-ai/dsh-tasks-local`
```ts config-catalog
/** Configuration for the process-local task registry. */
export interface Config {
/**
* Maximum `running` plus `stopping` tasks per exact owner or in the shared unowned bucket;
* omission defaults to 10.
*/
maxConcurrentTasksPerOwner?: number
}
```
来源:[`packages/tasks/tasks-local/src/index.ts:31`](../packages/tasks/tasks-local/src/index.ts)
## `@deepseek-ai/dsh-time-context`
需要:`agents`
@@ -2805,7 +2825,6 @@ export interface Config {
- `@deepseek-ai/dsh-storage`[`packages/storage/storage/src/index.ts`](../packages/storage/storage/src/index.ts)
- `@deepseek-ai/dsh-subagent`[`packages/subagent/subagent/src/index.ts`](../packages/subagent/subagent/src/index.ts)
- `@deepseek-ai/dsh-subprocess-local`[`packages/subprocess/subprocess-local/src/index.ts`](../packages/subprocess/subprocess-local/src/index.ts)
- `@deepseek-ai/dsh-tasks-local`[`packages/tasks/tasks-local/src/index.ts`](../packages/tasks/tasks-local/src/index.ts)
- `@deepseek-ai/dsh-timeout-policy` — 需要 `tools`[`packages/guard/timeout-policy/src/index.ts`](../packages/guard/timeout-policy/src/index.ts)
- `@deepseek-ai/dsh-tool-ask-user` — 需要 `tools` · `userInteraction`[`packages/interaction/tool-ask-user/src/index.ts`](../packages/interaction/tool-ask-user/src/index.ts)
- `@deepseek-ai/dsh-tool-schedule` — 需要 `agents` · `sessions` · `tools` · `sessionPersistence`[`packages/schedule/tool-schedule/src/index.ts`](../packages/schedule/tool-schedule/src/index.ts)
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/development.md
development.md: d5bd18f83f6f2f8fd3d507ec133eea4d4e06abef
development.zh.md: 0725cfaad74ec2aa3e52f6523a1dbdbdd9da8a39
development.md: 8d79756c50f82a840d412f52ee980cb0b505f6f1
development.zh.md: 285bc0309212694d11077fa590edd199b895fe10
+6
View File
@@ -126,6 +126,12 @@ The root [contributor instructions](../AGENTS.md#commands) summarize common comm
### Demos
Run the repository build separately before using these source-checkout demos:
```sh
pnpm run build
```
The one-shot Headless coding agent needs `DEEPSEEK_API_KEY` in the environment or repo-root `.env`:
```sh
+6
View File
@@ -126,6 +126,12 @@ keyless [CI 工作流](../.github/workflows/ci.yml) 将独立门禁分组到若
### 演示
从源码 checkout 运行这些演示前,请单独执行仓库构建:
```sh
pnpm run build
```
单次运行的 Headless coding agent 需要环境变量或仓库根目录 `.env` 中的 `DEEPSEEK_API_KEY`
```sh
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/i18n/README.md
README.md: 23400801426f77dae5136406cd747dbe4b06a4c5
README.zh.md: fe3cc7b5a5403fc9cf0c9ce536178d4fa7581e3c
README.md: 3acddd310a423b6a19014063418d81350562b188
README.zh.md: abc601f91bb3778c887f1249426481ef0920b73a
+1 -1
View File
@@ -18,7 +18,7 @@ This repo's documentation is read by people and agents both inside and outside t
Blob hashes, not commit hashes, so the record is computable for files edited in the same PR (`git hash-object foo.md`) and consistency is a pure content comparison. `--write` stores those snapshots in the local Git object database before recording them, including uncommitted working-tree contents, and pins every distinct stored blob under a content-addressed `refs/dsh/translation-pairing/snapshots/` ref so garbage collection cannot invalidate a recorded recovery pointer. The recorded hashes therefore recover the exact last-confirmed text of either side, so an out-of-sync pair is updated by patching the counterpart minimally against the edited side's diff — never by re-translating whole files. Routine work makes that patch directly; when the user explicitly invokes the extended workflow, `pnpm run gen-translation-brief <pair>` can instead assemble the update at the narrowest safely aligned granularity and `--apply` can splice a code-fence-only change after structural validation ([briefed-updates Agent Note](../../.agents/notes/implemented/process/2026-07-26-briefed-minimal-translation-updates.md)). After bringing the pair back in line, `pnpm run verify-translation-pairing --write <pair>` re-records both hashes; that yaml diff is the reviewable act of confirming consistency, which is why `--write` requires naming the pairs you confirmed (`--write --all` is the explicit corpus-wide form).
When two branches contain valid confirmations of the same pair, the installed `dsh-translation-pairing` Git merge driver composes a new record only if Git's default text merge succeeds for both recorded owner-blob triplets and the merged pair retains its required switchers and structural signature. The Chinese file must retain its English backlink; an authored English source must retain its Chinese link, while a listed generated English source is exempt. Any structure the driver cannot verify remains an ordinary conflict; `pnpm run resolve-translation-pairing-conflicts` applies the same fail-closed operation to a merge that has already stopped, stages every safe pairing record, and exits unsuccessfully when other pairing conflicts remain. The [automatic pairing merges Agent Note](../../.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md) owns the mechanism and alternatives.
- **Language switcher.** The Chinese file always links back immediately after its H1 heading with `[English](foo.md) | 中文`. An authored English file reciprocates there with `English | [中文](foo.zh.md)`; a listed generated English source omits that line so it remains byte-identical to generator output.
- **Language switcher.** The Chinese file always links back immediately after its H1 heading with `[English](foo.md) | 中文`. An authored English file reciprocates there with `English | [中文](foo.zh.md)`; a listed generated English source omits that line so it remains byte-identical to generator output. A README published outside GitHub, such as PyPI project metadata, may use the canonical `https://github.com/deepseek-ai/deepseek-harness/blob/master/<repository-path>` URL to the same counterpart so the switcher still resolves there.
- **Structure mirrors the counterpart.** Heading depths and order, list kinds, ordered-list starts, list item counts, table row and column counts, link targets, and verbatim code blocks match one to one across the pair — see [translation-rules.md](translation-rules.md) for the full preservation rules. Existing Markdown gates apply to `.zh.md` files unchanged (`verify-md-wrap`, `verify-md-links`).
## The gate: verify-translation-pairing
+1 -1
View File
@@ -18,7 +18,7 @@
用 blob hash 而不是 commit hash,这样同一个 PR 里改动的文件也能算出记录(`git hash-object foo.md`),一致性是纯内容比较。`--write` 会先把这些快照存入本地 Git 对象库再写下记录,未提交的工作树内容也不例外;它还会在内容寻址的 `refs/dsh/translation-pairing/snapshots/` ref 下固定每个不同的已存 blob,使垃圾回收无法让已记录的恢复指针失效。因此记录的 hash 能还原任一侧上次确认时的确切文本,所以失去同步的配对是「按被改一侧的 diff 最小化地修补另一侧」,从不整篇重译。日常工作会直接完成这份修补;用户显式调用扩展工作流时,可改由 `pnpm run gen-translation-brief <pair>` 以能安全对齐的最窄粒度汇集这次更新,并由 `--apply` 在结构校验后拼接仅涉及围栏代码块的改动([briefed-updates Agent Note](../../.agents/notes/implemented/process/2026-07-26-briefed-minimal-translation-updates.md))。两侧对齐后,`pnpm run verify-translation-pairing --write <pair>` 重新记录两个 hash;那份 yaml diff 就是「确认一致」这个动作本身,可以被评审,也正因如此,`--write` 要求点名你确认过的配对(`--write --all` 是显式的全语料形式)。
当两个分支都包含同一配对的有效确认时,已安装的 `dsh-translation-pairing` Git 合并驱动只会在 Git 默认文本合并能分别干净合并记录所指向的英文三方 blob 与中文三方 blob,且合并后的配对仍保留必需的语言切换行和结构签名时,组合出一份新记录。中文文件必须保留指向英文的反向链接;普通撰写的英文源必须保留指向中文的链接,而清单内的生成英文源不作此要求。任何合并驱动无法验证的结构都保留为普通冲突;`pnpm run resolve-translation-pairing-conflicts` 会对已经停止的合并执行同一套遇错即保留冲突的操作,暂存每份可安全生成的配对记录,并在还有其他配对冲突时以非零状态退出。[自动配对合并 Agent Note](../../.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md) 负责记录该机制与备选方案。
- **语言切换行。** 中文文件一律在 H1 标题后立即以 `[English](foo.md) | 中文` 链回英文。普通撰写的英文文件在同一位置以 `English | [中文](foo.zh.md)` 互链;清单内的生成英文源省略此行,以便与生成器输出逐字节一致。
- **语言切换行。** 中文文件一律在 H1 标题后立即以 `[English](foo.md) | 中文` 链回英文。普通撰写的英文文件在同一位置以 `English | [中文](foo.zh.md)` 互链;清单内的生成英文源省略此行,以便与生成器输出逐字节一致。发布到 GitHub 以外位置的 README(例如 PyPI 项目元数据)可以改用指向同一对侧文件的规范 `https://github.com/deepseek-ai/deepseek-harness/blob/master/<repository-path>` URL,使切换行在该位置仍可访问。
- **结构与另一侧一一对应。** 标题深度与顺序、列表类型、有序列表起始编号、列表项数量、表格行列数、链接目标与逐字节一致的代码块在配对两侧一一对应;完整保持规则见 [translation-rules.md](translation-rules.md)。既有 Markdown 门禁对 `.zh.md` 文件原样生效(`verify-md-wrap`、`verify-md-links`)。
## 门禁:verify-translation-pairing
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/i18n/translation-rules.md
translation-rules.md: ce20ed9a9673b0782ef07c9a4a21ff1c98ace960
translation-rules.zh.md: daea57ab1d3a1abbad442982c8bb1c189478b8a8
translation-rules.md: 79ec3de50ecbb57bc84cac39a0edc9b5ac26c5d2
translation-rules.zh.md: 5861c074e8e6cd79e3fd2799cf4152c6bac93182
+1 -1
View File
@@ -28,7 +28,7 @@ The pairing gate checks heading depths, fenced code blocks, table row and column
- tables (same columns, same row order; header cells translated per terminology),
- fenced code blocks — **byte-identical, including comments**; the pairing signature compares their info strings and contents, and ` ```ts ` blocks compile under `doc-typecheck`,
- inline code spans (commands, flags, config keys, file paths, event names, API names, version numbers) — verbatim, never translated or reformatted,
- links and anchors: every relative link MUST point at the same target in both files — by convention the `.md` path, not the `.zh.md` sibling — so links never dangle when one pair lands before its neighbors. The ONLY zh-specific link is the language switcher. Link TEXT is translated; the target is not.
- links and anchors: every relative link MUST point at the same target in both files — by convention the `.md` path, not the `.zh.md` sibling — so links never dangle when one pair lands before its neighbors. The ONLY zh-specific link is the language switcher. A README rendered outside GitHub MAY use the canonical public repository URL to its exact counterpart as documented in [README.md](README.md). Link TEXT is translated; the target is not.
The repo's Markdown conventions apply to `.zh.md` files unchanged: one physical line per paragraph (`verify-md-wrap`), resolving relative links (`verify-md-links`), exactly one trailing newline.
+1 -1
View File
@@ -28,7 +28,7 @@
- 表格(相同的列、相同的行序;表头单元格按术语表翻译);
- 围栏代码块:**逐字节一致,包括注释**。配对签名比对信息字符串与内容,` ```ts ` 块还要通过 `doc-typecheck` 编译;
- 行内代码(命令、flag、配置键、文件路径、事件名、API 名、版本号):原样保留,从不翻译或重排;
- 链接与锚点:每个相对链接在两个文件中必须指向相同的目标(按约定是 `.md` 路径而非 `.zh.md` 兄弟文件),这样即使某对文档先于相邻文件落地,链接也不会悬空。唯一的 zh 特有链接是语言切换行。链接**文字**翻译;链接目标不翻。
- 链接与锚点:每个相对链接在两个文件中必须指向相同的目标(按约定是 `.md` 路径而非 `.zh.md` 兄弟文件),这样即使某对文档先于相邻文件落地,链接也不会悬空。唯一的 zh 特有链接是语言切换行。在 GitHub 以外位置渲染的 README 可以按 [README.md](README.md) 的规定,使用指向确切对侧文件的规范公开仓库 URL。链接**文字**翻译;链接目标不翻。
本仓库的 Markdown 约定对 `.zh.md` 文件原样生效:一个段落一个物理行(`verify-md-wrap`)、相对链接必须可解析(`verify-md-links`)、文件末尾恰好一个换行。
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/tasks.md
tasks.md: 37807bc446f607c3e1635a49432670e86760f2a4
tasks.zh.md: 39014acb13f6051431a40ae084c1e03298ae8993
tasks.md: 8142e7d21a3db717745b841b95a0227c20ad5137
tasks.zh.md: a5724c1eb2f4d06259c6d8e7a89da78c61889098
+6 -5
View File
@@ -154,7 +154,7 @@ interface TaskRead {
## Service behavior
The abstract [`TaskService`](../../packages/tasks/tasks/src/index.ts) Service Definition specifies atomic `start`, caller-scoped `get` and `list`, `read`, `kill`, bounded `wait`, failure-isolated `onTaskDone` and `onTasksChanged` listeners, and when `attachController` becomes available; [`LocalTaskService`](../../packages/tasks/tasks-local/src/index.ts) is the process-local Service provider. Authorization compares owner sessions; owner cleanup selects the exact registered `Agent` instance. See [`dsh-tasks`](../../packages/tasks/tasks/README.md) for the Service Definition contract, [`dsh-tasks-local`](../../packages/tasks/tasks-local/README.md) for the registry lifecycle, and [`dsh-tool-tasks`](../../packages/tasks/tool-tasks/README.md) for the model-facing Consumer.
The abstract [`TaskService`](../../packages/tasks/tasks/src/index.ts) Service Definition specifies atomic `start`, caller-scoped `get` and `list`, `read`, `kill`, bounded `wait`, failure-isolated `onTaskDone` and `onTasksChanged` listeners, and when `attachController` becomes available; [`LocalTaskService`](../../packages/tasks/tasks-local/src/index.ts) is the process-local Service provider. Authorization compares owner sessions; owner cleanup and admission use the exact registered `Agent` instance. The local provider's positive-safe-integer `maxConcurrentTasksPerOwner` config defaults to `10` and counts `running` plus `stopping` records per exact owner, with one shared bucket for unowned tasks; terminal producer settlement releases capacity. See [`dsh-tasks`](../../packages/tasks/tasks/README.md) for the Service Definition contract, [`dsh-tasks-local`](../../packages/tasks/tasks-local/README.md) for the registry lifecycle and admission policy, and [`dsh-tool-tasks`](../../packages/tasks/tool-tasks/README.md) for the model-facing Consumer.
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
@@ -179,10 +179,11 @@ Implementations must honor these semantics:
```ts cordis-catalog
/**
* Preflight access, validation, and owner cleanup before starting and
* atomically registering work. A throwing starter leaves nothing registered;
* after it returns, registration cannot fail. Settlement records the outcome,
* notifies listeners, and releases waiters.
* Preflight access, validation, owner cleanup, and implementation-owned
* admission before starting and atomically registering work. Any preflight
* rejection leaves no task id or execution resource. A throwing starter
* leaves nothing registered; after it returns, registration cannot fail.
* Settlement records the outcome, notifies listeners, and releases waiters.
* @param spec - task identity, owner, and synchronous starter.
* @returns the registry-issued `<kind>-N` id.
*/
+6 -5
View File
@@ -154,7 +154,7 @@ interface TaskRead {
## 服务行为
抽象的 [`TaskService`](../../packages/tasks/tasks/src/index.ts) Service Definition 规定原子 `start`、限定调用方作用域的 `get` 和 `list`、`read`、`kill`、有界 `wait`、故障隔离的 `onTaskDone` 与 `onTasksChanged` 监听器,以及 `attachController` 何时可用;[`LocalTaskService`](../../packages/tasks/tasks-local/src/index.ts) 是其进程局部 Service provider。授权会比较拥有者会话;拥有者清理会选择确切的已注册 `Agent` 实例。Service Definition 约定见 [`dsh-tasks`](../../packages/tasks/tasks/README.md),注册表生命周期见 [`dsh-tasks-local`](../../packages/tasks/tasks-local/README.md),面向模型的 Consumer 见 [`dsh-tool-tasks`](../../packages/tasks/tool-tasks/README.md)。
抽象的 [`TaskService`](../../packages/tasks/tasks/src/index.ts) Service Definition 规定原子 `start`、限定调用方作用域的 `get` 和 `list`、`read`、`kill`、有界 `wait`、故障隔离的 `onTaskDone` 与 `onTasksChanged` 监听器,以及 `attachController` 何时可用;[`LocalTaskService`](../../packages/tasks/tasks-local/src/index.ts) 是其进程局部 Service provider。授权会比较拥有者会话;拥有者清理与准入会使用确切的已注册 `Agent` 实例。本地 Service provider 的 `maxConcurrentTasksPerOwner` 配置必须是正的安全整数,默认值为 `10`;它按确切 owner 统计 `running` 与 `stopping` 记录,所有无 owner 任务共享一个服务级桶,并在生产方终止结算后释放容量。Service Definition 约定见 [`dsh-tasks`](../../packages/tasks/tasks/README.md),注册表生命周期与准入策略见 [`dsh-tasks-local`](../../packages/tasks/tasks-local/README.md),面向模型的 Consumer 见 [`dsh-tool-tasks`](../../packages/tasks/tool-tasks/README.md)。
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
@@ -179,10 +179,11 @@ Implementations must honor these semantics:
```ts cordis-catalog
/**
* Preflight access, validation, and owner cleanup before starting and
* atomically registering work. A throwing starter leaves nothing registered;
* after it returns, registration cannot fail. Settlement records the outcome,
* notifies listeners, and releases waiters.
* Preflight access, validation, owner cleanup, and implementation-owned
* admission before starting and atomically registering work. Any preflight
* rejection leaves no task id or execution resource. A throwing starter
* leaves nothing registered; after it returns, registration cannot fail.
* Settlement records the outcome, notifies listeners, and releases waiters.
* @param spec - task identity, owner, and synchronous starter.
* @returns the registry-issued `<kind>-N` id.
*/
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent 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: 3ef0e6595b0b5b7dddfe05e659c58556dcc48874
python-sdk.zh.md: a46c79aa0c7cd3b6a286e1f64e01a8a81496c0f0
python-sdk.md: 5e1c31bf006fe22bf0c79bb3f80e96fe6ba3072f
python-sdk.zh.md: ee2a7306ec4b322a77647ab97ffe4e24aba5fc80
+1 -1
View File
@@ -8,7 +8,7 @@ This tutorial is the programmatic alternative to the Web UI. It installs the pub
- Python 3.10 or newer
- Git
- Linux x64, Linux arm64, or macOS arm64
- 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
+1 -1
View File
@@ -8,7 +8,7 @@
- Python 3.10 或更高版本
- Git
- Linux x64、Linux arm64 或 macOS arm64
- Linux x64、Linux arm64 或 macOS 14 或更高版本的 arm64
- DeepSeek 兼容的 API 端点与凭据
- agent 可以修改的隔离 workspace
@@ -0,0 +1,36 @@
# Keyless counterpart to background-task-admission.cordis.yml: replace the
# DeepSeek adapter with replay while preserving the app's one-task admission
# config and the recorded flash route.
- id: base
name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
- id: llm-deepseek
name: '@deepseek-ai/dsh-llm-deepseek'
disabled: true
- id: acp-agent
name: '@deepseek-ai/dsh-acp-demo'
config:
provider: deepseek-official
model: deepseek-v4-flash
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
persistenceCompression: none
workspaceContext:
maxBytes: 65536
tasks:
maxConcurrentTasksPerOwner: 1
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
@@ -0,0 +1,24 @@
# Bounded-task admission overlay: keep the ordinary ACP composition while
# configuring its task provider to allow one active task per exact owner. The
# scenario starts a real background Bash process, observes the second producer
# rejection, and cleans up the first task by its returned id.
- id: base
name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
- id: acp-agent
name: '@deepseek-ai/dsh-acp-demo'
config:
provider: deepseek-official
model: deepseek-v4-flash
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
persistenceCompression: !!js "process.env.DSH_SNAPSHOT === undefined ? 'zstd' : 'none'"
workspaceContext:
maxBytes: 65536
tasks:
maxConcurrentTasksPerOwner: 1
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.
+11
View File
@@ -60,6 +60,9 @@ 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))
const PARTIAL_LANDLOCK_CONFIG = fileURLToPath(new URL('../partial-landlock.cordis.yml', import.meta.url))
const PWSH_CONFIG = fileURLToPath(new URL('./pwsh.cordis.yml', import.meta.url))
const BACKGROUND_TASK_ADMISSION_CONFIG = fileURLToPath(
new URL('../background-task-admission.cordis.yml', import.meta.url),
)
const PRODUCT_SUBAGENT_CODEX_CONFIG = fileURLToPath(new URL('../product-subagent-codex.cordis.yml', import.meta.url))
const PRODUCT_SUBAGENT_BOTH_CONFIG = fileURLToPath(new URL('../product-subagent-both.cordis.yml', import.meta.url))
const FS_DIFF_BOUND_CONFIG = fileURLToPath(new URL('./fs-diff-bound.cordis.yml', import.meta.url))
@@ -218,6 +221,14 @@ const SCENARIOS: Scenario[] = [
configPath: PTY_CONFIG,
},
{ name: 'bash-tool-turn', hasModelTurn: true, recorded: true },
{
name: 'background-task-admission',
hasModelTurn: true,
recorded: false,
overridden: true,
configPath: BACKGROUND_TASK_ADMISSION_CONFIG,
posixOnly: true,
},
// The pwsh overlay (pwsh.cordis.yml / pwsh.cordis.snapshot.yml) swaps the
// bundle's bash tool for the PowerShell twin, so its header class pins its
// own prompt/tool sidecars and a recorded transcript.
@@ -0,0 +1,10 @@
{
"steps": [
{ "op": "initialize" },
{ "op": "newSession" },
{
"op": "prompt",
"text": "Start one background Bash task that stays alive. Immediately try to start a second background Bash task, observe the limit error, stop the first task by its returned task id, verify that second-task-ran.txt does not exist, then reply with exactly BOUNDED_BACKGROUND_TASKS and stop."
}
]
}
@@ -0,0 +1,52 @@
[
{
"kind": "chunks",
"chunks": [
{ "type": "block-start", "index": 0, "blockType": "tool-call" },
{ "type": "tool-call-delta", "index": 0, "id": "bounded-task-first", "name": "bash", "argumentsDelta": "{\"command\":\"while :; do sleep 60; done\",\"description\":\"Hold the only background task slot\",\"run_in_background\":true}" },
{ "type": "block-end", "index": 0, "block": { "type": "tool-call", "id": "bounded-task-first", "name": "bash", "arguments": "{\"command\":\"while :; do sleep 60; done\",\"description\":\"Hold the only background task slot\",\"run_in_background\":true}" } },
{ "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": "bounded-task-second", "name": "bash", "argumentsDelta": "{\"command\":\"printf SHOULD_NOT_RUN > second-task-ran.txt; while :; do sleep 60; done\",\"description\":\"Attempt a second background task\",\"run_in_background\":true}" },
{ "type": "block-end", "index": 0, "block": { "type": "tool-call", "id": "bounded-task-second", "name": "bash", "arguments": "{\"command\":\"printf SHOULD_NOT_RUN > second-task-ran.txt; while :; do sleep 60; done\",\"description\":\"Attempt a second background task\",\"run_in_background\":true}" } },
{ "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": "bounded-task-kill", "name": "task_kill", "argumentsDelta": "{\"task_id\":\"bash-1\",\"reason\":\"free the bounded task slot\"}" },
{ "type": "block-end", "index": 0, "block": { "type": "tool-call", "id": "bounded-task-kill", "name": "task_kill", "arguments": "{\"task_id\":\"bash-1\",\"reason\":\"free the bounded task slot\"}" } },
{ "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": "bounded-task-side-effect-check", "name": "bash", "argumentsDelta": "{\"command\":\"test ! -e second-task-ran.txt\",\"description\":\"Verify the rejected producer did not run\"}" },
{ "type": "block-end", "index": 0, "block": { "type": "tool-call", "id": "bounded-task-side-effect-check", "name": "bash", "arguments": "{\"command\":\"test ! -e second-task-ran.txt\",\"description\":\"Verify the rejected producer did not run\"}" } },
{ "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": "BOUNDED_BACKGROUND_TASKS" },
{ "type": "block-end", "index": 0, "block": { "type": "text", "text": "BOUNDED_BACKGROUND_TASKS" } },
{ "type": "usage", "usage": { "inputTokens": 10, "outputTokens": 2 } },
{ "type": "finish", "reason": { "kind": "stop" } }
]
}
]
@@ -0,0 +1,58 @@
{"type":"session","version":0,"id":"77777777-7777-4777-8777-777777777777","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0}
{"type":"agent/inbox/spliced","seq":0,"time":1786434813544,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Start one background Bash task that stays alive. Immediately try to start a second background Bash task, observe the limit error, stop the first task by its returned task id, verify that second-task-ran.txt does not exist, then reply with exactly BOUNDED_BACKGROUND_TASKS and stop."}],"source":{"kind":"user"},"role":"user","id":"fca9abcd-66a9-4c79-ab34-7e25e65e01af"}]}}
{"type":"turn/start","seq":1,"time":1786434813545,"data":{"turn":1}}
{"type":"agent/inbox/spliced","seq":2,"time":1786434813546,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":3,"time":1786434813574,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":4,"time":1786434813574,"data":{"content":[{"type":"text","text":"Start one background Bash task that stays alive. Immediately try to start a second background Bash task, observe the limit error, stop the first task by its returned task id, verify that second-task-ran.txt does not exist, then reply with exactly BOUNDED_BACKGROUND_TASKS and stop."}],"source":{"kind":"user"},"role":"user","id":"fca9abcd-66a9-4c79-ab34-7e25e65e01af"},"surfaceOp":"append"}
{"type":"user/message","seq":5,"time":1786434813575,"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":"f7801581-b729-4cbc-b205-1eabd5b96de7"},"surfaceOp":"append"}
{"type":"session/title","seq":6,"time":1786434813575,"data":{"title":"Start one background Bash task","messageSeqs":[4],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":7,"time":1786434813576,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":8,"time":1786434813576,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
{"type":"assistant/chunk","seq":9,"time":1786434813581,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","seq":10,"time":1786434813581,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"bounded-task-first","name":"bash","argumentsDelta":"{\"command\":\"while :; do sleep 60; done\",\"description\":\"Hold the only background task slot\",\"run_in_background\":true}"}}}
{"type":"assistant/chunk","seq":11,"time":1786434813581,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bounded-task-first","name":"bash","arguments":"{\"command\":\"while :; do sleep 60; done\",\"description\":\"Hold the only background task slot\",\"run_in_background\":true}"}}}}
{"type":"assistant/chunk","seq":12,"time":1786434813581,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":13,"time":1786434813581,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":14,"time":1786434813582,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bounded-task-first","name":"bash","arguments":"{\"command\":\"while :; do sleep 60; done\",\"description\":\"Hold the only background task slot\",\"run_in_background\":true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"f25e0e7c-76a4-45a6-a825-64d1bd42fe59"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"}
{"type":"tool/call","seq":15,"time":1786434813582,"data":{"turn":1,"step":1,"callId":"bounded-task-first","name":"bash","arguments":"{\"command\":\"while :; do sleep 60; done\",\"description\":\"Hold the only background task slot\",\"run_in_background\":true}"}}
{"type":"tool/result","seq":16,"time":1786434813594,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"bounded-task-first"},"content":[{"type":"tool-result","toolCallId":"bounded-task-first","content":[{"type":"text","text":"started background task bash-1"}],"isError":false}],"role":"user","id":"0e19086f-2a9a-4e78-b5eb-5a117cad9416"}},"sourceEventSeqs":[15],"surfaceOp":"append"}
{"type":"step/end","seq":17,"time":1786434813594,"data":{"turn":1,"step":1}}
{"type":"step/start","seq":18,"time":1786434813600,"data":{"turn":1,"step":2}}
{"type":"assistant/chunk","seq":19,"time":1786434813605,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","seq":20,"time":1786434813605,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"bounded-task-second","name":"bash","argumentsDelta":"{\"command\":\"printf SHOULD_NOT_RUN > second-task-ran.txt; while :; do sleep 60; done\",\"description\":\"Attempt a second background task\",\"run_in_background\":true}"}}}
{"type":"assistant/chunk","seq":21,"time":1786434813605,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bounded-task-second","name":"bash","arguments":"{\"command\":\"printf SHOULD_NOT_RUN > second-task-ran.txt; while :; do sleep 60; done\",\"description\":\"Attempt a second background task\",\"run_in_background\":true}"}}}}
{"type":"assistant/chunk","seq":22,"time":1786434813605,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":23,"time":1786434813605,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":24,"time":1786434813605,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bounded-task-second","name":"bash","arguments":"{\"command\":\"printf SHOULD_NOT_RUN > second-task-ran.txt; while :; do sleep 60; done\",\"description\":\"Attempt a second background task\",\"run_in_background\":true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"48c909b3-5651-462f-b0d3-09198d119a2f"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"}
{"type":"tool/call","seq":25,"time":1786434813606,"data":{"turn":1,"step":2,"callId":"bounded-task-second","name":"bash","arguments":"{\"command\":\"printf SHOULD_NOT_RUN > second-task-ran.txt; while :; do sleep 60; done\",\"description\":\"Attempt a second background task\",\"run_in_background\":true}"}}
{"type":"tool/result","seq":26,"time":1786434813609,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"bounded-task-second"},"content":[{"type":"tool-result","toolCallId":"bounded-task-second","content":[{"type":"text","text":"Error: background task limit reached for this owner (limit: 1); use task_kill to stop an unneeded task, wait for it to finish, then retry"}],"isError":true}],"role":"user","id":"c0386bdf-df3c-4d2b-af8e-04ee28682214"}},"sourceEventSeqs":[25],"surfaceOp":"append"}
{"type":"step/end","seq":27,"time":1786434813609,"data":{"turn":1,"step":2}}
{"type":"step/start","seq":28,"time":1786434813614,"data":{"turn":1,"step":3}}
{"type":"assistant/chunk","seq":29,"time":1786434813618,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","seq":30,"time":1786434813618,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"bounded-task-kill","name":"task_kill","argumentsDelta":"{\"task_id\":\"bash-1\",\"reason\":\"free the bounded task slot\"}"}}}
{"type":"assistant/chunk","seq":31,"time":1786434813618,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bounded-task-kill","name":"task_kill","arguments":"{\"task_id\":\"bash-1\",\"reason\":\"free the bounded task slot\"}"}}}}
{"type":"assistant/chunk","seq":32,"time":1786434813618,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":33,"time":1786434813618,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":34,"time":1786434813618,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bounded-task-kill","name":"task_kill","arguments":"{\"task_id\":\"bash-1\",\"reason\":\"free the bounded task slot\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"6dc2d854-59f7-4c70-8a0f-64416b324055"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[29,30,31,32,33],"surfaceOp":"append"}
{"type":"tool/call","seq":35,"time":1786434813618,"data":{"turn":1,"step":3,"callId":"bounded-task-kill","name":"task_kill","arguments":"{\"task_id\":\"bash-1\",\"reason\":\"free the bounded task slot\"}"}}
{"type":"tool/result","seq":36,"time":1786434813623,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"bounded-task-kill"},"content":[{"type":"tool-result","toolCallId":"bounded-task-kill","content":[{"type":"text","text":"requested cancellation of task bash-1"}],"isError":false}],"role":"user","id":"5abf87b2-3e10-448f-a529-dfc40dce2f08"}},"sourceEventSeqs":[35],"surfaceOp":"append"}
{"type":"step/end","seq":37,"time":1786434813623,"data":{"turn":1,"step":3}}
{"type":"step/start","seq":38,"time":1786434813628,"data":{"turn":1,"step":4}}
{"type":"assistant/chunk","seq":39,"time":1786434813632,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","seq":40,"time":1786434813632,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"bounded-task-side-effect-check","name":"bash","argumentsDelta":"{\"command\":\"test ! -e second-task-ran.txt\",\"description\":\"Verify the rejected producer did not run\"}"}}}
{"type":"assistant/chunk","seq":41,"time":1786434813632,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bounded-task-side-effect-check","name":"bash","arguments":"{\"command\":\"test ! -e second-task-ran.txt\",\"description\":\"Verify the rejected producer did not run\"}"}}}}
{"type":"assistant/chunk","seq":42,"time":1786434813632,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":43,"time":1786434813632,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":44,"time":1786434813632,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bounded-task-side-effect-check","name":"bash","arguments":"{\"command\":\"test ! -e second-task-ran.txt\",\"description\":\"Verify the rejected producer did not run\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"85ebd1ec-c3b2-4bd2-87cb-135089efc440"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[39,40,41,42,43],"surfaceOp":"append"}
{"type":"tool/call","seq":45,"time":1786436340879,"data":{"turn":1,"step":4,"callId":"bounded-task-side-effect-check","name":"bash","arguments":"{\"command\":\"test ! -e second-task-ran.txt\",\"description\":\"Verify the rejected producer did not run\"}"}}
{"type":"tool/result","seq":46,"time":1786436340886,"data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"bounded-task-side-effect-check"},"content":[{"type":"tool-result","toolCallId":"bounded-task-side-effect-check","content":[{"type":"text","text":"(no output)"}],"isError":false}],"role":"user","id":"49f76b57-92a2-4ae2-9711-11ad7cbd4e4c"}},"sourceEventSeqs":[45],"surfaceOp":"append"}
{"type":"step/end","seq":47,"time":1786436340886,"data":{"turn":1,"step":4}}
{"type":"step/start","seq":48,"time":1786436340891,"data":{"turn":1,"step":5}}
{"type":"assistant/chunk","seq":49,"time":1786436340897,"data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":50,"time":1786436340897,"data":{"turn":1,"step":5,"chunk":{"type":"text-delta","index":0,"text":"BOUNDED_BACKGROUND_TASKS"}}}
{"type":"assistant/chunk","seq":51,"time":1786436340897,"data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"BOUNDED_BACKGROUND_TASKS"}}}}
{"type":"assistant/chunk","seq":52,"time":1786436340897,"data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}}
{"type":"assistant/chunk","seq":53,"time":1786436340897,"data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":54,"time":1786436340897,"data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"text","text":"BOUNDED_BACKGROUND_TASKS"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"de775b06-2bb8-4bc0-8716-4fc31b9685c6"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[49,50,51,52,53],"surfaceOp":"append"}
{"type":"step/end","seq":55,"time":1786436340898,"data":{"turn":1,"step":5}}
{"type":"turn/end","seq":56,"time":1786436340898,"data":{"turn":1,"reason":{"kind":"completed"}}}
@@ -0,0 +1,4 @@
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}}
{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}"}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"BOUNDED_BACKGROUND_TASKS"}}}}
{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}
+1 -1
View File
@@ -127,7 +127,7 @@
"release:pack": "tsx scripts/release/pack.ts",
"release:verify-packed-install": "tsx scripts/release/verify-packed-install.ts",
"release:publish": "tsx scripts/release/publish.ts",
"dsh": "pnpm run build && node --import tsx/esm apps/cli/src/bin.ts",
"dsh": "node --import tsx/esm apps/cli/src/bin.ts",
"demo:code-mode": "node scripts/demo-code-mode.mjs",
"demo:cordis": "node scripts/demo-cordis.mjs",
"demo:acp": "node --import tsx packages/examples/acp-demo/src/bin.ts --config examples/acp-agent/cordis.yml",
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent 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/acp-demo/README.md
README.md: edc45c9857a631cef72eb41b1a98c390f112291e
README.zh.md: c2946aa3d1feaed558408cf0921e2480c031187d
README.md: c1a15a424d9d66b90bbec451e220198bfe0a45df
README.zh.md: 16a8d782c8fa38fe9e2ae1e634dbc406be1cbd12
+1
View File
@@ -34,6 +34,7 @@ The app does not install commands, user interaction, session navigation, configu
| `workspaceContext` | required | Workspace-instruction byte budget/config, or `false`. |
| `skills` | owner defaults | Skill registry, local provider, and model-facing skill tool. |
| `toolBash` | owner defaults | Model-facing bash tool config. |
| `tasks` | `{ maxConcurrentTasksPerOwner: 10 }` | Process-local per-owner active-task admission. |
| `toolTasks` | owner defaults | Generic background-task control config, or `false`. |
| `goals` | owner defaults | Persisted same-session goal domain and model tools, or `false`. |
+1
View File
@@ -34,6 +34,7 @@ ACPAgent Client Protocol)自动化服务器应用:默认 agent(智能
| `workspaceContext` | 必填 | 工作区指令字节预算/配置,或 `false`。 |
| `skills` | 拥有者默认值 | skill 注册表、本地提供方和面向模型的 skill 工具。 |
| `toolBash` | 拥有者默认值 | 面向模型的 bash 工具配置。 |
| `tasks` | `{ maxConcurrentTasksPerOwner: 10 }` | 进程内按 owner 限制活动任务的准入配置。 |
| `toolTasks` | 拥有者默认值 | 通用后台任务控制配置,或 `false`。 |
| `goals` | 拥有者默认值 | 持久化的同会话目标领域与模型工具,或 `false`。 |
+3
View File
@@ -65,6 +65,8 @@ export interface Config {
skills?: agentCore.SkillConfig
/** Model-facing bash tool config forwarded through agent-core. */
toolBash?: NonNullable<agentCore.Config['toolBash']>
/** Process-local background-task admission config forwarded through agent-core. */
tasks?: NonNullable<agentCore.Config['tasks']>
/** Generic background-task controls forwarded through agent-core; set false to omit their tools. */
toolTasks?: NonNullable<agentCore.Config['toolTasks']>
/** Persisted same-session goals; owner defaults enable them, or false disables the stack and tools. */
@@ -92,6 +94,7 @@ export const Config: z<Config> = z.object({
workspaceContext: z.union([z.const(false), workspaceContext.Config]).required(),
skills: agentCore.SkillConfigSchema,
toolBash: agentCore.ToolBashConfigSchema,
tasks: agentCore.TasksConfigSchema,
toolTasks: z.union([z.const(false), agentCore.ToolTasksConfigSchema]),
goals: z.union([z.const(false), agentCore.GoalConfigSchema]),
})
@@ -182,6 +182,31 @@ describe('dsh-acp-demo composition', () => {
await ctx.fiber.dispose()
})
it('forwards task admission config to the bundled task provider', async () => {
const ctx = await mount({
provider: 'mock',
model: 'mock',
tasks: { maxConcurrentTasksPerOwner: 1 },
skills: await isolatedSkillsConfig(),
workspaceContext: false,
})
let settle!: (outcome: { status: 'killed' }) => void
ctx.tasks.start({
kind: 'bash',
label: 'hold configured slot',
run: () => ({
cancel: () => { settle({ status: 'killed' }) },
done: new Promise((resolve) => { settle = resolve }),
}),
})
expect(() => ctx.tasks.start({
kind: 'bash',
label: 'blocked configured task',
run: () => ({ cancel: () => {}, done: Promise.resolve({ status: 'completed' }) }),
})).toThrow('(limit: 1)')
await ctx.fiber.dispose()
})
it('forwards bundled tool config into agent-core', async () => {
const ctx = await mount({
provider: 'mock',
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent 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: 5957d9a8e9218e18d5d7d0f620b6be811f2c230f
README.zh.md: 78372240764ff3c779ea0805aedd26a00baf1768
README.md: 789715e53038f610d1e2db79cf56f9aabd681fac
README.zh.md: 6561dd8948d9a4763123f9aae31e6dde64c54b27
+2 -2
View File
@@ -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?, persona?, toolOrder?, tools?, dshHome?, sessionTitle?, skills?, workspaceContext, toolBash?, toolTasks?, goals?, invariants? }
// { agents?, maxParallelToolCalls?, includeHarnessIdentity?, persona?, toolOrder?, tools?, dshHome?, sessionTitle?, skills?, workspaceContext, toolBash?, tasks?, toolTasks?, 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`. Prompt, tool, title, skill, workspace-context, invariant, goal, and task settings retain the schemas and defaults documented by their owning packages. `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`. Prompt, tool, title, skill, workspace-context, invariant, goal, and task settings retain the schemas and defaults documented by their owning packages; `tasks.maxConcurrentTasksPerOwner` configures the local provider independently of the model-facing `toolTasks` 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`](../../support/invariants/README.md) for regex and lifecycle rules.
@@ -55,11 +55,11 @@
```ts
import type { Config } from '@deepseek-ai/dsh-agent-spine-demo'
// { agents?, maxParallelToolCalls?, includeHarnessIdentity?, persona?, toolOrder?, tools?, dshHome?, sessionTitle?, skills?, workspaceContext, toolBash?, toolTasks?, goals?, invariants? }
// { agents?, maxParallelToolCalls?, includeHarnessIdentity?, persona?, toolOrder?, tools?, dshHome?, sessionTitle?, skills?, workspaceContext, toolBash?, tasks?, toolTasks?, goals?, invariants? }
// workspaceContext requires { maxBytes } or false; the other owner schemas supply defaults.
```
组合包将每个字段转发给拥有它的子节点。应用包提供预创建的 agent:无头和 JSON-RPC 组合会创建 `main`ACP 应用则在 `session/new` 按需创建 agent。提示词、工具、标题、skill、工作区上下文、不变式、目标和任务设置沿用其所属包记录的 schema 与默认值。`pickSpineConfig()` 只复制该组合包拥有的字段,`dshHome` 值冲突会在组合时失败。
组合包将每个字段转发给拥有它的子节点。应用包提供预创建的 agent:无头和 JSON-RPC 组合会创建 `main`ACP 应用则在 `session/new` 按需创建 agent。提示词、工具、标题、skill、工作区上下文、不变式、目标和任务设置沿用其所属包记录的 schema 与默认值`tasks.maxConcurrentTasksPerOwner` 配置本地 Service provider,并与面向模型的 `toolTasks` 控制工具相互独立`pickSpineConfig()` 只复制该组合包拥有的字段,`dshHome` 值冲突会在组合时失败。
例如,`{ invariants: { enabled: true, package_allowlist: ['^@deepseek-ai/dsh-'], package_blocklist: ['agent-loop$'] } }` 会让包拥有的配套插件保持挂载,但抑制被阻止的拥有者。Blocklist 匹配优先于 allowlist 匹配;正则表达式与生命周期规则见 [`dsh-invariants`](../../support/invariants/README.md)。
@@ -22,7 +22,7 @@ import AgentRegistry from '@deepseek-ai/dsh-agent'
import GoalService, { type Config as GoalDomainConfig } from '@deepseek-ai/dsh-goal'
import * as goalSession from '@deepseek-ai/dsh-goal-session'
import * as toolGoal from '@deepseek-ai/dsh-tool-goal'
import LocalTaskService from '@deepseek-ai/dsh-tasks-local'
import LocalTaskService, { type Config as TasksConfig } from '@deepseek-ai/dsh-tasks-local'
import InvariantService, { type Config as InvariantConfig } from '@deepseek-ai/dsh-invariants'
import * as sessionInvariant from '@deepseek-ai/dsh-session/invariant'
import * as agentInvariant from '@deepseek-ai/dsh-agent/invariant'
@@ -75,9 +75,10 @@ export interface GoalConfig {
* `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
* workspace-context loader, and `toolBash`/`toolTasks` to the model-facing tool
* plugins this bundle owns. Provider adapters own their `retryPolicy`; this
* bundle always mounts its executor.
* workspace-context loader, `tasks` to the process-local task provider, and
* `toolBash`/`toolTasks` to the model-facing tool plugins this bundle owns.
* Provider adapters own their `retryPolicy`; this bundle always mounts its
* executor.
* `goals` opts into and configures the persisted goal domain plus its model tool
* and same-session driver; `invariants` configures global and package-filtered
* relational checks. Owner schemas supply defaults for optional input;
@@ -114,6 +115,8 @@ export interface Config {
skills?: SkillConfig
/** Model-facing bash tool config, or false when another plugin owns `bash`. */
toolBash?: toolBash.Config | false
/** Process-local background-task admission config. */
tasks?: TasksConfig
/** Generic background-task controls; set false to keep the task service without model-facing task tools. */
toolTasks?: toolTasks.Config | false
/** Global enablement and package-name filters for invariant companions. */
@@ -138,6 +141,9 @@ export const SessionTitleConfigSchema: z<SessionTitleConfig> = SessionTitleServi
export const ToolBashConfigSchema: z<toolBash.Config | false> =
z.union([z.const(false), toolBash.Config])
/** The process-local task registry schema exported for app packages that forward `tasks`. */
export const TasksConfigSchema: z<TasksConfig> = LocalTaskService.Config
/** The task-control-tool config schema exported for app packages that forward `toolTasks`. */
export const ToolTasksConfigSchema: z<toolTasks.Config> = toolTasks.Config
@@ -158,10 +164,11 @@ export const Config = z.intersect([
skills: SkillConfigSchema,
workspaceContext: z.union([z.const(false), workspaceContext.Config]).required(),
toolBash: ToolBashConfigSchema,
tasks: TasksConfigSchema,
toolTasks: z.union([z.const(false), ToolTasksConfigSchema]),
invariants: InvariantService.Config,
goals: z.union([z.const(false), GoalConfigSchema]),
}) as unknown as z<Pick<Config, 'tools' | 'dshHome' | 'sessionTitle' | 'skills' | 'workspaceContext' | 'toolBash' | 'toolTasks' | 'invariants' | 'goals'>>,
}) as unknown as z<Pick<Config, 'tools' | 'dshHome' | 'sessionTitle' | 'skills' | 'workspaceContext' | 'toolBash' | 'tasks' | 'toolTasks' | 'invariants' | 'goals'>>,
]) as unknown as z<Config>
/**
@@ -181,6 +188,7 @@ export function pickSpineConfig(config: Omit<Config, 'agents'>): Omit<Config, 'a
workspaceContext: config.workspaceContext,
...config.skills !== undefined ? { skills: config.skills } : {},
...config.toolBash !== undefined ? { toolBash: config.toolBash } : {},
...config.tasks !== undefined ? { tasks: config.tasks } : {},
...config.toolTasks !== undefined ? { toolTasks: config.toolTasks } : {},
...config.invariants !== undefined ? { invariants: config.invariants } : {},
...config.goals !== undefined ? { goals: config.goals } : {},
@@ -228,7 +236,7 @@ export function apply(ctx: Context, config: Config): void {
ctx.plugin(toolGoal, config.goals.tool ?? {})
ctx.plugin(goalSession)
}
ctx.plugin(LocalTaskService)
ctx.plugin(LocalTaskService, config.tasks ?? {})
ctx.plugin(InvariantService, config.invariants ?? {})
ctx.plugin(sessionInvariant)
ctx.plugin(agentInvariant)
@@ -300,6 +300,28 @@ describe('dsh-agent-spine-demo bundle', () => {
await ctx.fiber.dispose()
})
it('forwards task admission config to the process-local provider', async () => {
const ctx = await mount({
tasks: { maxConcurrentTasksPerOwner: 1 },
workspaceContext: false,
})
let settle!: (outcome: { status: 'killed' }) => void
ctx.tasks.start({
kind: 'probe',
label: 'hold configured slot',
run: () => ({
cancel: () => { settle({ status: 'killed' }) },
done: new Promise((resolve) => { settle = resolve }),
}),
})
expect(() => ctx.tasks.start({
kind: 'probe',
label: 'blocked configured task',
run: () => ({ cancel: () => {}, done: Promise.resolve({ status: 'completed' }) }),
})).toThrow('(limit: 1)')
await ctx.fiber.dispose()
})
it('tolerates a schema-bypassing direct apply (the ?? fallbacks fire)', async () => {
// ctx.plugin validates + defaults the bundle config first; a direct apply
// skips the schema, so the forwarding `?? []` / `?? ''` are what fire.
@@ -716,6 +738,7 @@ describe('dsh-agent-spine-demo bundle', () => {
workspaceContext: false as const,
skills: { enabled: false },
toolBash: { enableRunInBackground: false },
tasks: { maxConcurrentTasksPerOwner: 4 },
toolTasks: false as const,
invariants: { enabled: false },
}
@@ -730,6 +753,7 @@ describe('dsh-agent-spine-demo bundle', () => {
workspaceContext: false,
skills: appConfig.skills,
toolBash: appConfig.toolBash,
tasks: appConfig.tasks,
toolTasks: appConfig.toolTasks,
invariants: appConfig.invariants,
})
@@ -1150,7 +1150,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
methods: [
{
signature: 'abstract start(spec: TaskStart): TaskId',
jsDoc: '/**\n * Preflight access, validation, and owner cleanup before starting and\n * atomically registering work. A throwing starter leaves nothing registered;\n * after it returns, registration cannot fail. Settlement records the outcome,\n * notifies listeners, and releases waiters.\n * @param spec - task identity, owner, and synchronous starter.\n * @returns the registry-issued `<kind>-N` id.\n */',
jsDoc: '/**\n * Preflight access, validation, owner cleanup, and implementation-owned\n * admission before starting and atomically registering work. Any preflight\n * rejection leaves no task id or execution resource. A throwing starter\n * leaves nothing registered; after it returns, registration cannot fail.\n * Settlement records the outcome, notifies listeners, and releases waiters.\n * @param spec - task identity, owner, and synchronous starter.\n * @returns the registry-issued `<kind>-N` id.\n */',
},
{
signature: 'abstract list(caller?: Agent): TaskSnapshot[]',
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/tasks/tasks-local/README.md
README.md: cc2e8422c367eeacfc5fc504298ecd6bfeae4c67
README.zh.md: 81fc0a5b1e12b2b15705370bb0748b1733b312aa
README.md: f558676b36bb5462453bde553eac27b458e1268e
README.zh.md: ed8f2b1220692a00c09b6605b18231f5263f00e5
+8 -2
View File
@@ -2,7 +2,13 @@
English | [中文](README.zh.md)
Process-local implementation of the [`@deepseek-ai/dsh-tasks`](../tasks/README.md) registry contract: `LocalTaskService` keeps every record in memory, issues per-kind `<kind>-N` ids, and hands out fresh snapshots, never live state. It has no config; load it as a plugin and it registers as `ctx.tasks`.
Process-local implementation of the [`@deepseek-ai/dsh-tasks`](../tasks/README.md) registry contract: `LocalTaskService` keeps every record in memory, issues per-kind `<kind>-N` ids, and hands out fresh snapshots, never live state. Load it as a plugin and it registers as `ctx.tasks`.
## Admission
`maxConcurrentTasksPerOwner` is a positive safe integer and defaults to `10`. Before invoking a producer, `start()` counts the exact owner's `running` and `stopping` records; all unowned tasks share one separate service bucket. Terminal history does not occupy capacity, and only producer `done` settlement releases a stopping task's place.
At capacity, `start()` fails before producer execution and id allocation with an error that names the limit and tells the model to use `task_kill`, wait for the task to finish stopping, and retry. The registry does not queue, preempt, or maintain a second mutable counter.
## Lifecycle
@@ -25,4 +31,4 @@ No direct invalidation; the named consumer owns any request-prefix changes.
## Known Limitations and Deferred Work
- **Tasks are process-local** — records die with the harness process; durable or cross-restart execution needs a separate backend implementing the seam.
- **A silently ineffective cancel can stall teardown** — only an explicit throw can be force-failed safely.
- **A silently ineffective cancel can stall teardown and hold capacity** — if `cancel` returns without settling `done`, the registry cannot distinguish it from a slow stop; the task keeps one bucket slot for the rest of the service lifetime, and only an explicit throw can be force-failed safely.
+8 -2
View File
@@ -2,7 +2,13 @@
[English](README.md) | 中文
[`@deepseek-ai/dsh-tasks`](../tasks/README.md) 注册表约定的进程本地实现:`LocalTaskService` 把每条记录保存在内存中,按 kind 签发 `<kind>-N` id,并且只交出全新快照,从不交出实时状态。它没有配置;作为插件加载后即注册为 `ctx.tasks`
[`@deepseek-ai/dsh-tasks`](../tasks/README.md) 注册表约定的进程本地实现:`LocalTaskService` 把每条记录保存在内存中,按 kind 签发 `<kind>-N` id,并且只交出全新快照,从不交出实时状态。作为插件加载后即注册为 `ctx.tasks`
## 准入
`maxConcurrentTasksPerOwner` 必须是正的安全整数,默认值为 `10`。调用生产方之前,`start()` 会统计确切 owner 的 `running``stopping` 记录;所有无 owner 任务共享另一个独立的服务级桶。终止历史不占用容量,处于 `stopping` 的任务只有在生产方 `done` 结算后才释放名额。
达到容量时,`start()` 会在生产方执行和 id 分配前失败;错误会给出上限,并告诉模型使用 `task_kill`、等待任务完全停稳后再重试。注册表不会排队或抢占任务,也不会维护第二份可变计数。
## 生命周期
@@ -25,4 +31,4 @@
## 已知限制与暂缓事项
- **任务只存在于进程本地**:记录会随 harness 进程终止而消失;持久或跨重启执行需要一个单独实现该 seam 的后端。
- **静默无效的取消可能使销毁过程停滞**只有显式抛出异常才能安全地强制标为失败。
- **静默无效的取消可能使销毁过程停滞并持续占用容量**:如果 `cancel` 返回后始终未结算 `done`,注册表就无法将其与缓慢停止区分开;该任务会在服务剩余生命周期内持续占用一个桶名额,只有显式抛出异常才能安全地强制标为失败。
+5
View File
@@ -39,7 +39,12 @@
"@deepseek-ai/dsh-timeout": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"dependencies": {
"@deepseek-ai/schemastery": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/cordis-plugin-include": "workspace:^",
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-brand": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
+42 -1
View File
@@ -10,6 +10,7 @@
*/
import { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { AnonymousEntries, ScopedLayers, scopeOf } from '@deepseek-ai/dsh-scope'
import type { ScopeLayer } from '@deepseek-ai/dsh-scope'
@@ -23,6 +24,18 @@ import type {
/** Timeout code that distinguishes a bounded wait from caller cancellation. */
export const TASK_WAIT_TIMEOUT = 'TASK_WAIT_TIMEOUT'
/** Default maximum number of active tasks in one exact-owner bucket. */
const DEFAULT_MAX_CONCURRENT_TASKS_PER_OWNER = 10
/** Configuration for the process-local task registry. */
export interface Config {
/**
* Maximum `running` plus `stopping` tasks per exact owner or in the shared unowned bucket;
* omission defaults to 10.
*/
maxConcurrentTasksPerOwner?: number
}
/** The registry's mutable per-task record (never handed out — see {@link LocalTaskService.snapshot}). */
interface TrackedTask {
id: TaskId
@@ -76,6 +89,16 @@ class TaskLayer implements ScopeLayer {
* semantics this implementation honors.
*/
export class LocalTaskService extends TaskService {
static Config: z<Config> = z.object({
maxConcurrentTasksPerOwner: z.number()
.step(1)
.min(1)
.max(Number.MAX_SAFE_INTEGER)
.default(DEFAULT_MAX_CONCURRENT_TASKS_PER_OWNER),
})
/** Schemastery-defaulted active-task limit. */
private readonly maxConcurrentTasksPerOwner: number
private store = new Map<TaskId, TrackedTask>()
private counters = new Map<string, number>()
/**
@@ -97,8 +120,10 @@ export class LocalTaskService extends TaskService {
/** Service context used by detached settlement continuations and teardown. */
private readonly selfCtx: Context
constructor(ctx: Context) {
constructor(ctx: Context, config: Config) {
super(ctx)
// Schemastery validates and fills the default before constructing the service.
this.maxConcurrentTasksPerOwner = (config as Required<Config>).maxConcurrentTasksPerOwner
this.selfCtx = ctx
ctx.effect(() => () => this.disposeAll(), 'tasks teardown')
}
@@ -115,6 +140,13 @@ export class LocalTaskService extends TaskService {
}
if (spec.owner !== undefined) this.ensureOwnerCleanup(spec.owner)
const active = this.activeTaskCount(spec.owner)
if (active >= this.maxConcurrentTasksPerOwner) {
throw new Error(
`background task limit reached for this owner (limit: ${this.maxConcurrentTasksPerOwner}); use task_kill to stop an unneeded task, wait for it to finish, then retry`,
)
}
const hooks = spec.run()
const count = (this.counters.get(spec.kind) ?? 0) + 1
this.counters.set(spec.kind, count)
@@ -286,6 +318,15 @@ export class LocalTaskService extends TaskService {
.some(layer => !layer.controllers.isEmpty())
}
/** Count authoritative active records for one exact owner or the shared unowned bucket. */
private activeTaskCount(owner: Agent | undefined): number {
let count = 0
for (const task of this.store.values()) {
if (task.owner === owner && (task.status === 'running' || task.status === 'stopping')) count += 1
}
return count
}
/**
* The completion listeners that own `owner`'s notices: the global layer's
* first, then each scoped layer along the owner's chain. A listener outside
+5 -2
View File
@@ -15,8 +15,11 @@ export const name = 'tasks-local-invariant'
export const inject = ['invariants']
/**
* No runtime invariant: the Service Definition companion in `@deepseek-ai/dsh-tasks` already
* validates every registry snapshot this implementation publishes.
* No runtime invariant: `@deepseek-ai/dsh-tasks/invariant` owns per-snapshot identity, status,
* timestamp, and owner checks. This provider's admission decision uses private configuration and
* must fail before a backend starter runs; `LocalTaskService.start()` enforces it synchronously
* for current producers. Repeating an aggregate after publication would expose private
* configuration solely to this companion and would not verify the fail-closed pre-start guarantee.
*/
const install: InvariantInstaller = () => {}
@@ -0,0 +1,66 @@
import { afterEach, describe, expect, it } from 'vitest'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { Context } from '@deepseek-ai/cordis'
import Include from '@deepseek-ai/cordis-plugin-include'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import LocalTaskService from '@deepseek-ai/dsh-tasks-local'
let root: string | undefined
let context: Context | undefined
afterEach(async () => {
await context?.fiber.dispose()
context = undefined
if (root !== undefined) await rm(root, { recursive: true, force: true })
root = undefined
})
describe('tasks-local through a real Loader composition', () => {
it('applies the provider-owned admission config from a Cordis row', async () => {
root = await mkdtemp(join(tmpdir(), 'dsh-tasks-local-loader-'))
const configPath = join(root, 'cordis.yml')
await writeFile(configPath, [
"- name: '@deepseek-ai/dsh-tasks-local'",
' config:',
' maxConcurrentTasksPerOwner: 1',
'',
].join('\n'))
context = new Context()
context.baseUrl = pathToFileURL(root).href + '/'
await context.plugin(Loader)
context.loader.builtins.include = Include
context.loader.internal = {
version: 'v2',
async import(specifier: string) {
if (specifier === '@deepseek-ai/dsh-tasks-local') return LocalTaskService
throw new Error(`unexpected Loader import: ${specifier}`)
},
} as unknown as NonNullable<typeof context.loader.internal>
await context.loader.create({
name: 'cordis:include',
config: { path: pathToFileURL(configPath).href },
})
await context.loader.await()
expect(context.tasks).toBeInstanceOf(LocalTaskService)
context.tasks.attachController('loader-test')
let settle!: (outcome: { status: 'killed' }) => void
context.tasks.start({
kind: 'bash',
label: 'hold loader slot',
run: () => ({
cancel: () => { settle({ status: 'killed' }) },
done: new Promise((resolve) => { settle = resolve }),
}),
})
expect(() => context!.tasks.start({
kind: 'bash',
label: 'blocked loader task',
run: () => ({ cancel: () => {}, done: Promise.resolve({ status: 'completed' }) }),
})).toThrow('(limit: 1)')
})
})
+98 -3
View File
@@ -7,7 +7,7 @@ import { bindScopeParent, createScope, scopeOf } from '@deepseek-ai/dsh-scope'
import type { ScopeKey } from '@deepseek-ai/dsh-scope'
import { TaskId } from '@deepseek-ai/dsh-tasks'
import type { TaskHooks, TaskKind, TaskOutcome, TaskSnapshot, TaskStart } from '@deepseek-ai/dsh-tasks'
import LocalTaskService from '@deepseek-ai/dsh-tasks-local'
import LocalTaskService, { type Config as TasksConfig } from '@deepseek-ai/dsh-tasks-local'
declare module '@deepseek-ai/dsh-tasks' {
interface TaskKindMap {
@@ -76,10 +76,10 @@ function producer(overrides: Partial<Omit<TaskStart, 'run'> & TaskHooks> = {}) {
return { spec, settle, reject, cancels }
}
async function harness() {
async function harness(config: TasksConfig = {}) {
const ctx = new Context()
await ctx.plugin(AgentRegistry)
await ctx.plugin(LocalTaskService)
await ctx.plugin(LocalTaskService, config)
ctx.tasks.attachController('test-controller')
return ctx
}
@@ -166,6 +166,101 @@ describe('LocalTaskService.start', () => {
expect(() => ctx.tasks.start(producer({ outputLimitBytes: 0 }).spec)).toThrow('outputLimitBytes')
})
it.each([0, -1, 1.5, Number.NaN, Number.POSITIVE_INFINITY, Number.MAX_SAFE_INTEGER + 1])(
'rejects invalid maxConcurrentTasksPerOwner config: %s',
async (maxConcurrentTasksPerOwner) => {
const ctx = new Context()
await expect(ctx.plugin(LocalTaskService, { maxConcurrentTasksPerOwner }))
.rejects.toThrow()
},
)
it('accepts the largest safe integer limit', async () => {
const ctx = await harness({ maxConcurrentTasksPerOwner: Number.MAX_SAFE_INTEGER })
expect(ctx.tasks).toBeInstanceOf(LocalTaskService)
})
it('defaults each owner bucket to ten active tasks', async () => {
const ctx = await harness()
const live = Array.from({ length: 10 }, () => producer())
for (const task of live) ctx.tasks.start(task.spec)
const blocked = producer()
const run = vi.fn(() => blocked.spec.run())
expect(() => ctx.tasks.start({ ...blocked.spec, run }))
.toThrow('background task limit reached for this owner (limit: 10)')
expect(run).not.toHaveBeenCalled()
for (const task of live) task.settle({ status: 'completed' })
})
it('rejects before producer start and id allocation, then admits immediately after settlement', async () => {
const ctx = await harness({ maxConcurrentTasksPerOwner: 1 })
const first = producer()
expect(ctx.tasks.start(first.spec)).toBe('bash-1')
const blocked = producer()
const run = vi.fn(() => blocked.spec.run())
expect(() => ctx.tasks.start({ ...blocked.spec, run }))
.toThrow('use task_kill to stop an unneeded task, wait for it to finish, then retry')
expect(run).not.toHaveBeenCalled()
first.settle({ status: 'completed' })
await tick()
expect(ctx.tasks.start(blocked.spec)).toBe('bash-2')
})
it('keeps a stopping task in the bucket until producer settlement', async () => {
const ctx = await harness({ maxConcurrentTasksPerOwner: 1 })
const first = producer()
const id = ctx.tasks.start(first.spec)
expect(ctx.tasks.kill(id)).toBe('requested')
const replacement = producer()
expect(() => ctx.tasks.start(replacement.spec)).toThrow('(limit: 1)')
first.settle({ status: 'killed' })
await tick()
expect(ctx.tasks.start(replacement.spec)).toBe('bash-2')
})
it.each(['completed', 'killed', 'failed'] as const)(
'releases the bucket after a %s terminal outcome',
async (status) => {
const ctx = await harness({ maxConcurrentTasksPerOwner: 1 })
const first = producer()
ctx.tasks.start(first.spec)
first.settle({ status })
await tick()
expect(() => ctx.tasks.start(producer().spec)).not.toThrow()
},
)
it('isolates exact owners, replacement objects with the same session id, and the unowned bucket', async () => {
const ctx = await harness({ maxConcurrentTasksPerOwner: 1 })
const oldOwner = stubAgent(ctx, 'shared-session')
const detachOld = ctx.agents.register(oldOwner)
const oldTask = producer({ owner: oldOwner })
ctx.tasks.start(oldTask.spec)
const otherOwner = stubAgent(ctx, 'other-session')
ctx.agents.register(otherOwner)
expect(() => ctx.tasks.start(producer({ owner: otherOwner }).spec)).not.toThrow()
detachOld()
const replacement = stubAgent(ctx, 'shared-session')
ctx.agents.register(replacement)
expect(() => ctx.tasks.start(producer({ owner: replacement }).spec)).not.toThrow()
ctx.tasks.start(producer().spec)
expect(() => ctx.tasks.start(producer().spec)).toThrow('(limit: 1)')
expect(() => ctx.tasks.start(producer({ owner: oldOwner }).spec))
.toThrow('is not the registered agent instance')
oldTask.settle({ status: 'completed' })
await tick()
await disposeAgentScope(oldOwner)
})
it('issues kind-prefixed ids from per-kind counters', async () => {
const ctx = await harness()
expect(ctx.tasks.start(producer().spec)).toBe('bash-1')
+3
View File
@@ -14,6 +14,9 @@
{
"path": "../../../vendor/cordis"
},
{
"path": "../../../vendor/schemastery"
},
{
"path": "../../core/agent"
},
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/tasks/tasks/README.md
README.md: 18029a2e93396336139612ba72804aeb11e87edf
README.zh.md: 93efabb212274cdb54114167ff38f0a884b987f5
README.md: 60898e8de8ffa29a823c537ba5f5876b63a03c88
README.zh.md: b726ca85374de68514473130bb3732c45f352cf7
+1 -1
View File
@@ -6,7 +6,7 @@ The background task registry contract (`ctx.tasks`). The abstract `TaskService`
## Service contract
- `start(spec): TaskId` validates the attached controller, spec, exact live owner, and optional positive `outputLimitBytes` before calling the producer's `run()` once. A starter throw leaves nothing registered; successful return commits without another failable step.
- `start(spec): TaskId` validates the attached controller, spec, exact live owner, optional positive `outputLimitBytes`, and any provider-owned admission policy before calling the producer's `run()` once. A preflight rejection or starter throw leaves no task id or registered work; successful return commits without another failable step.
- `get(id, caller?)` and `list(caller?)` return non-consuming snapshots. Listing includes only caller-owned and unowned tasks.
- `read(id, caller?)` consumes the single cursor for stream tasks and reads terminal output idempotently for final-output tasks.
- `kill(id, caller?, reason?)` invokes producer cancellation before changing status. A cancellation throw leaves the task running; success changes it to `stopping` and marks terminal delivery reported.
+1 -1
View File
@@ -6,7 +6,7 @@
## 服务约定
- `start(spec): TaskId` 验证已附加的任务控制器、spec、确切且仍存活的 owner,以及可选的 `outputLimitBytes`(如提供则须为正数),然后只调用生产方的 `run()` 一次。启动方抛出异常时不注册任何内容;成功返回会直接提交,不再执行其他可能失败的步骤。
- `start(spec): TaskId` 验证已附加的任务控制器、spec、确切且仍存活的 owner可选的正数 `outputLimitBytes`,以及 Service provider 所拥有的准入策略,然后只调用生产方的 `run()` 一次。预检拒绝或启动方抛出异常时都不会生成 task id 或注册工作;成功返回会直接提交,不再执行其他可能失败的步骤。
- `get(id, caller?)``list(caller?)` 返回非消费式快照。列表只包含调用方拥有及无 owner 的任务。
- `read(id, caller?)` 消费流任务的唯一游标;对于最终输出任务,则以幂等方式读取终止输出。
- `kill(id, caller?, reason?)` 在更改状态前调用生产方取消。取消抛出异常时任务保持运行;成功则把状态改为 `stopping`,并将终止交付标记为已报告。
+5 -4
View File
@@ -71,10 +71,11 @@ export abstract class TaskService extends Service {
}
/**
* Preflight access, validation, and owner cleanup before starting and
* atomically registering work. A throwing starter leaves nothing registered;
* after it returns, registration cannot fail. Settlement records the outcome,
* notifies listeners, and releases waiters.
* Preflight access, validation, owner cleanup, and implementation-owned
* admission before starting and atomically registering work. Any preflight
* rejection leaves no task id or execution resource. A throwing starter
* leaves nothing registered; after it returns, registration cannot fail.
* Settlement records the outcome, notifies listeners, and releases waiters.
* @param spec - task identity, owner, and synchronous starter.
* @returns the registry-issued `<kind>-N` id.
*/
@@ -759,6 +759,7 @@ describe('completion notices', () => {
const prior = producer({ kind: 'pty-send' })
ctx.tasks.start(prior.spec)
prior.settle({ status: 'completed' })
await tick()
}
const inject = vi.fn()
const owner = fakeAgent(ctx, 'sess-1', { inject })
+10
View File
@@ -7422,10 +7422,20 @@ importers:
version: link:../../core/session
packages/tasks/tasks-local:
dependencies:
'@deepseek-ai/schemastery':
specifier: link:../../../vendor/schemastery
version: link:../../../vendor/schemastery
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
version: link:../../../vendor/cordis
'@deepseek-ai/cordis-plugin-include':
specifier: workspace:^
version: link:../../../vendor/include
'@deepseek-ai/cordis-plugin-loader':
specifier: workspace:^
version: link:../../../vendor/loader
'@deepseek-ai/dsh-agent':
specifier: workspace:^
version: link:../../core/agent
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent 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: 9614c06436ab6863a5e1b2ff83fbe605552dc13b
development.zh.md: 1c646ca39735b85a5d380768fe215c92532be7e7
development.md: 31dc254b58c05c2a19c7c4cd5dc1e53207517902
development.zh.md: dbb85a0cffc5e06c7ca781b2b01f6993395204e3
+9 -1
View File
@@ -58,4 +58,12 @@ python scripts/build-python-release.py --package runtime --platform macos-arm64
pip install --find-links dist-python deepseek-harness-sdk=="$version"
```
The runtime distribution is wheel-only. The release pipeline publishes three platform wheels with the pure SDK wheel: Linux x64, Linux arm64, and macOS arm64. A `python-vX.Y.Z` tag is accepted only when it matches the repository version.
The runtime distribution is wheel-only. The release pipeline publishes three platform wheels with the pure SDK wheel: Linux x64, Linux arm64, and macOS 14 or newer on arm64. A `python-v<repository-version>` tag is accepted only when it matches the repository version; prerelease repository versions such as `0.0.1-rc.1` use their normalized PEP 440 spelling, such as `0.0.1rc1`, inside wheel filenames and metadata.
## Validate a release candidate
Label a pull request `python-release-dry-run`, or manually run the GitHub `Release (Python)` workflow with `publish=false`, to build all four wheels, install the Linux release set on Python 3.10 and 3.14, check exact filenames and metadata, enforce PyPI's default per-file size limit, and retain one aggregate artifact with SHA-256 hashes. Both paths have no registry credentials; a pull request run cannot enter either publication job.
Public publication runs from the private automation repository; package metadata points to the separate read-only public source mirror, which does not run release Actions. The private repository defines the repository variable `PYPI_PUBLISHER_REPOSITORY` as its own `owner/name` and keeps `PUBLIC_PYPI_RELEASE_ENABLED=false` except during an intentional release.
Separate runtime and SDK jobs let an SDK upload failure resume without resending immutable runtime files. They accept `publish=true` only when the workflow runs from the configured publisher repository at the matching `python-v*` tag and the protected `pypi-runtime` and `pypi` environments approve the runtime and SDK jobs, respectively. PyPI Trusted Publishing still supplies short-lived OIDC credentials, but public attestations are disabled because they would disclose the private publisher identity.
+9 -1
View File
@@ -58,4 +58,12 @@ python scripts/build-python-release.py --package runtime --platform macos-arm64
pip install --find-links dist-python deepseek-harness-sdk=="$version"
```
运行时分发包仅提供 wheel 包。发布流水线会连同纯 SDK wheel 包一起发布三个平台 wheel 包:Linux x64、Linux arm64 和 macOS arm64。只有与仓库版本匹配时,才接受 `python-vX.Y.Z` 标签
运行时分发包仅提供 wheel 包。发布流水线会连同纯 SDK wheel 包一起发布三个平台 wheel 包:Linux x64、Linux arm64 和 macOS 14 或更高版本的 arm64。只有与仓库版本匹配时,才接受 `python-v<repository-version>` 标签;`0.0.1-rc.1` 之类的仓库预发布版本在 wheel 包文件名和元数据中使用规范化的 PEP 440 写法,例如 `0.0.1rc1`
## 验证候选发行版
为拉取请求添加 `python-release-dry-run` 标签,或手动运行 GitHub 的 `Release (Python)` 工作流并设置 `publish=false`,即可构建全部四个 wheel 包,在 Python 3.10 和 3.14 上安装 Linux 发行集合,检查精确文件名和元数据,执行 PyPI 默认单文件大小限制,并保留一份带 SHA-256 哈希的汇总产物。两条路径都没有注册表凭据,拉取请求运行无法进入任何发布作业。
公开发布从私有自动化仓库运行;包元数据指向独立的只读公开源码镜像,该镜像不运行发布 Actions。私有仓库把仓库变量 `PYPI_PUBLISHER_REPOSITORY` 定义为自身的 `owner/name`,并且只在有意发布期间把 `PUBLIC_PYPI_RELEASE_ENABLED``false` 改为 `true`
独立的运行时与 SDK 作业使 SDK 上传失败后可以继续执行,而无需重新发送不可变的运行时文件。只有工作流从配置的发布仓库、匹配的 `python-v*` 标签运行,且受保护的 `pypi-runtime``pypi` 环境分别批准运行时与 SDK 作业时,才接受 `publish=true`。PyPI Trusted Publishing 仍会提供短期 OIDC 凭据,但公开 attestation 会披露私有发布仓库身份,因此将其禁用。
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent 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: fa2fc83a88212f6ff163e1a5f86246bfac37cc1f
README.zh.md: 5b82f33cfe1413e4fb6ceded04d9b6feca4c94ca
README.md: 71dedf4cb8064d55bd64b32008b452158a1b154f
README.zh.md: 83c99ed33b2a4ffe00bcb3fe670be455664bfa18

Some files were not shown because too many files have changed in this diff Show More