Merge updated master and helper identity into bundled Computer Use
This commit is contained in:
commit
7d9bc01a50
1349 files changed
+27206
-18734
No files matched your search
@@ -2662,6 +2662,9 @@
|
||||
"simplification/2026-09-19-deepseek-messages-only.i18n.yaml": "sha256:7ffd708c5823c39cc9536178292e7e7fa57db5267bbc9acdda0a87e4953d5ba8",
|
||||
"simplification/2026-09-19-deepseek-messages-only.md": "sha256:241861200ce73e02f77d182633d6ea10b1f29c696c81bff002680762db01d9b5",
|
||||
"simplification/2026-09-19-deepseek-messages-only.zh.md": "sha256:311ef6f8f4cf60dae2a43ca94d772028b645631501fb3648aaeb406ba174991e",
|
||||
"simplification/2026-09-26-team-direct-inbox.i18n.yaml": "sha256:9a6f1137f0c68d976fda4f44c5c301b851a8ee8e46965f3ea808ec5551045fb0",
|
||||
"simplification/2026-09-26-team-direct-inbox.md": "sha256:88f7d9be3ae0b740ddb5a61a16e3c64044a207584b973acab8c3a40c36a7ff30",
|
||||
"simplification/2026-09-26-team-direct-inbox.zh.md": "sha256:d6d1fafa4aa7035a11930cca3bf998d676a62b5f9b75176dff69c3c5ff6b92f0",
|
||||
"testing/2026-06-11-property-based-testing.i18n.yaml": "sha256:9d378912101ad30203f1fcbcad23e0f5d6ec6f39e04a83f17f90887b5c5d761e",
|
||||
"testing/2026-06-11-property-based-testing.md": "sha256:cca3b6299784cff94965f21b6d59966109a0bb41fdc2f8cf56653fa6c417a20a",
|
||||
"testing/2026-06-11-property-based-testing.zh.md": "sha256:09f3d1e2defd2792f87fb0e9892fb7e1db006f3c75ea4581f97e9cde4ea98175",
|
||||
|
||||
@@ -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/archived/simplification/2026-09-26-team-direct-inbox.md
|
||||
2026-09-26-team-direct-inbox.md: 9b90d7ae30e0b711c602de9280cc11845d0525f0
|
||||
2026-09-26-team-direct-inbox.zh.md: 8838fb4a2824838b76142f30a6ff4d0aedf0276b
|
||||
+1
@@ -1,6 +1,7 @@
|
||||
# Agent Note: Team messages use the target inbox
|
||||
|
||||
Status: implemented
|
||||
Archived: 2026-10-15
|
||||
|
||||
English | [中文](2026-09-26-team-direct-inbox.zh.md)
|
||||
|
||||
+1
@@ -1,6 +1,7 @@
|
||||
# Agent Note: Team 消息使用目标 inbox
|
||||
|
||||
Status: implemented
|
||||
Archived: 2026-10-15
|
||||
|
||||
[English](2026-09-26-team-direct-inbox.md) | 中文
|
||||
|
||||
+2
-2
@@ -21,8 +21,8 @@
|
||||
en: 985e0eb63f3d1fd6
|
||||
zh: 337cc44fadc6227e
|
||||
/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/build-pipeline-and-artifacts:
|
||||
en: 9a4ba22142a7be24
|
||||
zh: 8553b02a923641a3
|
||||
en: 4a55602284b462e8
|
||||
zh: 762c4f8b719f179c
|
||||
/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/python-sdk-distribution-two-carriers-exe-for-production-node-for-development:
|
||||
en: b2a740a1ab7a5002
|
||||
zh: a48be6ca6fc65912
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ The [Office kit](2026-09-14-independent-libreoffice-kit.md) and its installed de
|
||||
|
||||
### 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-python-runtime-closure deploy --legacy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true --config.hoist-workspace-packages=false` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`, including [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the carrier-root `runtime-bootstrap.mjs` → restore direct workspace packages omitted by legacy deploy and reject any remaining manifest gap → replace staged dependency symlinks with their target bytes, remove package-manager `.bin` links, and fail if any symlink remains → verify the deployed bootstrap and inject pkg configuration with that bin plus assets covering dynamic profile, bundle, frontend, preset, native-library, and configuration reads → stage the target `node-pty` addon → invoke `pkg --sea --compress Brotli` once per target, where the vendored pkg patch pins Brotli's quality to 6: pkg passes no options, so its default quality 11 measured 132.8 s against 3.2 s at quality 6 for an archive 12% larger → write `deepseek-harness-sdk-runtime-<platform>-<arch>` under `dist-exe/` and copy it with its lightweight resource manifest and Office skill assets into the runtime directory. The Python runtime owns that bootstrap. It calls the public CLI export for ordinary launches and dispatches a provider-private selection to the same `@deepseek-ai/dsh-subprocess-local/runner` core without changing CLI grammar or adding another executable; the [native-containment reference](../../../../packages/subprocess/subprocess-local/README.md) owns that private path. Linux CI rebuilds `pty.node` inside the matching manylinux 2.28 container because legacy deploy omits that install side effect. Every target copies its native `@vscode/ripgrep` binary beside the executable as the required `-rg` sidecar; pkg runtimes select that sidecar through `process.pkg`, while ordinary Node execution uses `@vscode/ripgrep` directly. macOS uses its target prebuild and also emits the required `-spawn-helper`. The deploy flags preserve the measured runtime layout: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted supplies package directories that staging makes symlink-free; staging removes nested workspace copies only when pnpm records the same dependency identity at the payload root, preserving singleton services; disabling workspace hoisting excludes unrelated workspace source packages; 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. Production deployment permits unused workspace patches because development-only tools can be absent from its closure; included-package patch failures still fail deployment. Workspace installs retain strict unused-patch validation.
|
||||
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-python-runtime-closure deploy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.pnpmfile=scripts/python-runtime-deploy.pnpmfile.mjs --config.link-workspace-packages=true --config.hoist-workspace-packages=false` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`, including [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the carrier-root `runtime-bootstrap.mjs` → replace staged dependency symlinks with their target bytes, remove package-manager `.bin` links, and fail if any symlink remains → reject missing direct dependency directories, verify the deployed bootstrap, and inject pkg configuration with that bin plus assets covering dynamic profile, bundle, frontend, preset, native-library, and configuration reads → stage the target `node-pty` addon → invoke `pkg --sea --compress Brotli` once per target, where the vendored pkg patch pins Brotli's quality to 6: pkg passes no options, so its default quality 11 measured 132.8 s against 3.2 s at quality 6 for an archive 12% larger → write `deepseek-harness-sdk-runtime-<platform>-<arch>` under `dist-exe/` and copy it with its lightweight resource manifest and Office skill assets into the runtime directory. The Python runtime owns that bootstrap. It calls the public CLI export for ordinary launches and dispatches a provider-private selection to the same `@deepseek-ai/dsh-subprocess-local/runner` core without changing CLI grammar or adding another executable; the [native-containment reference](../../../../packages/subprocess/subprocess-local/README.md) owns that private path. Linux CI rebuilds `pty.node` inside the matching manylinux 2.28 container and copies the resulting addon into the deployed tree. Every target copies its native `@vscode/ripgrep` binary beside the executable as the required `-rg` sidecar; pkg runtimes select that sidecar through `process.pkg`, while ordinary Node execution uses `@vscode/ripgrep` directly. macOS uses its target prebuild and also emits the required `-spawn-helper`. The deploy flags preserve the measured runtime layout: the dedicated deploy lockfile retains the source lockfile’s versions and peer settings; [`scripts/python-runtime-deploy.pnpmfile.mjs`](../../../../scripts/python-runtime-deploy.pnpmfile.mjs) preserves exact local build approvals; the [locked deployment decision](../bug-fix/2026-11-01-locked-python-runtime-deploy.md) supersedes the legacy deployment requirement and disabled-peer rationale; hoisted supplies package directories that staging makes symlink-free; staging removes nested workspace copies only when pnpm records the same dependency identity at the payload root, preserving singleton services; disabling workspace hoisting excludes unrelated workspace source packages; 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 deploy never resolves those unpublished names from a registry. Production deployment permits unused workspace patches because development-only tools can be absent from its closure; included-package patch failures still fail deployment. Workspace installs retain strict unused-patch validation.
|
||||
|
||||
CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml) runs [installed-wheel validation](../testing/2026-08-23-installed-python-wheel-black-box-ci.md) on Linux/Windows x64 for pull requests and Linux ARM64 plus both macOS architectures for master pushes. The [public publication workflow](../../archived/process/2026-08-11-python-publication-workflow.md) calls it for all five targets; `workflow_dispatch` can still select a subset. Native builds run on linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64 / macos-x64 (`macos-15-intel`) / win-x64 (`windows-2025`), with `~/.pkg-cache` cached where applicable, and pkg handles macOS ad-hoc signing. Each leg installs the release-shaped SDK and runtime wheels into a clean venv outside the checkout, verifies their package and executable origin, then drives the complete keyless scenario set through the public SDK and direct NDJSON JSON-RPC. Trusted pull requests and master pushes additionally run a real DeepSeek two-turn tool smoke on their selected targets; fork and Dependabot heads receive no key. Linux inspects the executable and native addon's GLIBC requirements and runs an additional manylinux 2.28 smoke, while macOS checks the runtime, ripgrep, and PTY helper architectures and verifies that all three deployment targets fit the wheel tag. A full five-target run retains six artifacts, each containing one release file: the platform-independent SDK wheel and five 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 five native runtime wheels, then a single serialized job checks and publishes all six to the project PyPI registry. The [`python/sdk-runtime` README](../../../../python/sdk-runtime/README.md) owns the Windows target and the explicit exclusion of Windows arm64.
|
||||
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ exe 的 VFS 内是**构建产物形态的真实包树**(各包的 `lib/` + 真
|
||||
|
||||
### 构建流水线与产物
|
||||
|
||||
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true --config.hoist-workspace-packages=false` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`,其中包含作为载体根目录 `runtime-bootstrap.mjs` 的 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) → 恢复 legacy deploy 遗漏的直接工作区包,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 验证已部署的 bootstrap,并注入以该文件为 bin 的 pkg 配置及覆盖动态读取 profile、bundle、前端、preset、原生库与配置文件的 assets → 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea --compress Brotli`,其中 vendor 的 pkg 补丁把 Brotli 质量固定为 6:pkg 不传参数时默认质量 11,同一载荷实测 132.8 s,质量 6 为 3.2 s,产物大 12% → 将 `deepseek-harness-sdk-runtime-<platform>-<arch>` 写入 `dist-exe/`,并将它与轻量资源清单及 Office skill 资源一同拷回运行时目录。该 bootstrap 由 Python runtime 拥有;普通启动时它调用公开 CLI export,而提供方私有选择会分派到同一个 `@deepseek-ai/dsh-subprocess-local/runner` 核心,不改变 CLI 语法,也不增加另一个可执行文件;[原生 containment 参考](../../../../packages/subprocess/subprocess-local/README.zh.md)负责这条私有路径。Linux CI 会在匹配的 manylinux 2.28 容器中重新构建 `pty.node`,因为 legacy deploy 会遗漏这一安装副作用。每个目标都会把对应的原生 `@vscode/ripgrep` 二进制复制到可执行文件旁,作为必需的 `-rg` 伴随文件;pkg 运行时通过 `process.pkg` 选择该伴随文件,普通 Node 执行则直接使用 `@vscode/ripgrep`。macOS 使用对应目标的预构建产物,并额外生成所需的 `-spawn-helper`。部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 提供包目录,再由暂存步骤消除符号链接;只有 pnpm 在载荷根目录记录了相同依赖标识时,暂存步骤才删除对应的嵌套工作区副本,以保留服务单例;关闭工作区提升可排除无关的工作区源码包;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 legacy deploy 不会从注册表解析这些未发布名称。 生产部署允许未使用的工作区补丁,因为开发工具可能不属于运行时闭包;闭包内包的补丁应用失败仍会中止部署。工作区安装继续严格校验未使用的补丁。
|
||||
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-python-runtime-closure deploy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.pnpmfile=scripts/python-runtime-deploy.pnpmfile.mjs --config.link-workspace-packages=true --config.hoist-workspace-packages=false` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`,其中包含作为载体根目录 `runtime-bootstrap.mjs` 的 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) → 将暂存依赖中的符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 拒绝缺失的直接依赖目录,验证已部署的 bootstrap,并注入以该文件为 bin 的 pkg 配置及覆盖动态读取 profile、bundle、前端、preset、原生库与配置文件的 assets → 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea --compress Brotli`,其中 vendor 的 pkg 补丁把 Brotli 质量固定为 6:pkg 不传参数时默认质量 11,同一载荷实测 132.8 s,质量 6 为 3.2 s,产物大 12% → 将 `deepseek-harness-sdk-runtime-<platform>-<arch>` 写入 `dist-exe/`,并将它与轻量资源清单及 Office skill 资源一同拷回运行时目录。该 bootstrap 由 Python runtime 拥有;普通启动时它调用公开 CLI export,而提供方私有选择会分派到同一个 `@deepseek-ai/dsh-subprocess-local/runner` 核心,不改变 CLI 语法,也不增加另一个可执行文件;[原生 containment 参考](../../../../packages/subprocess/subprocess-local/README.zh.md)负责这条私有路径。Linux CI 会在匹配的 manylinux 2.28 容器中重新构建 `pty.node`,并将生成的 addon 复制到部署树中。每个目标都会把对应的原生 `@vscode/ripgrep` 二进制复制到可执行文件旁,作为必需的 `-rg` 伴随文件;pkg 运行时通过 `process.pkg` 选择该伴随文件,普通 Node 执行则直接使用 `@vscode/ripgrep`。macOS 使用对应目标的预构建产物,并额外生成所需的 `-spawn-helper`。部署标志都有实测依据:独立部署锁文件保留源锁文件的版本及对等依赖设置;[`scripts/python-runtime-deploy.pnpmfile.mjs`](../../../../scripts/python-runtime-deploy.pnpmfile.mjs) 保留精确的本地构建审批;[按锁文件部署的决策](../bug-fix/2026-11-01-locked-python-runtime-deploy.zh.md)替代 legacy 部署要求及关闭对等依赖的理由;`hoisted` 提供包目录,再由暂存步骤消除符号链接;只有 pnpm 在载荷根目录记录了相同依赖标识时,暂存步骤才删除对应的嵌套工作区副本,以保留服务单例;关闭工作区提升可排除无关的工作区源码包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 deploy 不会从注册表解析这些未发布名称。 生产部署允许未使用的工作区补丁,因为开发工具可能不属于运行时闭包;闭包内包的补丁应用失败仍会中止部署。工作区安装继续严格校验未使用的补丁。
|
||||
|
||||
CI 使用 [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml):[安装后 wheel 包验证](../testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)在拉取请求上运行 Linux/Windows x64,在 master 推送上运行 Linux ARM64 与两种 macOS 架构。[公开发布工作流](../../archived/process/2026-08-11-python-publication-workflow.md)调用它构建全部五个目标;`workflow_dispatch` 仍可选择部分目标。linux-x64、linux-arm64(`ubuntu-24.04-arm`)、macos-arm64、macos-x64(`macos-15-intel`)与 win-x64(`windows-2025`)分别进行原生构建,并在适用平台缓存 `~/.pkg-cache`;macOS 的 ad-hoc 签名由 pkg 处理。每个平台都把发布形态的 SDK wheel 包与运行时 wheel 包安装到 checkout 外的干净 venv,证明包与可执行文件来源,再通过公开 SDK 与直接 NDJSON JSON-RPC 运行完整 keyless 场景。可信拉取请求与 master 推送还会在各自选定的目标上运行真实 DeepSeek 双轮工具冒烟测试;fork 与 Dependabot head 不会获得密钥。Linux 会检查可执行文件和原生 addon 各自的 GLIBC 依赖,并额外运行 manylinux 2.28 冒烟测试;macOS 则检查 runtime、ripgrep 与 PTY helper 的架构,并验证三个载荷的部署目标都符合 wheel 包标签。完整构建五个目标时保留 6 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 5 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-v<repository-version>` 标签流水线,构建一个 SDK wheel 包和 5 个原生运行时 wheel 包,再由单个串行任务校验并将这 6 个文件发布到项目的 PyPI 注册表。[`python/sdk-runtime` README](../../../../python/sdk-runtime/README.zh.md)负责 Windows 目标及对 Windows arm64 的明确排除。
|
||||
|
||||
|
||||
+14
-14
@@ -3,23 +3,23 @@
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.md
|
||||
/agent-note-adjacent-agents-share-one-steer-send-message-operation:
|
||||
en: 758d31ae04ce0d4f
|
||||
zh: 112475efbd52b9f6
|
||||
en: 9d0af2bae38b1eec
|
||||
zh: 8a6e8502a5195548
|
||||
/agent-note-adjacent-agents-share-one-steer-send-message-operation/problem:
|
||||
en: c988b8a3be4e6f19
|
||||
zh: b58011fd8d2e878e
|
||||
en: 5dc4c218f648c27e
|
||||
zh: b48531fac94b25db
|
||||
/agent-note-adjacent-agents-share-one-steer-send-message-operation/decision:
|
||||
en: 1188c699ce6e6ca2
|
||||
zh: 4d5b974cd1216f06
|
||||
en: f4789878ac0e79b2
|
||||
zh: 68eb68d34cba810d
|
||||
/agent-note-adjacent-agents-share-one-steer-send-message-operation/decision/one-model-tool-and-one-return-instruction:
|
||||
en: 4b0a25389324fa91
|
||||
zh: c2cdd9f7e40ef43b
|
||||
en: 62284bd7dd804188
|
||||
zh: b3a8dfebffc01bdb
|
||||
/agent-note-adjacent-agents-share-one-steer-send-message-operation/decision/complete-removal-and-reintroduction-condition:
|
||||
en: 5a9ec6478cf7430c
|
||||
zh: f4c2f067cc72040e
|
||||
en: 8c6d355d91ec83d2
|
||||
zh: fcc74d1e6ef477bf
|
||||
/agent-note-adjacent-agents-share-one-steer-send-message-operation/alternatives-considered:
|
||||
en: 1f62d5d938144666
|
||||
zh: dec0f30228c5b7a7
|
||||
en: 7386e88e7051dce7
|
||||
zh: dcea5b9e30374be3
|
||||
/agent-note-adjacent-agents-share-one-steer-send-message-operation/consequences:
|
||||
en: d55de74742c3af4b
|
||||
zh: d894f9a33c8e4691
|
||||
en: 9c23dd6ace029ec3
|
||||
zh: f08ef3f142534843
|
||||
+13
-14
@@ -4,26 +4,25 @@ Status: implemented
|
||||
|
||||
English | [中文](2026-08-27-adjacent-agent-steer-messaging.zh.md)
|
||||
|
||||
The adjacency, discovery, and parent-lifetime restrictions below are superseded by [tree-wide communication](2026-10-16-tree-wide-agent-communication.md); the remaining decisions stay active. The tool identity marker and initial-task communication guidance below are superseded by [tool-owned communication](2026-10-22-subagent-tool-owned-communication.md).
|
||||
|
||||
## Problem
|
||||
|
||||
Continuable Agents originally used direction-specific model controls. A parent called `send_message({ subagent_id, message })`, which delegated to a FIFO `followup` service operation. A child instead received a child-scoped `report({ output })` tool, a `tool:report` system-prompt section, and deployment-selected quiet or waking delivery. The tools described one adjacent-Agent operation through different schemas, service paths, source attribution, and scheduling.
|
||||
|
||||
A continuable child owns its own Session, so its parent does not automatically receive the child's transcript, tool output, or reasoning. The return path must therefore remain explicit and repeatable: a child may send progress before it finishes, remain available after sending, or fail before it can cooperate. Turning every final assistant message into an implicit result would conflate turn completion with model-selected communication and would not cover abnormal endings.
|
||||
A continuable child owns its own Session, so its parent does not automatically receive the child's transcript, tool output, or reasoning. Explicit messages let a child send actionable findings before finishing. The runtime separately delivers terminal output in a settlement notice when the direct creator is still online.
|
||||
|
||||
The child-only tool and system-prompt section also preceded every inherited fork turn. They made a continuable fork child's request head differ from its parent's before the history that fork exists to reuse, forcing the provider to prefill the entire copied transcript again.
|
||||
|
||||
## Decision
|
||||
|
||||
`SubagentRuntime.sendMessage(sender, targetId, content, { signal })` is the only public model-authored message operation. The continuation manager accepts only the exact live sender and a target on one adjacent edge:
|
||||
`SubagentRuntime.sendMessage(sender, targetId, content, { signal })` is the only public model-authored message operation. The continuation manager accepts only the exact live sender and a target in the same root tree; [tree-wide communication](2026-10-16-tree-wide-agent-communication.md) owns authorization and discovery.
|
||||
|
||||
- parent to direct continuable child, authorized by the child's durable `SessionHeader.parentSession`;
|
||||
- resident continuable child to its exact live direct parent, authorized by the child's Activation.
|
||||
Self-targets, stale Agent objects, unknown targets, and external executions are not alternate routes. The operation has no caller-supplied source, delivery mode, offline root mailbox, or provider dispatch.
|
||||
|
||||
Siblings, self-targets, ancestors beyond one edge, stale Agent objects, unknown targets, and one-shot children are not alternate routes. The operation has no caller-supplied source, delivery mode, offline parent mailbox, or provider dispatch.
|
||||
Every accepted message uses `Agent.steer()`. A running target receives it at the nearest step boundary; an idle target starts a turn. An absent target is cold-resumed under the root before the same Steer delivery; its creation ancestors can remain offline. The manager retains waking-send accounting so a continuation-managed target cannot settle between synchronous inbox insertion and driver admission.
|
||||
|
||||
Every accepted message uses `Agent.steer()`. A running target receives it at the nearest step boundary; an idle target starts a turn. An absent direct child is cold-resumed through the existing continuation lifecycle before the same Steer delivery. The manager retains waking-send accounting so a continuation-managed target cannot settle between synchronous inbox insertion and driver admission.
|
||||
|
||||
Every direction uses one durable source. The service derives `senderSessionId` from the authorized Agent and frames the model-visible content as `Agent <sender-id> sent a message:`, so attribution cannot diverge from authority.
|
||||
Every direction uses one durable source. The service derives `senderSessionId` from the exact live sender and frames content as `Agent <sender-id> sent a message:`.
|
||||
|
||||
```ts
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||
@@ -46,15 +45,15 @@ interface SendMessageInput {
|
||||
}
|
||||
```
|
||||
|
||||
Parents and children inherit the same definition in the same registry order. The standard definition carries a process-stable internal identity that a scoped same-name tool does not satisfy. An independently selected preset may omit the tool, a historical descriptor filter may remove it on resume, and a scoped replacement may provide different semantics; neither case receives the standard call instruction. When the standard tool remains visible, the continuation manager appends the JSON-encoded direct parent id and the instruction to send one self-contained result before finishing, plus earlier actionable findings, to the child's initial user task. For a fork child this task follows the inherited completed-turn prefix; no child-only system-prompt section or tool schema precedes that prefix.
|
||||
Parents and children inherit the same definition in the same registry order. The standard definition carries a process-stable internal identity that a scoped same-name tool does not satisfy. An independently selected preset may omit the tool, a historical descriptor filter may remove it on resume, and a scoped replacement may provide different semantics; none receives the standard call instruction. When the standard tool remains visible, the continuation manager appends the JSON-encoded direct parent id, automatic final-answer delivery conditions, and guidance to send earlier actionable findings to the child's initial user task. For a fork child this task follows the inherited completed-turn prefix; no child-only system-prompt section or tool schema precedes that prefix.
|
||||
|
||||
The instruction is guidance, not settlement enforcement. Sending does not end the child's turn, zero or several calls remain mechanically valid, and the runtime never rejects a child for staying silent. The manager-owned `subagent-settled` notice remains unconditional and separately attributed because it records how an Activation ended and preserves terminal output when the child cannot cooperate.
|
||||
The instruction is guidance, not settlement enforcement. Sending does not end the child's turn, zero or several calls remain mechanically valid, and the runtime never rejects a child for staying silent. The separately attributed `subagent-settled` notice records the Activation's outcome and terminal output only when its direct creator is still online and delivery is `parent`.
|
||||
|
||||
Human browser prompts are not model-authored Agent messages. The remote prompt path keeps a private Queue delivery so each human prompt remains a distinct turn. Interrupt behavior and settlement delivery remain independent.
|
||||
|
||||
### Complete removal and reintroduction condition
|
||||
|
||||
The standalone `@deepseek-ai/dsh-tool-subagent-report` package, `report` schema, `tool:report` prompt section, `reportDelivery` configuration, report-specific message source, catalog entries, composition rows, and supported-behavior snapshots are absent. The unified tool gives up the recipient-free child shortcut and the old ability for a structural return tool to survive an explicit child allow-list. Those capabilities return only if a concrete use case requires semantics that an adjacent `agent_id` and fixed Steer cannot express; reintroducing them requires a distinct model operation and prefix-cost evidence, not an alias over `sendMessage()`.
|
||||
The standalone `@deepseek-ai/dsh-tool-subagent-report` package, `report` schema, `tool:report` prompt section, `reportDelivery` configuration, report-specific message source, catalog entries, composition rows, and supported-behavior snapshots are absent. The unified tool gives up the recipient-free child shortcut and the old ability for a structural return tool to survive an explicit child allow-list. Those capabilities return only if a concrete use case requires semantics that a tree member’s `agent_id` and fixed Steer cannot express; reintroducing them requires a distinct model operation and prefix-cost evidence, not an alias over `sendMessage()`.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -64,16 +63,16 @@ The standalone `@deepseek-ai/dsh-tool-subagent-report` package, `report` schema,
|
||||
|
||||
**Make `report` global.** Roots, one-shot children, remote children, and agentless callers cannot derive a report recipient. Advertising it globally would make schema visibility disagree with authority, while `send_message` already makes the recipient explicit.
|
||||
|
||||
**Turn every child final message into an implicit send.** A long-lived child may have nothing useful to send in one turn and several findings in another. Automatic delivery would merge model-authored communication with the runtime's settlement account and could not replace the unconditional notice on errors, cancellation, or token exhaustion.
|
||||
**Treat the final answer as a model-authored send.** A settlement notice must preserve terminal output and abnormal endings even when the child cannot cooperate. It keeps runtime attribution distinct from findings the child explicitly chose to send.
|
||||
|
||||
**Rely only on the tool description.** A tool description helps after the model considers that tool; the failure mode is a child that believes it is finished without considering any return call. Initial-task guidance reaches that decision without changing the inherited system or tool prefix.
|
||||
**Rely only on the tool description.** A tool description helps after the model considers that tool; initial-task guidance also explains which earlier findings should reach the parent and what the final answer must contain, without changing the inherited system or tool prefix.
|
||||
|
||||
**Keep quiet delivery as deployment policy.** A quiet model-authored message can be accepted while an idle target never reads it. Fixed Steer gives both directions one delivery meaning and preserves accepted order with later settlement notices.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Model consumers expose one `send_message({ agent_id, message })` definition to parents and children, with no model-selected Queue versus Steer parameter.
|
||||
- The continuation manager remains the sole owner of adjacency authorization, residency, cold resume, waking admission, and teardown races.
|
||||
- The continuation manager remains the sole owner of tree authorization, residency, cold resume, waking admission, and teardown races.
|
||||
- Accepted messages may extend a running target's current turn; messages waiting together share next-step FIFO ordering.
|
||||
- Caller cancellation owns work only until inbox acceptance and does not retract an accepted message or dispose the target.
|
||||
- The initial task carries JSON-encoded dynamic parent addressing after a fork prefix, while the request-head system prompt and tool ordering remain reusable.
|
||||
|
||||
+13
-14
@@ -4,26 +4,25 @@ Status: implemented
|
||||
|
||||
[English](2026-08-27-adjacent-agent-steer-messaging.md) | 中文
|
||||
|
||||
邻接消息、发现范围和父子生命周期限制已由[整树通信](2026-10-16-tree-wide-agent-communication.zh.md)替代;其余决策仍然有效。 下文的工具身份标记和初始任务通信指导由[工具拥有通信指导](2026-10-22-subagent-tool-owned-communication.zh.md)取代。
|
||||
|
||||
## 问题
|
||||
|
||||
可继续 Agent 最初使用方向专属的模型控制。parent 调用 `send_message({ subagent_id, message })`,委托给 FIFO `followup` 服务操作。child 则获得 child 作用域的 `report({ output })` 工具、`tool:report` 系统提示词 section,以及由部署选择的静默或唤醒投递。两个工具用不同 schema、服务路径、来源与调度描述同一个相邻 Agent 操作。
|
||||
|
||||
可继续 child 拥有自己的 Session,因此 parent 不会自动收到 child 的 transcript(文本记录)、工具输出或推理。返回路径必须保持显式且可重复:child 可以在结束前发送进度、发送后仍保持可用,也可能在来得及配合前失败。把每条最终 assistant 消息变成隐式结果会混淆轮次完成与模型选择的通信,而且无法覆盖异常结束。
|
||||
可继续 child 拥有自己的 Session,因此 parent 不会自动收到 child 的 transcript(文本记录)、工具输出或推理。显式消息让 child 在结束前发送可操作的发现。直接创建者仍在线时,runtime 另行通过结算通知投递终态输出。
|
||||
|
||||
child 专属工具与系统提示词 section 还位于每个继承 fork 轮次之前。它们让可继续 fork child 的请求头在 fork 旨在复用的历史之前就与 parent 不同,迫使提供方重新预填充整份复制 transcript。
|
||||
|
||||
## 决策
|
||||
|
||||
`SubagentRuntime.sendMessage(sender, targetId, content, { signal })` 是唯一公开的模型编写消息操作。继续执行管理器只接受确切在线 sender 与一条相邻边上的目标:
|
||||
`SubagentRuntime.sendMessage(sender, targetId, content, { signal })` 是唯一公开的模型编写消息操作。继续执行管理器只接受确切在线 sender 与同一根树中的目标;[整树通信](2026-10-16-tree-wide-agent-communication.zh.md)负责授权与发现规则。
|
||||
|
||||
- parent 到直接可继续 child,由 child 的持久化 `SessionHeader.parentSession` 授权;
|
||||
- 驻留的可继续 child 到其确切在线直接 parent,由 child 的 Activation 授权。
|
||||
自身目标、过期 Agent 对象、未知目标和外部执行不是替代路由。操作不接受调用方提供的 source、投递模式、离线根邮箱或提供方分发。
|
||||
|
||||
sibling、自身目标、超过一条边的 ancestor、陈旧 Agent 对象、未知目标与一次性 child 都不是替代路由。该操作没有调用方提供的 source、投递模式、离线 parent mailbox 或提供方分发。
|
||||
每条被接受的消息都使用 `Agent.steer()`。运行中目标在最近 step 边界接收消息;空闲目标启动轮次。不在线目标先在根下冷恢复,再接受同一 Steer 投递;其创建祖先可以保持离线。管理器保留唤醒发送记账,因此受继续执行管理的目标不会在同步 inbox 插入与 driver 准入之间结算。
|
||||
|
||||
每条被接受的消息都使用 `Agent.steer()`。运行中目标在最近 step 边界接收消息;空闲目标启动轮次。缺失的直接 child 会先通过现有继续执行生命周期冷恢复,再接受同一 Steer 投递。管理器保留唤醒发送记账,因此受继续执行管理的目标不会在同步 inbox 插入与 driver 准入之间结算。
|
||||
|
||||
两个方向使用同一种持久来源。服务从已授权 Agent 推导 `senderSessionId`,并把模型可见内容组装为 `Agent <sender-id> sent a message:`,因此来源信息不会偏离权限。
|
||||
所有方向使用同一种持久 source。服务从确切在线发送方推导 `senderSessionId`,并以 `Agent <sender-id> sent a message:` 包装内容。
|
||||
|
||||
```ts
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||
@@ -46,15 +45,15 @@ interface SendMessageInput {
|
||||
}
|
||||
```
|
||||
|
||||
parent 与 child 以相同注册表顺序继承相同定义。标准定义携带进程稳定的内部身份,同名的作用域工具不满足该身份。独立选择的 preset 可以省略该工具,历史 descriptor 过滤器可在恢复时移除该工具,作用域替代工具也可以提供不同语义;两种情况都不会收到标准调用指令。当标准工具仍可见时,继续执行管理器会把经过 JSON 编码的直接 parent id、结束前发送一份自包含结果的指令,以及更早发送可操作发现的指令追加到 child 初始用户任务。对 fork child 而言,该任务位于继承的已完成轮次前缀之后;没有 child 专属系统提示词 section 或工具 schema 位于此前缀之前。
|
||||
parent 与 child 以相同注册表顺序继承相同定义。标准定义携带进程稳定的内部身份,同名的作用域工具不满足该身份。独立选择的 preset 可以省略该工具,历史 descriptor 过滤器可在恢复时移除该工具,作用域替代工具也可以提供不同语义;这些情况都不会收到标准调用指令。当标准工具仍可见时,继续执行管理器会把经过 JSON 编码的直接 parent id、最终回答的自动投递条件,以及提前发送可操作发现的指导追加到 child 初始用户任务。对 fork child 而言,该任务位于继承的已完成轮次前缀之后;没有 child 专属系统提示词 section 或工具 schema 位于此前缀之前。
|
||||
|
||||
该指令是指导,不是结算强制。发送不会结束 child 轮次,机制仍允许零次或多次调用,runtime 绝不会因 child 保持沉默而拒绝它。由管理器负责的 `subagent-settled` 通知仍无条件发送并采用独立来源,因为它记录 Activation 如何结束,并在 child 无法配合时保留终态输出。
|
||||
该指令是指导,不是结算强制。发送不会结束 child 轮次,机制仍允许零次或多次调用,runtime 绝不会因 child 保持沉默而拒绝它。采用独立来源的 `subagent-settled` 通知仅在直接创建者仍在线且投递方式为 `parent` 时,记录 Activation 的结果与终态输出。
|
||||
|
||||
浏览器中的人类提示不是模型编写的 Agent 消息。远程提示路径保留私有 Queue 投递,使每条人类提示保持为独立轮次。中断行为与结算投递保持独立。
|
||||
|
||||
### 完整移除与重新引入条件
|
||||
|
||||
独立的 `@deepseek-ai/dsh-tool-subagent-report` 包、`report` schema、`tool:report` 提示词 section、`reportDelivery` 配置、report 专属消息来源、目录项、组合行和受支持行为快照均已不存在。统一工具放弃了无需接收方的 child 快捷方式,也放弃了让结构性返回工具绕过显式 child allow-list 的旧能力。只有具体用例需要相邻 `agent_id` 与固定 Steer 无法表达的语义时,这些能力才会重新出现;重新引入需要独立的模型操作与前缀成本证据,而非 `sendMessage()` 之上的别名。
|
||||
独立的 `@deepseek-ai/dsh-tool-subagent-report` 包、`report` schema、`tool:report` 提示词 section、`reportDelivery` 配置、report 专属消息来源、目录项、组合行和受支持行为快照均已不存在。统一工具放弃了无需接收方的 child 快捷方式,也放弃了让结构性返回工具绕过显式 child allow-list 的旧能力。只有具体用例需要树成员的 `agent_id` 与固定 Steer 无法表达的语义时,这些能力才会重新出现;重新引入需要独立的模型操作与前缀成本证据,而非 `sendMessage()` 之上的别名。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
@@ -64,16 +63,16 @@ parent 与 child 以相同注册表顺序继承相同定义。标准定义携带
|
||||
|
||||
**让 `report` 全局可见。** 根 Agent、一次性 child、远程 child 与无 Agent 调用方无法推导 report 接收方。全局宣传它会让 schema 可见性与权限不一致,而 `send_message` 已显式给出接收方。
|
||||
|
||||
**把每条 child 最终消息变成隐式发送。** 长期运行的 child 可能在某个轮次没有值得发送的内容,在另一个轮次却有多条发现。自动投递会混合模型编写通信与 runtime 结算说明,而且无法替代错误、取消或 token 耗尽时的无条件通知。
|
||||
**将最终回答当作模型编写的发送。** 即使 child 无法配合,结算通知仍须保留终态输出与异常结束信息。它将 runtime 来源与 child 显式选择发送的发现区分开。
|
||||
|
||||
**只依赖工具描述。** 工具描述会在模型考虑该工具后提供帮助;失败模式是 child 认为自己已经完成而根本没有考虑返回调用。初始任务指导能触及该决策,又不会改变继承的系统或工具前缀。
|
||||
**只依赖工具描述。** 工具描述会在模型考虑该工具后提供帮助;初始任务指导还说明哪些早期发现应发送给 parent,以及最终回答须包含什么,又不会改变继承的系统或工具前缀。
|
||||
|
||||
**保留静默投递作为部署策略。** 静默的模型编写消息可能被接受,但空闲目标永远不会读取它。固定 Steer 为两个方向提供一种投递含义,并保持与后续结算通知的接受顺序。
|
||||
|
||||
## 后果
|
||||
|
||||
- 模型 Consumer 向 parent 与 child 公开一个 `send_message({ agent_id, message })` 定义,不提供模型选择的 Queue 与 Steer 参数。
|
||||
- 继续执行管理器仍是相邻关系授权、驻留、冷恢复、唤醒准入与拆卸竞态的唯一所有者。
|
||||
- 继续执行管理器仍是整树授权、驻留、冷恢复、唤醒准入与拆卸竞态的唯一所有者。
|
||||
- 被接受的消息可以延长运行中目标的当前轮次;一起等待的消息共享 next-step FIFO 顺序。
|
||||
- 调用方取消只在 inbox 接受前掌管工作,不会撤回已接受消息或 dispose(资源释放)目标。
|
||||
- 初始任务在 fork 前缀之后携带经过 JSON 编码的动态 parent 地址,而请求头系统提示词与工具顺序保持可复用。
|
||||
|
||||
+8
-8
@@ -9,14 +9,14 @@
|
||||
en: 98b49b900b8d2f84
|
||||
zh: 557faac2976592be
|
||||
/agent-note-explicit-agent-identity-at-runtime-boundaries/decision:
|
||||
en: 1c628cf9c743c639
|
||||
zh: d969b2475fe4a155
|
||||
en: 4a3ab5108f37520f
|
||||
zh: d13fcd448101a95d
|
||||
/agent-note-explicit-agent-identity-at-runtime-boundaries/verification:
|
||||
en: 85130faaa171036b
|
||||
zh: 06e5e9fab32dbfb4
|
||||
en: ce1733edf03f0387
|
||||
zh: 8e1ca2b69e81968e
|
||||
/agent-note-explicit-agent-identity-at-runtime-boundaries/alternatives-considered:
|
||||
en: 2c41456509d54d21
|
||||
zh: 5704ee9a2274e300
|
||||
en: 34f5f8571b21897c
|
||||
zh: cf16094bc0027ca1
|
||||
/agent-note-explicit-agent-identity-at-runtime-boundaries/consequences:
|
||||
en: 08ab86b89fc3ec6f
|
||||
zh: 3866e6557858543a
|
||||
en: 0b5914067f14282e
|
||||
zh: c41d21aeb28e357f
|
||||
+8
-8
@@ -14,34 +14,34 @@ Without an explicit owner, `SubagentManager` creates and resumes children throug
|
||||
|
||||
## Decision
|
||||
|
||||
Runtime interfaces carry Agent identity at the point that owns it. `AgentSetup` receives `(agentCtx, agent)`; Agent creation and resume options carry `parentAgent` for a runtime child; scoped events carry their Agent in the payload; Remote forwarding verifies that `request.agent` is the carrier key; and Host Typert Context resolution maps wire identity to a live Agent Context without a reverse scan. `agent.ctx` remains the registration and lifecycle owner and exposes no reverse Agent property.
|
||||
Runtime interfaces carry Agent identity at the point that owns it. `AgentSetup` receives `(agentCtx, agent)`; each subagent Activation retains its exact runtime root; scoped events carry their Agent in the payload; Remote forwarding verifies that `request.agent` is the carrier key; and Host Typert Context resolution maps wire identity to a live Agent Context without a reverse scan. `agent.ctx` remains the registration and lifecycle owner and exposes no reverse Agent property.
|
||||
|
||||
Scope-aware registries continue to use the opaque scope key only for registration membership. Tool-subagent does not classify that key or resolve an Agent from Context. A direct `AgentSetup` passes the unpublished Session explicitly and installs through the supplied Context before publication. For a settings-backed standing preset, the event payload supplies the Agent, its Session supplies the policy target, and its Context owns the registrations.
|
||||
Scope-aware registries continue to use the opaque scope key only for registration membership. Tool-subagent does not classify that key or resolve an Agent from Context. A direct `AgentSetup` receives the Agent explicitly and installs through the supplied Context before Agent publication. For a settings-backed standing preset, the event payload supplies the Agent, its Session supplies the policy target, and its Context owns the registrations.
|
||||
|
||||
`SubagentManager` puts the exact parent in both fresh-creation and cold-resume options. A live continuable child is therefore excluded from `AgentRegistry.roots()` and satisfies `isOwnedBy(child.id, parent)`. Durable `parentSession` metadata does not substitute for this relation: a fork or resumed Session may be a runtime root when no live Agent owns it.
|
||||
`SubagentManager.rootOf(agent)` reads runtime membership from existing Activations; both the child and root must match their live registry objects. Ordinary Sessions resolve to themselves, and unmanaged subagent headers grant no membership. Durable `parentSession` describes creation edges; an Activation retains the exact root while intermediate creators are offline, and same-id replacements cannot inherit it. Root-only consumers classify the explicit subagent origin.
|
||||
|
||||
The [scope reference](../../../../packages/core/scope/README.md) and [Agent loop reference](../../../../packages/core/agent-loop/README.md) document registration and lifecycle behavior. The [initiator-scope decision](2026-07-15-agent-initiator-scope.md) owns private-chain rationale; explicit runtime ownership does not change causal initiation or registration lifetime.
|
||||
|
||||
## Verification
|
||||
|
||||
Agent creation tests pin explicit root and child ownership. Continuation integration tests keep a real child live long enough to assert both `roots()` exclusion and `isOwnedBy()` membership. Existing Schedule tests verify that root-only registrations stay absent from an explicitly owned child.
|
||||
Managed-child tests verify intermediate-creator disposal and exact-instance checks after child or root replacement. SDK approval tests verify routing during a turn after the direct creator exits. Schedule and user-question tests reject delegated Sessions while ordinary historical forks remain independent.
|
||||
|
||||
Remote-event tests reject a missing or mismatched Agent before forwarding a scoped waterfall. Tool-subagent tests verify that direct setup installs before Session publication; standing-preset tests verify per-Session policy sampling and inheritance.
|
||||
Remote-event tests reject a missing or mismatched Agent before forwarding a scoped waterfall. Tool-subagent tests verify that direct setup installs before Agent publication; standing-preset tests verify per-Session policy sampling and inheritance.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep `Context.agent`.** A reverse accessor makes registration ownership look like operation identity and requires every Context derivation, adapter, and test double to preserve an association unrelated to Cordis service selection or effect cleanup.
|
||||
|
||||
**Infer runtime ownership from the caller Context.** A private manager Context, an Agent Context, and a standing preset Context can all call the same factory. Context ancestry therefore does not state which live Agent owns the result; the creator must put the parent it already knows in the request options.
|
||||
**Infer runtime ownership from the caller Context.** A private manager Context, an Agent Context, and a standing preset Context can all call the same factory. Context ancestry therefore does not state which live Agent owns the result; the subagent Activation retains the exact root supplied at admission.
|
||||
|
||||
**Classify Agent scope keys.** An opaque scope key states routing membership, not domain identity. Classifying it would make Agent the center of composition and would still couple a plugin's effect owner to the Session whose policy it needs.
|
||||
|
||||
**Use the initiating Agent as creation ownership.** Initiator scope records causal asynchronous execution, not lifetime ownership. A parent may initiate work that intentionally creates a root, and setup remains outside the child's driver boundary.
|
||||
|
||||
**Use durable Session lineage.** `parentSession` records conversation ancestry across process lifetimes. Runtime ownership controls live roots and teardown, so equating the two would prevent a legitimately resumed fork from becoming a top-level Agent.
|
||||
**Use durable Session lineage.** `parentSession` records creation ancestry across process lifetimes. An intermediate creator can be offline while its descendant remains managed by a live root; durable ids also cannot distinguish replaced Agent instances. Historical ordinary forks may retain parent metadata without being delegated.
|
||||
|
||||
## Consequences
|
||||
|
||||
Lifecycle options, events, service requests, and transport requests carry explicit Agent identities, so each operation states the identity it uses and TypeScript checks both sides. Context remains reusable for dependency access and effect ownership without becoming an alternate domain-object locator.
|
||||
|
||||
Continuable children have the same runtime parent relation as one-shot in-process children. Root-only consumers exclude them, parent teardown can reason from one live ownership graph, and durable lineage remains free to describe history rather than process-local lifetime.
|
||||
Continuable children have one runtime root independent of creator residency. The subagent manager owns tree teardown and local membership; the core factory and registry retain only instance creation, publication, and disposal. Third-party callers create managed children through the subagent service rather than assigning ownership to arbitrary factory-created Agents.
|
||||
+8
-8
@@ -14,34 +14,34 @@ Agent 的 Cordis Context 拥有注册及其清理。Agent 身份则为某项操
|
||||
|
||||
## 决策
|
||||
|
||||
运行时接口在拥有身份的位置携带 Agent 身份。`AgentSetup` 接收 `(agentCtx, agent)`;创建与恢复 Agent 的 options 通过 `parentAgent` 标识运行时子级;作用域事件在 payload 中携带 Agent;Remote 转发校验 `request.agent` 就是 carrier key;Host Typert Context 解析则把协议身份映射到存活 Agent Context,不执行反向扫描。`agent.ctx` 继续拥有注册和生命周期,不暴露反向 Agent 属性。
|
||||
运行时接口在拥有身份的位置携带 Agent 身份。`AgentSetup` 接收 `(agentCtx, agent)`;每个 subagent Activation 保留其确切运行时树根;作用域事件在 payload 中携带 Agent;Remote 转发校验 `request.agent` 就是 carrier key;Host Typert Context 解析则把协议身份映射到存活 Agent Context,不执行反向扫描。`agent.ctx` 继续拥有注册和生命周期,不暴露反向 Agent 属性。
|
||||
|
||||
感知作用域的注册表继续仅使用不透明作用域键判断注册成员关系。tool-subagent 不会分类该键,也不会从 Context 解析 Agent。直接 `AgentSetup` 显式传入尚未发布的 Session,并在发布前通过所给 Context 完成安装。对于由设置控制的常驻 preset,事件 payload 提供 Agent,其 Session 提供策略目标,其 Context 拥有注册项。
|
||||
感知作用域的注册表继续仅使用不透明作用域键判断注册成员关系。tool-subagent 不会分类该键,也不会从 Context 解析 Agent。直接 `AgentSetup` 显式接收 Agent,并在 Agent 发布前通过所给 Context 完成安装。对于由设置控制的常驻 preset,事件 payload 提供 Agent,其 Session 提供策略目标,其 Context 拥有注册项。
|
||||
|
||||
`SubagentManager` 会把确切父级放进全新创建与冷恢复的 options。因此,存活的可续跑子级不会出现在 `AgentRegistry.roots()` 中,并且满足 `isOwnedBy(child.id, parent)`。持久化 `parentSession` 元数据不能代替这项关系:没有存活 Agent 拥有 fork 或已恢复会话时,它仍可成为 runtime root。
|
||||
`SubagentManager.rootOf(agent)` 从已有 Activation 读取运行时成员关系;子级和树根都必须匹配注册表中的存活对象。普通 Session 返回自身,非托管的 subagent header 不授予成员身份。持久化 `parentSession` 描述创建关系;中间创建者离线时,Activation 保留确切树根,同 id 替代实例不能继承它。仅限根级的消费方依据显式 subagent 来源分类。
|
||||
|
||||
[作用域参考](../../../../packages/core/scope/README.zh.md)与 [Agent loop 参考](../../../../packages/core/agent-loop/README.zh.md)说明注册和生命周期行为。[发起方作用域决策](2026-07-15-agent-initiator-scope.zh.md)负责私有调用链理由;显式运行时归属不改变因果发起关系或注册生命周期。
|
||||
|
||||
## 验证
|
||||
|
||||
Agent 创建测试锁定显式的根级与子级归属。continuation 集成测试让一个真实子级保持存活,直到断言其既不属于 `roots()`、又满足 `isOwnedBy()`。现有 Schedule 测试验证仅限根级的注册项不会出现在显式归属的子级中。
|
||||
托管子级测试验证中间创建者释放,以及子级或树根替换后的确切实例检查。SDK 审批测试验证直接创建者退出后的轮次内路由。Schedule 与用户问题测试拒绝委派 Session,历史普通 fork 则保持独立。
|
||||
|
||||
Remote 事件测试会在转发作用域 waterfall 前拒绝缺失或不匹配的 Agent。tool-subagent 测试验证 direct setup 会在 Session 发布前完成安装;常驻 preset 测试验证逐 Session 的策略读取与继承。
|
||||
Remote 事件测试会在转发作用域 waterfall 前拒绝缺失或不匹配的 Agent。tool-subagent 测试验证 direct setup 会在 Agent 发布前完成安装;常驻 preset 测试验证逐 Session 的策略读取与继承。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**保留 `Context.agent`。** 反向 accessor 会让注册所有权看起来等同于操作身份,还要求每个 Context 派生、适配器和测试替身保留一项与 Cordis 服务选择或 effect 清理无关的关联。
|
||||
|
||||
**从调用方 Context 推断运行时归属。** 私有管理器 Context、Agent Context 和常驻 preset Context 都能调用同一个工厂。因此,Context 祖先关系无法说明由哪个存活 Agent 拥有结果;创建方必须把它已知的父级放进请求 options。
|
||||
**从调用方 Context 推断运行时归属。** 私有管理器 Context、Agent Context 和常驻 preset Context 都能调用同一个工厂。因此,Context 祖先关系无法说明由哪个存活 Agent 拥有结果;subagent Activation 保留准入时传入的确切树根。
|
||||
|
||||
**分类 Agent 作用域键。** 不透明作用域键表达路由成员关系,而不是领域身份。分类该键会让 Agent 成为组合中心,也仍会把插件的 effect 所有者与策略所需的 Session 耦合起来。
|
||||
|
||||
**使用发起 Agent 作为创建归属。** 发起方作用域记录异步执行的因果关系,而非生命周期归属。父级可能发起有意创建根级 Agent 的工作,而 setup 仍位于子级驱动边界之外。
|
||||
|
||||
**使用持久化会话谱系。** `parentSession` 跨进程生命周期记录对话祖先关系。运行时归属控制存活根级和 teardown,因此把二者等同会阻止合法恢复的 fork 成为顶层 Agent。
|
||||
**使用持久化会话谱系。** `parentSession` 跨进程生命周期记录创建祖先关系。中间创建者可以离线,而后代仍由存活树根管理;持久 id 也无法区分替换后的 Agent 实例。历史普通 fork 可能保留父级元数据,但并不属于委派。
|
||||
|
||||
## 后果
|
||||
|
||||
生命周期 options、事件、服务请求和传输请求会携带显式 Agent 身份,因此每项操作都会声明自身使用的身份,TypeScript 也会检查两侧。Context 可以继续复用于依赖访问与 effect 所有权,而不会成为另一种领域对象定位器。
|
||||
|
||||
可续跑子级与一次性进程内子级使用同一种运行时父级关系。仅限根级的消费方会排除这些子级,父级 teardown 可以依据唯一的存活归属图推理,而持久化谱系仍可描述历史,不必承担进程内生命周期语义。
|
||||
可续跑子级拥有独立于创建者驻留状态的唯一运行时树根。subagent 管理器拥有整树 teardown 和本地成员关系;核心工厂与注册表仅保留实例创建、发布和释放。第三方调用方通过 subagent 服务创建托管子级,而非给任意工厂创建的 Agent 指定归属。
|
||||
+2
-2
@@ -9,8 +9,8 @@
|
||||
en: 386864a282e5ad58
|
||||
zh: a4ea75ec70eb7778
|
||||
/agent-note-shared-native-cua-process-with-agent-scoped-state/decision:
|
||||
en: 11bab6870c3d802f
|
||||
zh: a22114f2217a3a41
|
||||
en: 60ec2276ed0bff33
|
||||
zh: 5fb48cc787194254
|
||||
/agent-note-shared-native-cua-process-with-agent-scoped-state/alternatives-considered:
|
||||
en: 883f71387f599160
|
||||
zh: 4b5f1450be799397
|
||||
|
||||
@@ -12,7 +12,7 @@ On macOS 26.6.2, replayd derives capture-client identity from the executable pat
|
||||
|
||||
One user shares a native service per executable content digest across Agents and Hosts. A version lock elects the service; an additional canonical-executable-path lock prevents different builds published at one path from running together. Locks remain held until process exit. The managed startup command hands service lifetime to its client connections, and the last connection's completed cleanup ends the service. The diagnostic stdin/stdout entry proxies to this same service.
|
||||
|
||||
The service runs as a signed application launched through LaunchServices. Directly spawning the executable inherits its launcher’s TCC responsibility, so it cannot give the helper independent system permissions. A launch lock serializes discovery and startup. Complete signed applications use immutable content-addressed directories: replacing an executable used by a live process would violate both path-lock ownership and code-signature integrity. Ad-hoc builds can require fresh grants when their code hash changes; a stable bundle identifier alone does not preserve those grants. Installed packages retain their distributed signatures.
|
||||
The service runs as a signed application launched through LaunchServices. Directly spawning the executable inherits its launcher’s TCC responsibility, so it cannot give the helper independent system permissions. A launch lock serializes discovery and startup. Complete signed applications use immutable content-addressed directories: replacing an executable used by a live process would violate both path-lock ownership and code-signature integrity. Ad-hoc builds can require fresh grants when their code hash changes; a stable bundle identifier alone does not preserve those grants. Source checkouts can keep grants with the [development signing identity](2026-10-29-cua-dev-signing-identity.md). Installed packages retain their distributed signatures.
|
||||
|
||||
Each Agent connection owns its application handles, AX snapshots, limits and cancellation state. An app handle grants no exclusive use of the application. A FIFO orders complete desktop operations, including clipboard transactions, while response writes are independent. Agent disposal retires only its connection. The [object API decision](2026-09-17-experimental-cua-object-api.md) retains evaluator independence and coordinate semantics; the [consent decision](2026-09-26-cua-application-consent.md) retains application authorization.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
同一用户的多个 Agent 和 Host 按可执行文件内容摘要共享原生服务。版本锁选出服务实例,规范化可执行路径上的另一把锁阻止发布在同一路径的不同构建同时运行。进程退出才释放锁。受管启动命令将服务生命期交给客户端连接,最后一条连接清理完成后服务退出。诊断用 stdin/stdout 入口也代理到同一个服务。
|
||||
|
||||
服务通过 LaunchServices 以已签名应用运行。直接启动可执行文件会继承启动器的 TCC 责任,无法让 helper 独立取得系统权限。启动锁串行处理发现与启动。完整签名应用保存在不可变的内容寻址目录中,替换运行中进程的可执行文件会破坏路径锁的归属与代码签名完整性。临时签名构建的代码哈希变化后可能需要重新授权,固定 Bundle ID 不能保证保留授权。安装版保留分发时的签名。
|
||||
服务通过 LaunchServices 以已签名应用运行。直接启动可执行文件会继承启动器的 TCC 责任,无法让 helper 独立取得系统权限。启动锁串行处理发现与启动。完整签名应用保存在不可变的内容寻址目录中,替换运行中进程的可执行文件会破坏路径锁的归属与代码签名完整性。临时签名构建的代码哈希变化后可能需要重新授权,固定 Bundle ID 不能保证保留授权。源码检出可以用[开发签名身份](2026-10-29-cua-dev-signing-identity.zh.md)保留授权。安装版保留分发时的签名。
|
||||
|
||||
每条 Agent 连接独立持有应用句柄、AX 快照、资源限制和取消状态。持有 app 句柄不代表独占应用。FIFO 队列串行执行包含剪贴板事务的完整桌面操作,响应写入独立进行。Agent 销毁只释放自己的连接。[对象接口决策](2026-09-17-experimental-cua-object-api.zh.md)继续负责执行器独立性及坐标语义,[授权决策](2026-09-26-cua-application-consent.zh.md)继续负责应用许可。
|
||||
|
||||
|
||||
+13
-13
@@ -2,18 +2,18 @@
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.md
|
||||
/agent-note-team-identity-in-required-runtime-context:
|
||||
en: 8c88759320ae8481
|
||||
zh: 4068b18723907eb9
|
||||
/agent-note-team-identity-in-required-runtime-context/problem:
|
||||
en: c9a81cb731ddf7b2
|
||||
zh: 428c0e6f8937d7c5
|
||||
/agent-note-team-identity-in-required-runtime-context/decision:
|
||||
en: 6c73479973c1e378
|
||||
zh: 98677c77f77f8d81
|
||||
/agent-note-team-identity-in-required-runtime-context/alternatives-considered:
|
||||
/agent-note-subagent-identity-in-required-runtime-context:
|
||||
en: ce8f378b6e7b57fa
|
||||
zh: 26bd0415b4dba94a
|
||||
/agent-note-subagent-identity-in-required-runtime-context/problem:
|
||||
en: f944254565541454
|
||||
zh: 27fa9f67c671dec7
|
||||
/agent-note-subagent-identity-in-required-runtime-context/decision:
|
||||
en: 85100936f97a139c
|
||||
zh: 614081943944e28b
|
||||
/agent-note-subagent-identity-in-required-runtime-context/alternatives-considered:
|
||||
en: 774566ffbf9e9db8
|
||||
zh: ea761d876af462f2
|
||||
/agent-note-team-identity-in-required-runtime-context/consequences:
|
||||
en: 18d1aa67936f1227
|
||||
zh: 7e06c1f51553ecdd
|
||||
/agent-note-subagent-identity-in-required-runtime-context/consequences:
|
||||
en: 2c01a24cd8cc4556
|
||||
zh: 37b5d1ca1173318a
|
||||
@@ -1,4 +1,4 @@
|
||||
# Agent Note: Team identity in required runtime context
|
||||
# Agent Note: Subagent identity in required runtime context
|
||||
|
||||
Status: implemented
|
||||
|
||||
@@ -6,15 +6,15 @@ English | [中文](2026-10-13-team-identity-runtime-context.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A teammate identity embedded in its initial task depends on ordinary history retention. Compaction can remove it, and an ordinary fork from that teammate can retain the old role even though the fork is an independent Lead. Identity must reflect current membership while keeping shared system instructions uniform.
|
||||
A teammate identity embedded in its initial task depends on ordinary history retention. Compaction can remove it, and an ordinary fork from that teammate can retain the old role even though the fork is an independent Lead. Identity must reflect the current subagent tree while keeping shared system instructions uniform.
|
||||
|
||||
## Decision
|
||||
|
||||
`tool-agent-team` contributes `team:identity` through the Agent-scoped `systemPrompt.context()` registry with `required: true`. Its provider reads the existing Team membership and renders the member's role, name, and addressing instructions. Optional-context suppression cannot remove it. Fixed collaboration policy stays in the system prompt; roster and task state remain tool queries. Initial tasks contain only their supplied content.
|
||||
`tool-subagent-control` contributes required `subagent:identity` through `systemPrompt.context()` in ordinary subagent and Team compositions. It derives identity from `ctx.subagents.rootOf(agent)`: the root sees its own Session id, and descendants see their own, direct parent, and root ids. An unavailable root is reported explicitly. Optional-context suppression and Team mode do not remove identity. Agents receive messaging and discovery guidance only while the root is available and each corresponding standard tool is admitted. Team task policy stays in the shared system prompt; task state remains a tool query.
|
||||
|
||||
The loop owns logging, retained-snapshot comparison, and re-admission after compaction. A changed role appends the current full runtime-context snapshot after inherited history; unchanged retained context adds no message. No Team-specific retention state, event type, or log migration is introduced. Existing recorded reminders remain historical messages.
|
||||
|
||||
This supersedes only the initial-task identity decision in [Agent Teams](../feature/2026-08-05-agent-teams.md#team-identity); its roster, mailbox, and task decisions remain active.
|
||||
This supersedes only the initial-task identity decision in [Agent Teams](../feature/2026-08-05-agent-teams.md#team-identity). [Task-board wait coordination](2026-10-19-team-task-board-inbox.md) keeps ordinary inbox waiting independent of Team-only model guidance.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -28,4 +28,4 @@ This supersedes only the initial-task identity decision in [Agent Teams](../feat
|
||||
|
||||
Members pay for identity within the full runtime-context snapshot. Other context changes repeat it, while stable retained context adds no new message. Forks preserve inherited history and append current identity; actual provider cache hits remain best-effort. Existing Team authorization remains in tool execution.
|
||||
|
||||
Verification covers fresh and fork teammates, an independent fork from a teammate, optional-context suppression, compaction restoration, cold resume, and registration disposal. The recorded `team-targets` scenario pins Lead context; the keyless headless Team workflow pins teammate context through the shipped profile.
|
||||
Verification covers fresh and fork children, an independent Session fork, optional-context suppression, compaction restoration, unavailable roots, cold resume, and registration disposal. Recorded ordinary subagent and Team workflows pin identity through their shipped compositions; delegated tasks retain final-result return guidance.
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
# Agent Note: 必需 runtime context 中的 Team 身份
|
||||
# Agent Note: 必需 runtime context 中的 subagent 身份
|
||||
|
||||
Status: implemented
|
||||
|
||||
@@ -6,15 +6,15 @@ Status: implemented
|
||||
|
||||
## Problem
|
||||
|
||||
嵌入初始任务的 teammate 身份依赖普通历史保留。压缩可能移除它;从 teammate 创建的普通 fork 即使已成为独立 Lead,也可能保留旧角色。身份必须反映当前 membership,同时保持共享系统指令一致。
|
||||
嵌入初始任务的 teammate 身份依赖普通历史保留。压缩可能移除它;从 teammate 创建的普通 fork 即使已成为独立 Lead,也可能保留旧角色。身份必须反映当前 subagent 树,同时保持共享系统指令一致。
|
||||
|
||||
## Decision
|
||||
|
||||
`tool-agent-team` 通过 Agent 作用域的 `systemPrompt.context()` 注册 `team:identity`,并设置 `required: true`。提供方读取现有 Team membership,渲染成员角色、名字与寻址说明。关闭可选 context 不能移除它。固定协作策略保留在 system prompt;roster 和 task 状态仍通过工具查询。初始任务只包含传入的正文。
|
||||
`tool-subagent-control` 在普通 subagent 和 Team 组合中通过 `systemPrompt.context()` 提供必需的 `subagent:identity`。它从 `ctx.subagents.rootOf(agent)` 派生身份:根看到自身 Session id,后代看到自身、直接父级和根 id。根不可用时会明确报告。可选上下文抑制和 Team 模式都不会移除身份。只有根可用时,agent 才会分别根据对应标准工具的获准情况收到通信与发现指导。Team 任务策略保留在共享系统提示中;任务状态仍通过工具查询。
|
||||
|
||||
Loop 负责日志记录、保留快照比较和压缩后的重新接纳。角色变化会在继承历史之后追加当前完整 runtime-context 快照;保留的 context 未变化时不追加消息。不引入 Team 专属的保留状态、事件类型或日志迁移。已有身份提醒仍作为历史消息保留。
|
||||
|
||||
本决策仅取代 [Agent Teams](../feature/2026-08-05-agent-teams.zh.md#team-identity) 中的初始任务身份决策;其 roster、mailbox 和 task 决策仍然有效。
|
||||
这仅取代 [Agent Teams](../feature/2026-08-05-agent-teams.zh.md#team-identity) 中将身份放入初始任务的决策。[任务板等待协调](2026-10-19-team-task-board-inbox.zh.md)使普通收件箱等待独立于 Team 专用模型指导。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -28,4 +28,4 @@ Loop 负责日志记录、保留快照比较和压缩后的重新接纳。角色
|
||||
|
||||
成员承担完整 runtime-context 快照中的身份 token 成本。其他 context 变化会重复身份,保留的 context 稳定时则不追加消息。Fork 保留继承历史并追加当前身份;提供方实际缓存命中仍为尽力而为。现有 Team 授权仍在工具执行时落实。
|
||||
|
||||
验证覆盖 fresh 和 fork teammate、从 teammate 创建的独立 fork、关闭可选 context、压缩后恢复、冷恢复和注册释放。录制的 `team-targets` 场景固定 Lead context;无密钥 headless Team 工作流通过内置 profile 固定 teammate context。
|
||||
验证覆盖新建和 fork 子级、独立 Session fork、可选上下文抑制、压缩恢复、不可用根、冷恢复和注册释放。录制的普通 subagent 和 Team 工作流通过各自的分发组合固定身份行为;委派任务保留最终结果返回指导。
|
||||
@@ -0,0 +1,22 @@
|
||||
# Bilingual-pair consistency record for 2026-10-15-agent-teams-as-subagents.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md
|
||||
/agent-note-agent-teams-teammates-are-subagents:
|
||||
en: 51e2fc51a6e1622a
|
||||
zh: 7e09ea6292e5921d
|
||||
/agent-note-agent-teams-teammates-are-subagents/problem:
|
||||
en: ca2848e3bbc3a009
|
||||
zh: 0551e32ff6d187d0
|
||||
/agent-note-agent-teams-teammates-are-subagents/decision:
|
||||
en: 2c29c42cc02a9a05
|
||||
zh: bc542e3a27cfd03d
|
||||
/agent-note-agent-teams-teammates-are-subagents/alternatives-considered:
|
||||
en: ef0cf648ff54784f
|
||||
zh: 2a0b3745a54e7072
|
||||
/agent-note-agent-teams-teammates-are-subagents/consequences:
|
||||
en: 3ff718671be076fa
|
||||
zh: 38a367077ce95d52
|
||||
/agent-note-agent-teams-teammates-are-subagents/testing:
|
||||
en: a0ec8546a783bfd9
|
||||
zh: 8920b46970e3a2de
|
||||
@@ -0,0 +1,41 @@
|
||||
# Agent Note: Agent Teams teammates are subagents
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-10-15-agent-teams-as-subagents.zh.md)
|
||||
|
||||
The adjacency and parent-lifetime restrictions are superseded by [tree-wide communication](2026-10-16-tree-wide-agent-communication.md). [Task-board inbox coordination](2026-10-19-team-task-board-inbox.md) supersedes the separate roster and Team-owned wait; the single subagent identity decision remains active.
|
||||
|
||||
## Problem
|
||||
|
||||
Teammates already use the subagent runtime for creation, Session identity, catalog membership, capacity, cold resume, settlement, and interruption. A separate Team roster duplicates identity, provisioning recovery, and teardown. Team-scoped messaging and control tools shadow the standard subagent tools, forcing profiles to disable ordinary delegation and making fork tool schemas diverge from their parent.
|
||||
|
||||
## Decision
|
||||
|
||||
Use one subagent identity: the Session id. The existing `description` tool argument supplies the catalog label and display text; descriptions may repeat and never address an agent. Creation accepts no member name, reserves no names, and adds no catalog payload version.
|
||||
|
||||
[Tree-wide communication](2026-10-16-tree-wide-agent-communication.md) authorizes an exact live sender within one root tree and preserves the actual sender’s id. Interruption retains ancestor authority and id addressing.
|
||||
|
||||
Every root Agent and its descendants form the Team identified by the root Session id. Team creates no agents and contributes task tools and task policy. Ordinary subagent controls supply required identity, discovery, messaging, interruption, and inbox waiting. Parent and fork children receive identical tool schemas and shared policies.
|
||||
|
||||
Task ownership uses `ownerId`; root reassignment accepts itself or any reachable continuable descendant. Historical roster records remain decode-only. Team persists task changes without ending inbox waits or starting idle turns; [inbox-only waiting](../simplification/2026-10-29-inbox-only-agent-wait.md) owns wake behavior. Historical `team/message/*` records remain readable but are never delivered or acknowledged.
|
||||
|
||||
This supersedes the roster, provisioning, recovery, and messaging decisions in [Agent Teams](../feature/2026-08-05-agent-teams.md) and the [Team direct inbox decision](../../archived/simplification/2026-09-26-team-direct-inbox.md). The task-board and shared-checkout decisions remain active. [Adjacent Agent messaging](2026-08-27-adjacent-agent-steer-messaging.md) includes sibling routing, while [required runtime-context identity](2026-10-13-team-identity-runtime-context.md) owns the role statement. The [upgrade guide](../../../../docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.md) lists removed tools, methods, and configuration.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Separate Team authority and tools.** This retains a second roster and recovery lifecycle for Agents already owned by the subagent runtime, plus same-name tools and a private delivery adapter. The shared parent relation supplies the required authority.
|
||||
|
||||
**Unique names as sibling-routing opt-in.** Names add validation, reservation, persistence, and ambiguous resolution beside existing Session ids. Descriptions already explain responsibilities; stable ids distinguish repeated descriptions without another identity system.
|
||||
|
||||
**Pluggable address books.** A registry would support groups spanning unrelated parents but add another authorization input. No current consumer needs this; revisit when collaboration must cross parent trees.
|
||||
|
||||
## Consequences
|
||||
|
||||
Tree members discover and message one another through ordinary controls. Team retains task ownership, dependencies, and task policy. Subagent capacity and lifecycle apply; failed starts create no Team roster row. Existing durable task generations remain unchanged.
|
||||
|
||||
Descendants at any depth share the board and can be task owners. Historical members grant no authority. Separate working directories remain explicit caller choices; cross-process task coordination is unsupported.
|
||||
|
||||
## Testing
|
||||
|
||||
Verification covers tree delivery and cold resume with actual sender attribution, task CAS and owner discovery, historical decoding, inbox wait isolation, and registration disposal. Recorded sessions cover Team policy and identity, plus ordinary subagent return guidance.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Agent Note: Agent Teams 队友是子代理
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-10-15-agent-teams-as-subagents.md) | 中文
|
||||
|
||||
邻接和父级生命周期限制由[整树通信](2026-10-16-tree-wide-agent-communication.zh.md)取代。[任务板收件箱协调](2026-10-19-team-task-board-inbox.zh.md)取代独立名册和 Team 所有的等待;单一 subagent 身份决策仍有效。
|
||||
|
||||
## Problem
|
||||
|
||||
队友已经使用子代理运行时完成创建、Session 身份、目录成员关系、容量、冷恢复、结算和中断。独立 Team 成员列表重复了身份、创建恢复和释放职责。Team 作用域内的消息及控制工具覆盖标准子代理工具,迫使 profile 禁用普通委派,并使分叉工具模式偏离父级。
|
||||
|
||||
## Decision
|
||||
|
||||
统一使用子代理的 Session id 作为身份。现有 `description` 工具参数提供目录标签和展示文本;描述可以重复,不用于寻址代理。创建不接收成员名称、不预留名称,也不增加目录载荷版本。
|
||||
|
||||
[整树通信](2026-10-16-tree-wide-agent-communication.zh.md)在同一根树内授权当前在线发送方,并保留实际发送方 id。中断保留祖先权限和 id 寻址。
|
||||
|
||||
每个根 Agent 及其后代组成以根 Session id 标识的 Team。Team 不创建 agent,提供任务工具和任务策略。普通 subagent 控制工具提供必需身份、发现、通信、中断和收件箱等待。父级和 fork 子级接收相同工具 schema 和共享策略。
|
||||
|
||||
任务归属使用 `ownerId`;根重新分配可指定自身或任意可达的可继续执行后代。历史名册记录仅供解码。Team 持久化任务变更,不结束收件箱等待,也不启动空闲轮次;[仅等待收件箱](../simplification/2026-10-29-inbox-only-agent-wait.zh.md)负责唤醒行为。历史 `team/message/*` 记录仍可读取,但绝不再投递或确认。
|
||||
|
||||
本决策取代 [Agent Teams](../feature/2026-08-05-agent-teams.zh.md) 的成员列表、创建、恢复和消息决策,以及 [Team 直接收件箱决策](../../archived/simplification/2026-09-26-team-direct-inbox.md)。任务板和共享工作树决策仍有效。[相邻 Agent 消息](2026-08-27-adjacent-agent-steer-messaging.zh.md)纳入同级路由,[必需运行时身份](2026-10-13-team-identity-runtime-context.zh.md)负责角色声明。[升级指南](../../../../docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.zh.md)列出移除的工具、方法和配置。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**独立 Team 权限和工具。** 这会为子代理运行时已拥有的 Agent 保留第二套成员列表与恢复生命周期,以及同名工具和私有投递适配器。共同父级关系已经提供所需权限。
|
||||
|
||||
**以唯一名称选择性启用同级路由。** 名称会在现有 Session id 之外增加校验、预留、持久化和歧义解析。描述已能说明职责;稳定 id 可以区分重复描述,无需另一套身份系统。
|
||||
|
||||
**可插拔地址簿。** 注册表可支持跨不同父级的组,但会增加另一种授权输入。当前没有消费者需要此能力;协作必须跨父级树时再考虑。
|
||||
|
||||
## Consequences
|
||||
|
||||
树内成员通过普通控制工具发现和联系彼此。Team 保留任务归属、依赖和任务策略。容量和生命周期遵循 subagent 规则;启动失败不会创建 Team 名册行。现有持久化任务代次保持不变。
|
||||
|
||||
任意深度后代共享任务板,均可成为任务所有者。历史成员不授予权限。独立工作目录仍由调用者显式选择;不支持跨进程任务协调。
|
||||
|
||||
## Testing
|
||||
|
||||
验证覆盖具有实际发送方归属的整树投递和冷恢复、任务 CAS 与所有者发现、历史解码、收件箱等待隔离和注册释放。录制会话覆盖 Team 策略和身份,以及普通 subagent 返回指导。
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-10-16-tree-wide-agent-communication.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.md
|
||||
/agent-note-communication-follows-the-subagent-tree:
|
||||
en: 849d806b901e9f04
|
||||
zh: 649b2984f844d510
|
||||
/agent-note-communication-follows-the-subagent-tree/problem:
|
||||
en: c583c3cdb894e417
|
||||
zh: 6edc0354b79f4f26
|
||||
/agent-note-communication-follows-the-subagent-tree/decision:
|
||||
en: da35a75e069fa1e4
|
||||
zh: 7edf62ad66ab04ec
|
||||
/agent-note-communication-follows-the-subagent-tree/alternatives-considered:
|
||||
en: 470f9cf8c3e32af9
|
||||
zh: 73bbe29767c5d02a
|
||||
/agent-note-communication-follows-the-subagent-tree/consequences:
|
||||
en: 6d409833d762d473
|
||||
zh: 000cd93577ec6b34
|
||||
@@ -0,0 +1,29 @@
|
||||
# Agent Note: Communication follows the subagent tree
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-10-16-tree-wide-agent-communication.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A Team shares one task board across the root's entire subagent tree. Restricting discovery and messages to adjacent relatives forces deeper members to relay work through parents despite sharing tasks.
|
||||
|
||||
## Decision
|
||||
|
||||
`list_agents` starts at the caller's live root and lists that root and all reachable continuable descendants, including the caller. Parent ids and depth remain relative to the root. The existing parent catalogs remain authoritative; no Team directory is stored separately.
|
||||
|
||||
`send_message` accepts another root or continuable member of the same tree. The sender must be the exact live root or a resident continuable subagent. Cross-branch delivery verifies the target descriptor and creation catalogs, then restores only the target under the Team root. Final inbox admission rechecks the sender and root. Cancellation or rejection before acceptance releases the new target instance; accepted input follows normal Agent persistence.
|
||||
|
||||
The existing Agent registry records runtime ownership by the Team root. Creation parent ids and catalogs preserve ancestry for discovery and interrupt authority. A member finishing or being released affects only its own instance; Team-root shutdown joins all startup and cleanup. This removes ancestor restoration, parent retention, and recursive disposal without adding a Team runtime registry. Saved target presets and permission events supply restoration configuration.
|
||||
|
||||
This supersedes the adjacency, discovery, and parent-lifetime restrictions in [adjacent messaging](2026-08-27-adjacent-agent-steer-messaging.md) and [subagent teammates](2026-10-15-agent-teams-as-subagents.md). Their shared inbox, scheduling, attribution, and Team reuse decisions remain active. Members use Session ids for addressing; ordinary Session forks lead independent Teams.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Relay through parents.** This preserves the narrower routing implementation but makes the creation hierarchy determine who can collaborate. Shared work requires direct communication across branches.
|
||||
|
||||
**Separate Team directory and delivery.** A second member list and message path would duplicate the existing catalogs, inbox, and restoration lifecycle. Tree membership supplies the required communication scope without another registry.
|
||||
|
||||
## Consequences
|
||||
|
||||
Discovery output grows with the whole tree. Cold delivery requires one capacity slot and readable creation history. Intermediate creators may be offline. Completion notices reach only online creators, so work that requires descendant results must explicitly wait. Listing is a snapshot and does not promise that a later send succeeds. External and one-shot executions cannot receive follow-ups.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Agent Note: 通信范围由子代理树决定
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-10-16-tree-wide-agent-communication.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
一个 Team 在根的整棵子代理树中共享任务板。把发现和消息限定在相邻成员之间,会迫使深层成员在共享任务的同时,仍通过父级转发工作。
|
||||
|
||||
## Decision
|
||||
|
||||
`list_agents` 从调用方的在线根开始,列出该根与所有可达的可继续后代,包括调用方。父级 id 和深度始终相对根计算。现有父级目录仍是权威来源;不单独存储 Team 目录。
|
||||
|
||||
`send_message` 接受同一棵树中的其他根或可继续成员。发送者必须是确切在线根或驻留的可继续子代理。跨分支投递验证目标描述符和创建目录后,仅在 Team 根下恢复目标。最终收件箱准入重新检查发送者与根。接受前的取消或拒绝释放新建目标实例;已接受输入遵循普通 Agent 持久化。
|
||||
|
||||
现有 Agent 注册表记录 Team 根对运行时实例的所有权。创建父级 id 和目录保留用于发现及中断授权的祖先关系。成员完成或释放只影响自身实例;关闭 Team 根会等待全部启动和清理。这删除了祖先恢复、父级保留和递归释放,无需增加 Team 运行时注册表。目标保存的 preset 和权限事件提供恢复配置。
|
||||
|
||||
本决策替代[相邻消息](2026-08-27-adjacent-agent-steer-messaging.zh.md)与[子代理队友](2026-10-15-agent-teams-as-subagents.zh.md)中的邻接、发现及父子生命周期限制。它们的共享收件箱、调度、发送者归属和 Team 复用决策仍有效。成员使用 Session id 寻址;普通 Session 分叉领导独立 Team。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**通过父级转发。** 这保留了更窄的路由实现,却让创建层级决定谁能协作。共享工作需要跨分支直接通信。
|
||||
|
||||
**单独的 Team 目录与投递。** 第二份成员列表和消息路径会重复现有目录、收件箱与恢复生命周期。树成员身份已经提供所需通信范围,无需另一个注册表。
|
||||
|
||||
## Consequences
|
||||
|
||||
发现输出随整棵树增长。冷投递需要一个容量名额和可读的创建历史。中间创建者可以离线。完成通知仅投递给在线创建者,因此依赖后代结果的工作必须显式等待。列表是快照,不保证后续发送成功。外部执行和一次性执行不能接收后续消息。
|
||||
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-10-19-team-task-board-inbox.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-19-team-task-board-inbox.md
|
||||
/agent-note-team-task-boards-share-ordinary-subagent-waits:
|
||||
en: 7f19734715f5d50f
|
||||
zh: 95af09c9d855d1af
|
||||
/agent-note-team-task-boards-share-ordinary-subagent-waits/problem:
|
||||
en: 6fdc766a348d8659
|
||||
zh: 8a84748e147deed5
|
||||
/agent-note-team-task-boards-share-ordinary-subagent-waits/decision:
|
||||
en: 154da8a8a5146c89
|
||||
zh: 76a7c921153d238d
|
||||
/agent-note-team-task-boards-share-ordinary-subagent-waits/alternatives-considered:
|
||||
en: eed6f6d4fa0ccd7b
|
||||
zh: 61271057d6e5d019
|
||||
/agent-note-team-task-boards-share-ordinary-subagent-waits/consequences:
|
||||
en: cc250eb21dbb45a1
|
||||
zh: 9d03d1eb53075fb6
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: Team task boards share ordinary subagent waits
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-10-19-team-task-board-inbox.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The subagent tree already supplies identity, discovery, routing, and lifecycle. A second Team roster can grant assignment authority from stale history and duplicates member navigation. Team-wide status waits observe unrelated activity and miss input already pending for the caller. Persisting task-board wakeups as messages adds unread state and message schemas even though the task journal already owns the state to inspect.
|
||||
|
||||
## Decision
|
||||
|
||||
The subagent tree is the Team. Ordinary controls own required identity, communication, discovery, interruption, and `wait_agent`, independently of the optional task board. Team tools own task policy, without adding it to ordinary subagent compositions. Team owns task storage, dependencies, and compare-and-set commands. Assignment accepts the root or any discovered continuable descendant. Historical roster records grant no authority; the Team panel shows tasks and ordinary navigation owns members.
|
||||
|
||||
The wait-signal decision is superseded by [inbox-only Agent waits](../simplification/2026-10-29-inbox-only-agent-wait.md). Tree ownership, task durability, and historical non-authority remain unchanged.
|
||||
|
||||
Task state remains in the existing journal. Ordinary tool results record what the waiting model observes. Historical roster, mailbox, and task generations remain readable and unchanged. This partially supersedes the roster and wait decisions in [Agent Teams](../feature/2026-08-05-agent-teams.md) and [subagent teammates](2026-10-15-agent-teams-as-subagents.md). Their task durability and single-identity rationale remain active; [required runtime identity](2026-10-13-team-identity-runtime-context.md) owns retention.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep Team membership and a separate wait tool.** This duplicates tree authority and wait lifecycle, allowing stale roster rows or unrelated activity to affect current work.
|
||||
|
||||
**Persist task-board notices in every inbox.** The task journal already preserves every revision. Durable notices add message types, unread state, coalescing, and delivery-failure recovery solely to interrupt a current wait.
|
||||
|
||||
**Poll Agent status or start every idle task owner.** Activity does not identify useful input, and task readiness does not authorize a new turn. Explicit messages continue idle work when needed.
|
||||
|
||||
## Consequences
|
||||
|
||||
Task completion, inbox availability, current waiting, and Agent lifetime remain separate. Task updates do not end waits; callers inspect current tasks when deciding what to do. The task journal retains committed revisions without storing redundant notifications in recipient Sessions.
|
||||
|
||||
Verification distinguishes pending inbox input from task commits, isolates callers, excludes next-turn input and status changes, and covers wait cleanup and task persistence. Assignment coverage retains arbitrary depth and historical non-authority.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: Team 任务板复用普通 subagent 等待
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-10-19-team-task-board-inbox.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
subagent 树已经提供身份、发现、路由和生命周期。第二份 Team 名册可能根据过期历史授予分配权限,并重复成员导航。Team 范围状态等待会观察无关活动,却漏掉调用者已有的待处理输入。任务日志已经拥有待查询状态,将任务板唤醒持久化为消息仍会增加未读状态和消息 schema。
|
||||
|
||||
## 决策
|
||||
|
||||
subagent 树就是 Team。普通控制工具独立于可选任务板,负责必需身份、通信、发现、中断和 `wait_agent`。Team 工具负责任务策略,不向普通 subagent 组合添加该策略。Team 负责任务存储、依赖和比较并交换命令。分配接受根或任意已发现的可继续执行后代。历史名册不授予权限;Team 面板显示任务,普通导航负责成员。
|
||||
|
||||
等待信号决策已由[仅等待收件箱的 Agent 等待](../simplification/2026-10-29-inbox-only-agent-wait.zh.md)取代。树归属、任务持久性和历史记录不授予权限的决策保持不变。
|
||||
|
||||
任务状态保留在现有日志中。普通工具结果记录等待中的模型所观察到的结果。历史名册、邮箱和任务代次保持可读且不变。这部分取代 [Agent Teams](../feature/2026-08-05-agent-teams.zh.md) 和 [subagent 队友](2026-10-15-agent-teams-as-subagents.zh.md) 中的名册与等待决策。其任务持久性和单一身份理由继续生效;[必需运行时身份](2026-10-13-team-identity-runtime-context.zh.md)负责保留规则。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**保留 Team 成员关系与独立等待工具。** 这会重复树权限和等待生命周期,让过期名册行或无关活动影响当前工作。
|
||||
|
||||
**将任务板通知持久化到每个收件箱。** 任务日志已保留每个修订。持久通知仅为打断当前等待,就增加消息类型、未读状态、合并和投递失败恢复。
|
||||
|
||||
**轮询 Agent 状态或启动每个空闲任务所有者。** 活动状态不能标识有用输入,任务就绪也不授权新轮次。需要时通过显式消息继续空闲成员的工作。
|
||||
|
||||
## 后果
|
||||
|
||||
任务完成、收件箱可用性、当前等待和 Agent 生命周期保持独立。任务更新不会结束等待;调用者在决定下一步工作时查询当前任务。任务日志保留已提交修订,不在接收者 Session 中存储重复通知。
|
||||
|
||||
验证区分已有收件箱输入和任务提交,隔离调用者,排除下一轮输入及状态变化,并覆盖等待清理和任务持久性。分配验证保留任意深度和历史记录无权限场景。
|
||||
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-10-20-session-team-mode.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-20-session-team-mode.md
|
||||
/agent-note-team-availability-is-selected-per-session:
|
||||
en: 5f36d81bd414ea67
|
||||
zh: 71883b0fe2afd1b6
|
||||
/agent-note-team-availability-is-selected-per-session/problem:
|
||||
en: db7a81ab452fdd0f
|
||||
zh: a14fdcdb8ff4cd47
|
||||
/agent-note-team-availability-is-selected-per-session/decision:
|
||||
en: f4e4ba7be0eda796
|
||||
zh: 410649e86ff5ca72
|
||||
/agent-note-team-availability-is-selected-per-session/alternatives-considered:
|
||||
en: cd3cc1f3fab84f80
|
||||
zh: 6d7792742d62f9fe
|
||||
/agent-note-team-availability-is-selected-per-session/consequences:
|
||||
en: b56a6a1c8ad1aa7a
|
||||
zh: 43bdd8a5bccce8bf
|
||||
@@ -0,0 +1,29 @@
|
||||
# Agent Note: Team availability is selected per Session
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-10-20-session-team-mode.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A plugin-level Team switch requires users to change the shared profile before choosing collaboration for one conversation. It can disable another Session’s capability and leaves a second control beside the Session selection. Ordinary tasks still need their existing tool catalog, and disabling collaboration must not discard task history or children.
|
||||
|
||||
## Decision
|
||||
|
||||
The base bundle loads the Team service and task tools, and the Web bundle loads the Team UI. The packages belong to the subagent and client product groups. A saved root-Session selection gates task tools and Team guidance throughout the tree, and the shared board in the root conversation; new Sessions default off. Ordinary subagent creation, communication, and navigation remain available.
|
||||
|
||||
The model and reasoning menu owns the user’s selection. The root conversation’s Subagent panel displays members above shared tasks. Disabling Team keeps the journal and children; re-enabling restores the board. Historical task boards without a saved selection stay enabled. Profile loading removes the retired optional bundle selection, retaining row overrides.
|
||||
|
||||
This supersedes the explicit-composition choice in [Agent Teams](../feature/2026-08-05-agent-teams.md). Its durability, ownership, and lifecycle decisions remain active. [Optional bundles](../process/2026-09-15-shipped-optional-bundles.md) still governs other experimental features.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep both switches.** Capability installation remains a prerequisite that the conversation control cannot explain or resolve; users must manage shared profile state for a Session decision.
|
||||
|
||||
**Hide the plugin card without loading Team.** New profiles would have no way to obtain the capability. Loading experimental packages in the default profile would violate product isolation; moving them to their owning product groups makes the delivered support explicit.
|
||||
|
||||
## Consequences
|
||||
|
||||
Default profiles carry the Team implementation while exposing no task tools until enabled. The removed package names require custom compositions to migrate; durable event names and task data remain unchanged. [The upgrade guide](../../../../docs/upgrade-guide/v0.2.1-rc.2/session-team-mode/guide.md) owns those steps.
|
||||
|
||||
Verification covers default startup, new-Session opt-in, independent roots, persisted selections, retained tasks, old profile migration, and absence of the plugin switch. Product-isolation checks retain an unrelated experimental plugin as their negative control.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Agent Note: 按 Session 选择 Team 能力
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-10-20-session-team-mode.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
插件级 Team 开关要求用户先修改共享 profile,才能为一个会话选择协作。它可能关闭另一个 Session 的能力,并在会话选项之外留下第二个控件。普通任务仍需要原有工具目录,关闭协作也不能丢弃任务历史或子智能体。
|
||||
|
||||
## Decision
|
||||
|
||||
基础 bundle 加载 Team 服务与任务工具,Web bundle 加载 Team UI。相关包归属 subagent 与 client 产品组。根 Session 保存的选择控制整棵树的任务工具与 Team 指引,以及根会话中的共享任务板;新 Session 默认关闭。普通 subagent 创建、通信与导航继续可用。
|
||||
|
||||
模型与推理等级菜单承载用户选择。根会话的 Subagent 面板在成员下方显示共享任务。关闭 Team 保留日志与子智能体;重新开启恢复任务板。没有保存选择的历史任务板保持开启。profile 加载时移除已停用的可选 bundle 选择,保留配置行覆盖。
|
||||
|
||||
本决策替代 [Agent Teams](../feature/2026-08-05-agent-teams.zh.md) 中显式选择组合的部分;其持久性、归属与生命周期决策继续有效。[可选 bundle](../process/2026-09-15-shipped-optional-bundles.zh.md)仍约束其他实验性功能。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**保留两个开关。** 安装能力仍是会话控件无法解释或解决的前置条件;用户必须为 Session 决策管理共享 profile 状态。
|
||||
|
||||
**隐藏插件卡片但不加载 Team。** 新 profile 将无法获得该能力。默认 profile 加载实验性包会违反产品隔离;将相关包移入所属产品组可明确随产品提供的支持。
|
||||
|
||||
## Consequences
|
||||
|
||||
默认 profile 包含 Team 实现,但开启前不暴露任务工具。自定义组合必须迁移已移除的包名;持久事件名称与任务数据保持不变。[升级指南](../../../../docs/upgrade-guide/v0.2.1-rc.2/session-team-mode/guide.zh.md)负责具体步骤。
|
||||
|
||||
验证覆盖默认启动、新 Session 手动开启、独立根、选择持久化、任务保留、旧 profile 迁移和插件开关移除。产品隔离检查使用另一个无关实验性插件作为负对照。
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-10-22-subagent-target-user-controls.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-22-subagent-target-user-controls.md
|
||||
/agent-note-user-controls-address-the-target-subagent:
|
||||
en: d7dcf54ddb311881
|
||||
zh: 5f388447737c5414
|
||||
/agent-note-user-controls-address-the-target-subagent/problem:
|
||||
en: 962c75771235903c
|
||||
zh: 203b651c0830310e
|
||||
/agent-note-user-controls-address-the-target-subagent/decision:
|
||||
en: 13a2617e28445744
|
||||
zh: 75d193ef7d8c7804
|
||||
/agent-note-user-controls-address-the-target-subagent/alternatives-considered:
|
||||
en: 447adb24280bf2bb
|
||||
zh: 08d95a93b14aa5c7
|
||||
/agent-note-user-controls-address-the-target-subagent/consequences:
|
||||
en: 98e784f55775247d
|
||||
zh: 1c62d2ebc71b658f
|
||||
@@ -0,0 +1,33 @@
|
||||
# Agent Note: User controls address the target subagent
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-10-22-subagent-target-user-controls.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A browser knows the subagent it is displaying, but requiring its direct creator and a continuable marker makes every control caller repeat history metadata. A caller-supplied parent id checks address consistency without establishing the caller's identity or permission. Calls that resolve only the global access resource also cannot express a grant on one target Session.
|
||||
|
||||
## Decision
|
||||
|
||||
Browser prompts and `interruptByUser` address only the target child Session. Each Remote method explicitly declares that Session as its access resource; the Gateway checks the invocation's principal before attachment admission, restoration, or cancellation. A global or parent Session grant does not imply a child Session grant. Policies decide whether to grant both actions, either action, or inherited access. Same-process user interruption trusts its caller and does not accept a wire-supplied principal.
|
||||
|
||||
Prompt delivery derives the live root from the resident Activation or verified persisted creation edges. Only the cold target resumes; its creator can remain offline. Shared delivery retains exact live root checks, shutdown admission, cancellation, capacity, and final inbox acceptance. A root Session is not a target of the browser subagent prompt endpoint. User interruption addresses a resident Activation without persistence reads or restoration; an absent target remains a no-op, and interruption preserves parked input.
|
||||
|
||||
Model messages retain the exact live sender and same-tree rule from [tree-wide communication](2026-10-16-tree-wide-agent-communication.md). Model interruption retains its stricter ancestor rule. Creation parents still determine catalog edges, history-address consistency, ancestry, and completion recipients; they are not browser control credentials.
|
||||
|
||||
This supersedes the direct-parent addressing requirement for human continuation in [continuable subagents](../feature/2026-07-28-continuable-subagent-conversations.md). That record retains its inbox and durable continuation rationale; tree-wide communication owns its superseded model adjacency and runtime ownership restrictions. No Session format changes.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep the parent check as authorization.** The client supplies the parent id and can obtain it from history. Matching that id does not authorize the principal, and requiring it couples controls to discovery metadata.
|
||||
|
||||
**Require a root id from the browser.** The server already derives the runtime owner from the target. Another supplied id adds a consistency check without granting permission; Agent senders still need an independently resolved root to enforce same-tree messaging.
|
||||
|
||||
**Unify user and Agent interruption permissions.** A user with a target grant can stop that target, while an Agent may stop only its descendants. Same-tree collaboration does not grant members control over each other's execution.
|
||||
|
||||
## Consequences
|
||||
|
||||
Remote callers update their prompt fields, interrupt endpoint, and non-owner policy actions and resources; the [upgrade guide](../../../../docs/upgrade-guide/v0.2.1-rc.2/subagent-user-controls/guide.md) owns the migration. History URLs retain parent ids for consistency checks. Cold host prompts may read creation ancestry twice, once to find the live root and again during restoration; no verified-ancestry cache is retained across asynchronous work.
|
||||
|
||||
Gateway tests cover target grants, global-only and parent-only refusal, separate send and interrupt actions, owner access, and refusal before side effects. Continuation tests cover nested and cold targets with online or offline creators, unavailable roots, and rejection of a root as a subagent target. The Web interrupt scenario exercises the renamed endpoint and parked-input behavior through the shipped composition.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Agent Note: 用户控制按目标子代理寻址
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-10-22-subagent-target-user-controls.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
浏览器知道当前展示的子代理,但要求提供直接创建者和 continuable 标记,使每个控制调用方都要重复历史元数据。调用方提供的父级 id 只能检查地址一致性,不能确定调用者身份或权限。仅解析出 global 访问资源的调用也无法表达对单个目标 Session 的授权。
|
||||
|
||||
## 决策
|
||||
|
||||
浏览器提示词和 `interruptByUser` 仅按目标子 Session 寻址。每个 Remote 方法显式声明该 Session 为访问资源;Gateway 在附件接纳、恢复或取消之前检查调用的 principal。global 或父 Session 授权不意味着子 Session 授权。策略决定授予两个操作、其中一个操作,或继承访问权。同进程用户中断信任其调用方,不接受从线上传入的 principal。
|
||||
|
||||
提示词投递从驻留 Activation 或经过验证的持久化创建关系中解析在线根。仅恢复冷目标,创建者可以保持离线。共享投递保留精确在线根检查、关闭准入、取消、容量和最终 inbox 接纳。根 Session 不是浏览器子代理提示词端点的目标。用户中断直接寻址驻留 Activation,不读取持久化数据,也不恢复目标;目标缺席仍为无操作,中断保留停放的输入。
|
||||
|
||||
模型消息保留[整树通信](2026-10-16-tree-wide-agent-communication.zh.md)中的精确在线发送者和同树规则。模型中断保留更严格的祖先规则。创建父级仍决定目录关系、历史地址一致性、祖先关系和完成通知接收者;它们不是浏览器控制凭证。
|
||||
|
||||
本决策取代[可继续的 subagent](../feature/2026-07-28-continuable-subagent-conversations.zh.md)对人工继续执行要求直接父级地址的部分。旧记录保留 inbox 和持久化继续执行的理由;整树通信决策拥有其中已取代的模型相邻关系和运行时所有权限制。Session 格式不变。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**保留父级检查作为授权。** 客户端提供父级 id,也能从历史中取得它。匹配该 id 不能授权 principal,还使控制依赖于发现元数据。
|
||||
|
||||
**要求浏览器提供根 id。** 服务端已经能从目标解析运行时所有者。再提供一个 id 只会增加一致性检查,不会授予权限;Agent 发送者仍需要独立解析的根来约束同树消息。
|
||||
|
||||
**统一用户和 Agent 的中断权限。** 用户获得目标授权后可以停止该目标,而 Agent 只能停止自己的后代。同树协作不赋予成员控制彼此执行的权限。
|
||||
|
||||
## 影响
|
||||
|
||||
Remote 调用方需要更新提示词字段、中断端点,以及非 owner 策略的操作和资源;迁移步骤由[升级指南](../../../../docs/upgrade-guide/v0.2.1-rc.2/subagent-user-controls/guide.zh.md)拥有。历史 URL 保留父级 id 用于一致性检查。冷目标的宿主提示词可能读取两次创建祖先:先查找在线根,再在恢复期间验证;不跨异步操作保留已验证祖先缓存。
|
||||
|
||||
Gateway 测试覆盖目标授权、仅 global 和仅父级授权的拒绝、独立的发送和中断操作、owner 访问,以及副作用前拒绝。继续执行测试覆盖创建者在线或离线时的嵌套和冷目标、根不可用,以及拒绝把根当作子代理目标。Web 中断场景通过交付组合验证重命名后的端点和输入停放行为。
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-10-22-subagent-tool-owned-communication.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-22-subagent-tool-owned-communication.md
|
||||
/agent-note-subagent-tools-own-communication-guidance:
|
||||
en: 0bd031aa81fbc18d
|
||||
zh: c530e980826698ed
|
||||
/agent-note-subagent-tools-own-communication-guidance/problem:
|
||||
en: ac391b569d2bc7e9
|
||||
zh: a95f613aa2b330b8
|
||||
/agent-note-subagent-tools-own-communication-guidance/decision:
|
||||
en: 52fb33963bedfe07
|
||||
zh: 75fe66d80f47323f
|
||||
/agent-note-subagent-tools-own-communication-guidance/alternatives-considered:
|
||||
en: c55d44c6414e0c4c
|
||||
zh: 9fa2ab31843e259b
|
||||
/agent-note-subagent-tools-own-communication-guidance/consequences:
|
||||
en: 52f7234664c515ec
|
||||
zh: 55ec7b54c044e9af
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
# Agent Note: Subagent tools own communication guidance
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-10-22-subagent-tool-owned-communication.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The continuation manager inspected a Symbol on the standard messaging tool before adding tool instructions and parent addressing to the initial task. This coupled orchestration to one tool implementation and made result-delivery guidance depend on tool installation. A published internal entry point carried both that marker and a host prompt adapter used only by tests.
|
||||
|
||||
## Decision
|
||||
|
||||
`tool-subagent-control` owns messaging instructions and parent addressing. Its logged runtime context gives delegated children their direct creator’s id from the Session header, explains when to send findings, and states that sending does not finish a turn. The context is emitted only for delegated children when the plugin’s own tool definition is admitted in the current scope and step; roots and ordinary Session forks receive none. Communication guidance follows inherited history without changing the shared system prompt or tool schema.
|
||||
|
||||
The manager owns result delivery. Local activations with `delivery: 'parent'` receive automatic-result guidance even without messaging tools; caller-owned activations receive no parent-delivery promise. Settlement timing, recipients, output, and suppression of closing text after a final message to the parent remain unchanged. Tool visibility grants no messaging authority.
|
||||
|
||||
This supersedes the Symbol identity and initial-task communication-guidance choices in [adjacent messaging](2026-08-27-adjacent-agent-steer-messaging.md). Its message attribution, fixed Steer delivery, and rejection of a separate report operation remain active. The internal entry point and prompt adapter are removed; the [upgrade guide](../../../../docs/upgrade-guide/v0.2.1-rc.2/subagent-user-controls/guide.md) owns caller migration.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep a cross-package marker or add a service hook.** Tool registration already gives its owner the exact definition, and prompt assembly already supplies the current Agent. Another protocol or stored identity has no independent consumer.
|
||||
|
||||
**Move all return guidance into the tool.** Automatic parent notification depends on activation delivery, not on messaging capability. The tool must not promise it to caller-owned activations or withhold it from children without tools.
|
||||
|
||||
## Consequences
|
||||
|
||||
Tests cover visible, hidden and replaced definitions, logged identity, plugin disposal, fork prefix reuse, and parent versus caller result delivery. Recorded-session snapshots cover the assembled prompt and durable runtime context. Tests needing host input use the existing private manager test helper; no production adapter exists solely to expose that helper.
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
# Agent Note: Subagent 工具拥有通信指导
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-10-22-subagent-tool-owned-communication.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
继续执行管理器检查标准通信工具上的 Symbol,随后在初始任务中加入工具说明和父级地址。这让编排依赖一个工具实现,也让结果交付指导取决于是否安装工具。已发布的内部入口同时承载该标记和仅供测试使用的宿主 prompt 适配器。
|
||||
|
||||
## 决策
|
||||
|
||||
`tool-subagent-control` 拥有通信说明和父级寻址。持久记录的运行时上下文从 Session header 读取委派子级的直接创建者 id,说明何时发送发现,并指出发送不会结束轮次。仅当本插件的工具定义在当前作用域和 step 中可用时,才向委派子级提供上下文;根代理和普通 Session fork 不接收。通信指导位于继承历史之后,不改变共享系统提示词或工具 schema。
|
||||
|
||||
管理器拥有结果交付。采用 `delivery: 'parent'` 的本地 activation 即使没有通信工具,也会收到自动结果交付指导;调用方拥有的 activation 不会收到父级交付承诺。结算时序、接收方、输出,以及最后向父级发送消息后省略结束文本的规则保持不变。工具可见性不授予通信权限。
|
||||
|
||||
本决策取代[相邻代理通信](2026-08-27-adjacent-agent-steer-messaging.zh.md)中的 Symbol 身份和初始任务通信指导选择。该记录的消息来源归属、固定 Steer 投递,以及不另设 report 操作的决策继续有效。内部入口和 prompt 适配器已删除;调用方迁移由[升级指南](../../../../docs/upgrade-guide/v0.2.1-rc.2/subagent-user-controls/guide.zh.md)说明。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**保留跨包标记或增加 service hook。** 工具注册已经向所有者提供确切定义,提示词组装也已提供当前 Agent。另一套协议或身份存储没有独立消费者。
|
||||
|
||||
**把全部返回指导移入工具。** 自动父级通知取决于 activation 的交付方式,而非通信能力。工具不能向调用方拥有的 activation 承诺自动通知,也不能让没有工具的子级缺少交付说明。
|
||||
|
||||
## 后果
|
||||
|
||||
测试覆盖可见、隐藏及替换的定义、持久记录的身份、插件释放、fork 前缀复用,以及父级与调用方结果交付。录制会话快照覆盖组装后的提示词和持久运行时上下文。需要宿主输入的测试使用现有的私有 manager 测试帮助函数;生产代码不再仅为暴露该帮助函数而保留适配器。
|
||||
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-10-29-cua-dev-signing-identity.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-29-cua-dev-signing-identity.md
|
||||
/agent-note-development-signing-identity-for-source-built-cua-helpers:
|
||||
en: 472c4efd47184a32
|
||||
zh: 49f64628531970e9
|
||||
/agent-note-development-signing-identity-for-source-built-cua-helpers/problem:
|
||||
en: a24857a312482724
|
||||
zh: dbac75d363670e86
|
||||
/agent-note-development-signing-identity-for-source-built-cua-helpers/decision:
|
||||
en: d2265f9ec5fb7a79
|
||||
zh: 05da2479b024a4bb
|
||||
/agent-note-development-signing-identity-for-source-built-cua-helpers/alternatives-considered:
|
||||
en: e30db5bc9c7aab43
|
||||
zh: fa84883b5527334e
|
||||
/agent-note-development-signing-identity-for-source-built-cua-helpers/consequences:
|
||||
en: fb0e8fe44890bdf9
|
||||
zh: d9e9ace1a4c10060
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: Development signing identity for source-built CUA helpers
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-10-29-cua-dev-signing-identity.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Source checkouts compile `DSH Computer Use.app` with an ad-hoc signature. macOS stores Accessibility and Screen Recording grants for a bundle identifier with the designated requirement of the granted code, and an ad-hoc requirement is the code hash. Every changed build and every other worktree therefore needs new grants, and these builds shared the release helper's bundle identifier and replaced its grant entry.
|
||||
|
||||
## Decision
|
||||
|
||||
The Host builds source checkouts as a development variant with bundle identifier `com.deepseek.harness.computer-use.dev` and name `DSH Computer Use Dev`, in `DSH Computer Use Dev.app`. The macOS privacy list and the helper’s reported launch application use the bundle filename, so that filename also includes Dev. The release variant and its packaging are unchanged. Grants stay with the helper application launched through LaunchServices, as the [shared helper decision](2026-10-09-cua-shared-native-helper.md) requires.
|
||||
|
||||
`sourceSigning` belongs to the macOS provider and accepts only default `ad-hoc` or `{ identity, keychain }` selecting an existing certificate and absolute keychain path. Builds do not create, unlock or remove identities. Developer credentials stay separate from the user-facing DSH home, which stores only the certificate selection; the shared `cua` package owns no macOS signing configuration.
|
||||
|
||||
The [developer signing skill](../../../skills/dsh-cua-macos-signing/SKILL.md) guides Agents through creating, reusing and unlocking an isolated identity under `~/.cua-macos-keys/`, then selecting its fingerprint and keychain path in user configuration. The tool stores a random password in a `0600` file, permits codesign access, and adds the dedicated keychain to the user search list. A file lock serializes setup and removal for that directory; a missing keychain with an existing record is preserved and rejected. A directory marker limits identity management to explicitly claimed, initially empty directories. Unexpected files or symlinked identity entries stop setup and removal before changes, so a developer-selected path cannot erase unrelated contents.
|
||||
|
||||
Certificate signatures embed a signing time even without a timestamp server. Development publication records which generation each unsigned bundle and identity produced, then selects that generation again instead of re-signing, so identical sources keep one executable digest and one shared service. The cache uses certificate fingerprints; explicit certificate names are not reused because renewal can change the certificate behind a name. Signing failures stop publication. Developer setup diagnostics omit passwords and complete command arguments. The build CLI defaults to `--identity -` and accepts an existing certificate with `--identity` and optional `--keychain` for release packaging.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Grant the launching terminal.** Launching the service directly makes macOS attribute permissions to the terminal or editor that started DSH. Every program that application runs, including commands run by the model, then has the permissions.
|
||||
|
||||
**Automatically create a fixed identity during source builds.** This makes ordinary startup create developer private keys and ties their lifetime to user data. An explicit developer workflow preserves the option to reduce repeated grants while keeping identity creation, removal and selection under developer control.
|
||||
|
||||
**Self-signed identity in the login keychain.** The login keychain is already unlocked and on the search list, but allowing `codesign` to use a new key there without a dialog requires the user's login password.
|
||||
|
||||
**Add the keychain to the search list only while signing.** The release packaging script does this for one invocation. Concurrent builds in several worktrees would read and restore the user list over each other, and a stopped build would leave the entry behind anyway.
|
||||
|
||||
**A stable launcher that spawns each worktree's helper.** Grants would follow the launcher, but any local process could ask it to run an arbitrary executable with those grants, which is the same exposure as the stored key without a signature check.
|
||||
|
||||
## Consequences
|
||||
|
||||
Development builds signed by the same certificate share a signing requirement; actual permission retention after grants, rebuilds and worktree changes still requires verification. Other processes of the same user can read the stored password and sign with that identity; the release helper has a different bundle identifier. Deleting DSH home removes its selection, not the developer identity; deleting the identity requires clearing every configuration that references it. The real-signing e2e checks reuse, unlocking, requirements across output directories and search-list restoration after removal.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: 源码构建的 CUA helper 使用开发签名身份
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-10-29-cua-dev-signing-identity.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
源码检出用临时签名编译 `DSH Computer Use.app`。macOS 按 Bundle ID 和被授权代码的指定要求保存辅助功能与屏幕录制授权,临时签名的指定要求就是代码哈希。因此每次构建内容变化、每换一个 worktree,都需要重新授权;这些构建还与正式 helper 共用 Bundle ID,会替换正式版的授权条目。
|
||||
|
||||
## 决策
|
||||
|
||||
Host 以开发版构建源码检出,Bundle ID 为 `com.deepseek.harness.computer-use.dev`,名称为 `DSH Computer Use Dev`,应用包文件名为 `DSH Computer Use Dev.app`。macOS 隐私列表和 helper 返回的启动应用名称使用应用包文件名,因此文件名也需包含 Dev。正式版及其打包流程不变。按[共享 helper 决策](2026-10-09-cua-shared-native-helper.zh.md)的要求,授权仍归经 LaunchServices 启动的 helper 应用。
|
||||
|
||||
`sourceSigning` 由 macOS provider 定义,只接受默认的 `ad-hoc` 和指定已有证书及钥匙串绝对路径的 `{ identity, keychain }`。构建不创建、解锁或移除身份。开发者凭据与面向用户的 DSH home 分开,后者只保存选择证书的配置;共享 `cua` 包不持有 macOS 签名配置。
|
||||
|
||||
[开发者签名 skill](../../../skills/dsh-cua-macos-signing/SKILL.md)指导 Agent 在 `~/.cua-macos-keys/` 创建、复用和解锁独立身份,将证书指纹和钥匙串路径写入用户配置。工具把随机密码存入 `0600` 文件,允许 codesign 访问私钥,并把专用钥匙串加入用户搜索列表。文件锁让同一目录的初始化和移除串行执行;已有记录但钥匙串缺失时保留记录并报错。目录标记将身份管理限制在明确占用的初始空目录中。发现无关文件或身份文件软链接时,初始化和移除都会在修改前停止,避免开发者指定的路径导致无关内容被删除。
|
||||
|
||||
即使不使用时间戳服务器,证书签名也包含签名时间。开发构建记录每份未签名内容与身份对应的已签名目录,再次构建相同内容时复用该目录,保持可执行文件摘要和共享服务一致。缓存使用证书指纹;按证书名称选择时每次签名,避免续期后复用旧证书。签名失败会中止发布。开发者工具的诊断不包含密码或完整命令参数;构建 CLI 默认 `--identity -`,支持指定证书,正式打包也可省略 `--keychain`。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**把权限交给启动终端。** 直接启动服务时,macOS 把权限归属到启动 DSH 的终端或编辑器。该应用运行的所有程序,包括模型执行的命令,都会带上这些权限。
|
||||
|
||||
**源码构建自动创建固定身份。** 这会让普通启动生成开发者私钥,并把身份生命周期绑定到用户数据目录。显式开发者操作保留了减少重新授权的选择,也让证书的创建、移除和使用范围由开发者决定。
|
||||
|
||||
**把自签名身份放进登录钥匙串。** 登录钥匙串已解锁且在搜索列表中,但要让 `codesign` 不弹窗地使用其中的新私钥,需要用户的登录密码。
|
||||
|
||||
**只在签名期间把钥匙串加入搜索列表。** 正式打包脚本对单次调用这样处理。多个 worktree 并发构建时会互相覆盖读取和恢复的列表,构建被中断时条目也会残留。
|
||||
|
||||
**用固定的启动器运行各 worktree 的 helper。** 授权会跟随启动器,但任何本机进程都能让它带着这些授权运行任意可执行文件,暴露面与保存的私钥相同,还少了签名检查。
|
||||
|
||||
## 后果
|
||||
|
||||
同一证书签出的开发构建共用签名要求;授权后重建和跨 worktree 的实际权限保持仍需验证。同一用户的进程可读取保存的密码并使用该身份签名,正式 helper 使用不同的 Bundle ID。删除 DSH home 只移除配置,不删除开发者身份;删除该身份后应清理所有引用它的配置。真实签名 e2e 验证复用、解锁、不同输出目录的签名要求和移除后的搜索列表恢复。
|
||||
@@ -1,22 +0,0 @@
|
||||
# Bilingual-pair consistency record for 2026-09-17-multiline-display-dollar-math.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-17-multiline-display-dollar-math.md
|
||||
/agent-note-multi-line-display-dollar-math:
|
||||
en: 8f57c77aa43fbd6c
|
||||
zh: ef5192184e4923d5
|
||||
/agent-note-multi-line-display-dollar-math/problem:
|
||||
en: e4b73644a982844c
|
||||
zh: 48f86358242d6ccc
|
||||
/agent-note-multi-line-display-dollar-math/decision:
|
||||
en: d6b136b1f62285b7
|
||||
zh: aba323bfdb800162
|
||||
/agent-note-multi-line-display-dollar-math/alternatives-considered:
|
||||
en: e0833b5674e648e7
|
||||
zh: b0472020b07099b9
|
||||
/agent-note-multi-line-display-dollar-math/consequences:
|
||||
en: 70f16597bd640e93
|
||||
zh: 0e0c9005f331e514
|
||||
/agent-note-multi-line-display-dollar-math/testing:
|
||||
en: f17a1453df91ce88
|
||||
zh: 4f15be0139e8b694
|
||||
@@ -1,66 +0,0 @@
|
||||
# Agent Note: Multi-line display-dollar math
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-17-multiline-display-dollar-math.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Authors and models write `$$` display math in three shapes. The settled grammar rendered two of them: `$$math$$` on one line, and an opening `$$` alone on its line closed by `$$` alone on its line. The third shape — math on the opening line, a closing `$$` at the end of the last math line — did not:
|
||||
|
||||
```markdown
|
||||
$$f(x)=x
|
||||
\text{continued}$$
|
||||
```
|
||||
|
||||
`micromark-extension-math` treats text after an opening `$$` as fence meta and accepts a closing fence only on a line whose content is exactly `$$` (after optional whitespace). The [markdown compatibility layer](../../archived/feature/2026-07-23-web-assistant-markdown.md) claimed only the same-line shape, and the settled renderer passes `node.value` to KaTeX while ignoring `node.meta`. The block therefore never closed: it consumed the document text that followed, `value` retained the literal `$$`, KaTeX raised `Can't use function '$' in math mode`, and [katex.tsx](../../../../packages/client/ui-primitives/src/markdown/katex.tsx)'s error arm painted the TeX source plus the swallowed prose in red.
|
||||
|
||||
In the session that surfaced this, one chat reply lost lines 22–55 of 55 to a single error span, and six note files lost every line after such a block (up to 500 lines) to one. A second defect shared the mechanism: when a later lone `$$` did close the flow, the opening line had already been assigned to `meta`, so the renderer silently dropped the first line of the formula.
|
||||
|
||||
## Decision
|
||||
|
||||
[mathCompatibility.ts](../../../../packages/client/ui-primitives/src/markdown/mathCompatibility.ts) gains multi-line display-dollar blocks. Its dollar construct keeps the opening line's text in `mathFlowValue` instead of fence meta and closes at a trailing `$$` on any later content line, so `$$a+b` continued by `c+d$$` becomes one math node.
|
||||
|
||||
A `$$` sequence that fails to close ends the candidate: immediately on a later line, and at that line's ending on the opening line (`$$x$$ trailing` fails there). Within the opening line the scan continues, so a later `$$` there still closes (`$$\verb*a$$a$$`). Scanning past either point would consume a later block's closing fence. A solitary `$` is content rather than a failed fence, so a formula that uses one — `$$P = \begin{cases} 1 & \text{if $x>0$} \end{cases}$$` — keeps the block open and closes normally wherever this flow owns the block. A trailing backslash escapes the line ending, not the next line's first character, so the escape run ends at the line ending instead of reaching into the next candidate. An unescaped `%` starts a TeX comment that runs to the line ending, so a `$$` at the end of a later line closes nothing when the comment holds text before it. A `%` whose text before the fence is empty or whitespace (`95%$$`, `95 %$$`) holds no comment text, and a block that opens and closes on one line keeps closing at that line's `$$`, as it did before this flow spanned lines. That state and its gate on the closing check belong to the dollar flow. The characters right after `\verb` or `\verb*` are that command's delimiter rather than a fence, so the delimiter and, when the region is empty, the `$` closing it are content; that tail is cleared at a line ending and after an escaped backslash (`\\verb` is a line break followed by letters), and a comment holds no commands. Otherwise verbatim regions are not parsed, which keeps `\verb`-bearing formulas on the same value path as every other input.
|
||||
|
||||
Its paragraph-interruption check declines instead of consuming the line ending, for the dollar construct only. Consuming it there keeps the paragraph open across the line boundary, so the document layer keeps the container and counts the next line lazy for every construct tried afterwards; that made upstream's `mathFlow`, the construct tried after this one, end empty inside a list item. Declining also leaves the interruption decision to that upstream flow, whose own check short-circuits without consuming and claims only the fences it accepts, so a candidate neither construct accepts (`**before` followed by `$$a\$b`) leaves its paragraph intact. The backslash construct has no competing upstream construct and keeps consuming there.
|
||||
|
||||
[parse.ts](../../../../packages/client/ui-primitives/src/markdown/parse.ts) registers `mathCompatibility()` after `math()`: micromark dispatches the extension registered last first, so the dollar flow above takes precedence over upstream's `mathFlow` while `math()` still owns inline `$…$`.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep the compatibility layer at same-line `$$…$$`.** The multi-line shape is what GitHub, KaTeX's auto-render, VS Code, and Typora accept, and the form models emit for multi-case formulas; leaving it unparsed means a red error span in place of the formula plus the rest of the document.
|
||||
|
||||
**Repair the parsed tree afterwards.** The flow consumes the swallowed text into `mathFlowValue`, where it is indistinguishable from math source; `meta` and `value` cannot be reassembled into the intended block, and the following blocks were never parsed at all.
|
||||
|
||||
**Rewrite `$$` fences in the source text before parsing.** The compatibility layer is a grammar extension by design; a text pass would need to locate code spans, fences, and escapes itself, and would change author text rather than parse it.
|
||||
|
||||
**Continue the dollar flow after a failed close.** That is what a plain `multiline` flag produces: `$$x$$ trailing\n\n$$y$$` then scans to the second block's `$$` and renders one math node holding both lines' text, where upstream inline math renders `x`, the prose, and `y` separately. Existing behavior for `$$x$$ trailing` already pinned the fallback, so the flow declines the candidate instead. Declining on any `$` rather than a full `$$` sequence also broke formulas that use a solitary `$` inside `\text{}`.
|
||||
|
||||
**End the attempt as soon as one line resists parsing.** Declining every failed same-line close swallows nothing but turns `$$a$b` lines into a quadratic scan and drops the solitary-`$` formulas above. The condition is the full `$$` sequence, not the character.
|
||||
|
||||
**Claim the paragraph interruption in the check.** Returning success there, as upstream's flow does for the fences it accepts, prevents the lazy bookkeeping that broke the list case, but it also ends the paragraph for candidates that later fail (`**before\n$$a\$b\nafter**` split in two and exposed its `**`). Declining instead lets upstream's own check decide.
|
||||
|
||||
**Apply the interruption guard to the backslash construct too.** The guard exists to protect the construct tried after this one; `\[`/`\(` have no second construct, and an unconditional guard dropped their multi-line blocks (`Prose line` followed by `\[`).
|
||||
|
||||
## Consequences
|
||||
|
||||
The three shapes now produce the same math node, so formulas stop truncating at the opening line and stop swallowing the prose after them. `$$` fence meta is no longer honored: `$$asciimath$$` parses as a formula whose value is `asciimath`, which upstream's flow treats as fence meta and drops. That spelling has no user in this repository, and the renderer never read `meta` anyway.
|
||||
|
||||
Six inputs still reach upstream's flow or stay in the surrounding paragraph, because the compatibility flow declines them. Trailing text after a closing `$$` (`$$x$$ trailing`, or a CJK quote mark closing a line of prose) resolves through upstream inline math, as the existing `markdown.client.spec.tsx` case pins — including when a later `$$` block follows. A `$$` sequence the line does not end after declines the block: immediately on a later line, and at that line's ending on the opening line. A `$$` block left open runs to the next lone `$$` or to EOF exactly as it did before this change, because the upstream flow closes at EOF.
|
||||
|
||||
The fourth is a multi-line block whose opening line carries a solitary `$` and that must interrupt a paragraph, with no blank line before it. Both flows decline the interruption there — this one by design, upstream because its fence meta stops at the `$` — so the block stays inside the paragraph and renders as inline math or not at all. Separating the block from the paragraph with a blank line makes this flow own it and render the display block.
|
||||
|
||||
The fifth follows from the comment gate: a block whose only closing fence sits behind a comment that holds text keeps scanning instead of closing there, so when nothing else closes it the block reaches the upstream flow as the shapes above do. A fence behind a whitespace-only comment closes the block there, which leaves a following lone `$$` as its own empty block where the flow it replaced kept one node.
|
||||
|
||||
The sixth is a multi-line block inside a container whose continuation line falls back to the container's indentation. `- Define:` followed by `$$f(x)=x` at the item's indentation and then a de-indented continuation line gives upstream's flow a fence meta and no value, so the reader sees an empty display block and the continuation as prose — the opening line is gone. Keeping the continuation at the same indentation, or moving the block out of the list, renders it normally. Both flow behaviors match the grammar this replaced; only the multi-line blocks this flow owns are new.
|
||||
|
||||
One further limitation is not a fallback: the flow reads the six characters before a fence and does not parse verbatim regions, so a fence inside an unterminated region (`\verb|$$`) closes the block there, and a region whose text ends in `\verb` (`\verb**\verb*$$`) has its fence read as that text's delimiter. The six characters before either fence are locally identical, so no rule that reads only that tail separates them.
|
||||
|
||||
Repeated unclosed candidates keep linear parser work: an open candidate declines at the next line that starts with a full `$$` it cannot close, rather than scanning each candidate to EOF, which would make 9.6 KB of `$$a$b` lines take ~4 s instead of ~16 ms. KaTeX-level failures in a formula's own source, such as `\arccot`, remain errors by design.
|
||||
|
||||
## Testing
|
||||
|
||||
`packages/client/ui-primitives/tests/markdown.client.spec.tsx` adds the multi-line case — opening-line math closed on a content line, closed by a lone fence, after a paragraph, inside a list item, and at column zero after a list item — and that case fails without the change. The same file also pins a multi-line formula that uses a solitary `$`, which like the case above renders only with this change. Further cases pin one condition each: a trailing-text candidate ahead of a later block (the flow must decline a full `$$` the line does not end after), a solitary `$` inside a single-line display formula (the flow must not decline on a single `$`), an escaped-dollar candidate that leaves its paragraph intact (the interruption check must not claim the paragraph), an unclosed `\[` candidate that leaves its paragraph intact (the guard must not apply to the backslash construct), a candidate ending in an odd backslash (the escape run must end at the line ending), a closing fence inside a TeX comment, a `%` the fence follows immediately or after whitespace, a second `%` inside an open comment, a `%` inside `\verb|…|`, the delimiter right after `\verb` and `\verb*` together with its empty region, an escaped backslash before `verb`, a verb tail that crosses no line ending, a comment ending in `verb`, a later-line trailing fence whose following prose upstream takes, a failed fence inside a comment, and bounded-work cases for repeated candidates with that backslash, without it, and ending in a verb tail.
|
||||
|
||||
The DOM compatibility corpus adds `math-multiline-display` with its `.settled.txt` and `.streaming.txt` fixtures, which also pin that a column-zero `$$` after a list item stays outside the list item. `apps/web/tests/math-rendering.e2e.ts` renders the multi-line shape in the browser lane.
|
||||
@@ -1,66 +0,0 @@
|
||||
# Agent Note: Multi-line display-dollar math
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-17-multiline-display-dollar-math.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
作者和模型写 `$$` 行间公式时有三种写法。定稿语法只渲染其中两种:一行内开闭的 `$$math$$`,以及 `$$` 独占一行、由独占一行的 `$$` 闭合。第三种——开围栏同一行就带公式内容,闭合 `$$` 落在最后一行公式行行尾——不渲染:
|
||||
|
||||
```markdown
|
||||
$$f(x)=x
|
||||
\text{continued}$$
|
||||
```
|
||||
|
||||
`micromark-extension-math` 把开围栏 `$$` 之后的同一行文本当作围栏 meta,并且只接受内容恰为 `$$`(后接可选空白)的行作为闭合围栏。[markdown 兼容层](../../archived/feature/2026-07-23-web-assistant-markdown.md)只认同一行开闭这一种形状,而定稿渲染器把 `node.value` 交给 KaTeX,不读 `node.meta`。于是该块始终不闭合:它吞掉后续正文,`value` 里残留字面 `$$`,KaTeX 抛出 `Can't use function '$' in math mode`,[katex.tsx](../../../../packages/client/ui-primitives/src/markdown/katex.tsx) 的错误分支把 TeX 原文连同被吞掉的正文一起显示为红色。
|
||||
|
||||
在暴露该问题的会话中,一条聊天回复 55 行里有 34 行(第 22–55 行)变成同一个错误块,六个笔记文件从出错点起的所有行(最多约 500 行)同样变成一个错误块。同一机制还带出第二个缺陷:当更靠后的独占行 `$$` 恰好闭合了这个流时,开围栏那一行已经归给 `meta`,渲染器于是静默丢掉公式的第一行。
|
||||
|
||||
## Decision
|
||||
|
||||
[mathCompatibility.ts](../../../../packages/client/ui-primitives/src/markdown/mathCompatibility.ts) 现在支持跨行的显示美元块。dollar 构造把开围栏同一行的文本留在 `mathFlowValue` 里而不是当作围栏 meta,并在后续任意内容行行尾的 `$$` 处闭合,于是 `$$a+b` 接 `c+d$$` 成为一个 math 节点。
|
||||
|
||||
闭合失败的 `$$` 序列会结束该候选:出现在后续行时立即结束;出现在开围栏行时在该行行尾结束(`$$x$$ trailing` 即在此放弃)。在开围栏行内扫描继续,因此同一行更靠后的 `$$` 仍能闭合(`$$\verb*a$$a$$`)。越过这两点继续扫描都会吞掉更靠后一个块的闭合围栏。孤立的一个 `$` 属于公式内容而不是失败的围栏,所以凡是本 flow 拥有该块的场合,用到它的公式——`$$P = \begin{cases} 1 & \text{if $x>0$} \end{cases}$$`——保持块打开并正常闭合。行尾反斜杠转义的是换行本身,不是下一行的第一个字符,因此转义状态在换行处结束,不会延续到下一个候选。未转义的 `%` 会开启延续到行尾的 TeX 注释:当注释在围栏之前还有文本时,块后续行行尾的 `$$` 不闭合任何东西;而围栏之前只有空白或什么都没有的 `%`(`95%$$`、`95 %$$`)不含注释文本,且在一行内开闭的块仍在该行的 `$$` 处闭合,与它跨行之前的行为一致。该状态及其对闭合判定的门控属于 dollar flow。`\verb` 或 `\verb*` 之后的那个字符是该命令的分隔符而不是围栏,因此该分隔符、以及区域为空时紧随其后的那个 `$` 都属于内容;这段尾部状态在换行处、以及在被转义的反斜杠之后清空(`\\verb` 是换行加字母,不是该命令),注释里也不识别命令。除此之外不解析原样文本区域,使含 `\verb` 的公式与其它输入走同一条 value 路径。
|
||||
|
||||
它的段落打断检查改为「不认领」,且只作用于 dollar 构造。在那里消耗换行会让段落跨行边界保持打开,document 层因此保留容器并把下一行对随后尝试的所有构造计为 lazy;这正是上游 `mathFlow`(本构造之后被尝试的构造)在列表项内以空块结束的原因。不认领同时把打断决定交给上游 flow:它自己的检查不消耗换行就短路返回,并且只认领它接受的围栏,因此两个构造都不接受的候选(`**before` 后接 `$$a\$b`)保持原段落。反斜杠构造没有竞争的上游构造,仍然照旧消耗换行。
|
||||
|
||||
[parse.ts](../../../../packages/client/ui-primitives/src/markdown/parse.ts) 把 `mathCompatibility()` 注册在 `math()` 之后:micromark 先派发最后注册的扩展,上面的 dollar flow 因此优先于上游 `mathFlow`,而行内 `$…$` 仍由 `math()` 负责。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**兼容层维持只处理同一行 `$$…$$`。** 多行写法是 GitHub、KaTeX auto-render、VS Code、Typora 都接受的形状,也是模型写多分支公式时实际采用的形状;不解析它意味着公式位置显示为红色错误块,并连带吞掉后文。
|
||||
|
||||
**解析后修补语法树。** 被吞掉的正文已经进入 `mathFlowValue`,与数学源码无法区分;`meta` 和 `value` 无法还原成原意的块,而且后面的块根本没有被解析。
|
||||
|
||||
**解析前用文本替换改写 `$$` 围栏。** 兼容层的设计就是语法扩展;文本替换需要自己定位代码 span、围栏和转义,而且改的是作者原文而不是解析它。
|
||||
|
||||
**闭合失败后继续扫描。** 这正是单纯打开 `multiline` 会得到的行为:`$$x$$ trailing\n\n$$y$$` 会一路扫到第二个块的 `$$`,渲染成一个同时含着两行文本的 math 节点;而上游行内数学会把 `x`、正文和 `y` 分开渲染。`$$x$$ trailing` 的既有行为已经固定了这条回退,因此该构造放弃这个候选。对任意一个 `$`(而不是完整的 `$$` 序列)就放弃,还会破坏在 `\text{}` 内使用单个 `$` 的公式。
|
||||
|
||||
**只要有一行解析不下去就结束尝试。** 对每次同一行闭合失败都放弃虽然不再吞文,却会让 `$$a$b` 这类行变成平方级扫描,并丢掉上面那些含单个 `$` 的公式。判据是完整的 `$$` 序列,不是那个字符。
|
||||
|
||||
**在检查阶段认领段落打断。** 像上游 flow 那样返回成功可以避免破坏列表用例的 lazy 记账,但也会让最终失败的候选提前结束段落(`**before\n$$a\$b\nafter**` 被拆成两段并暴露 `**`)。改为不认领,把决定交给上游自己的检查。
|
||||
|
||||
**把打断守卫也用在反斜杠构造上。** 这个守卫是为了保护随后被尝试的构造;`\[`/`\(` 没有第二个构造,无条件套用会丢掉它们的跨行块(`Prose line` 后接 `\[`)。
|
||||
|
||||
## Consequences
|
||||
|
||||
三种写法现在产生同一个 math 节点,公式不再在开围栏那一行被截断,也不再吞掉其后的正文。`$$` 围栏 meta 不再生效:`$$asciimath$$` 会解析成 value 为 `asciimath` 的公式,而上游 flow 会把该段当作 fence meta 丢弃。这种写法在本仓库没有使用者,且渲染器本来就从不读 `meta`。
|
||||
|
||||
有六种输入仍会走到上游 flow,或留在所属段落里,因为兼容 flow 主动放弃它们。闭合 `$$` 之后还有文本时(`$$x$$ trailing`,或用中文引号收尾的散文行)走上游行内数学,这一点由 `markdown.client.spec.tsx` 中已有用例固定,后面还有别的 `$$` 块时同样如此。「后面不是行尾」的 `$$` 序列会让块放弃:出现在后续行时立即放弃,出现在开围栏行时在该行行尾放弃。未闭合的 `$$` 块仍像本次改动之前那样延伸到下一个独占行 `$$` 或文件结尾,因为上游 flow 在 EOF 处闭合。
|
||||
|
||||
第四种是「开围栏行含孤立 `$`、且需要打断段落、前面又没有空行」的多行块。两个 flow 都不认领这次打断——本 flow 出于设计,上游则因为它的围栏 meta 在 `$` 处即停止——该块因此留在段落里,渲染成行内数学或根本不渲染。用空行把块与段落分开,本 flow 就会拥有它并渲染 display。
|
||||
|
||||
第五种来自注释门控:当块唯一的闭合围栏位于「带文本的注释」之后时,块继续扫描而不是在那里闭合,若再没有别的围栏闭合它,就会像上面几种一样走到上游 flow。若围栏之前只有空白,则块在该处闭合,此后单独一行的 `$$` 会成为它自己的空块——而被替换掉的语法那时只给出一个节点。
|
||||
|
||||
第六种是容器内续行退回容器缩进的多行块。`- Define:` 之后以列表项缩进写 `$$f(x)=x`,再接一行不缩进的续行,上游 flow 会得到有 meta、无 value 的结果:读者看到一个空的 display 块加一行散文,公式首行无声消失。续行保持同样缩进,或把整块移到列表之外,即可正常渲染。这两种 flow 行为都与本次替换掉的语法一致;真正新增的只是「本 flow 拥有的多行块」。
|
||||
|
||||
另有一项不属于回退的限制:flow 只读取围栏之前的六个字符、不解析原样文本区域,因此未闭合区域内部的围栏(`\verb|$$`)会在那里闭合块,而内容以文本 `\verb` 结尾的区域(`\verb**\verb*$$`)其围栏会被读成该文本的分隔符。两种情况下围栏前的六个字符在局部完全相同,仅凭这段尾部无法区分。
|
||||
|
||||
重复的未闭合候选保持线性解析开销:未闭合的候选会在下一行遇到无法闭合的完整 `$$` 时放弃,而不是把每个候选都扫到 EOF——否则 9.6 KB 的 `$$a$b` 行会花约 4 秒而不是约 16 毫秒。公式自身源码层面的 KaTeX 失败,例如 `\arccot`,仍按设计报错。
|
||||
|
||||
## Testing
|
||||
|
||||
`packages/client/ui-primitives/tests/markdown.client.spec.tsx` 新增多行用例——开围栏行内容由内容行行尾 `$$` 闭合、由独占行 `$$` 闭合、出现在段落之后、出现在列表项内,以及列表项之后的零缩进围栏——该用例去掉改动即失败。同一文件还固定了「使用单个 `$` 的多行公式」,它与上面的多行用例一样只有本次改动才会渲染成 display。另有一组用例各自对应一个条件:后面还有块时的尾随文本候选(flow 必须放弃「后面不是行尾」的完整 `$$`)、单行显示公式内的单个 `$`(不得因为一个 `$` 就放弃)、保持段落完整性的转义美元候选(打断检查不得认领段落)、保持段落完整性的未闭合 `\[` 候选(守卫不得作用于反斜杠构造)、以奇数个反斜杠结尾的候选(转义状态必须在换行处结束)、TeX 注释内的闭合围栏、紧邻围栏或只隔空白的 `%`、已开启注释内的第二个 `%`、`\verb|…|` 内的 `%`、`\verb` 与 `\verb*` 之后的分隔符及其空区域、`verb` 之前的转义反斜杠、不跨行的 verb 尾部、以 `verb` 结尾的注释、后续行尾随文本围栏(其后正文由上游接管)、注释内失败的围栏,以及带该反斜杠、不带该反斜杠、以 verb 尾部结尾三种重复候选的有界解析开销。
|
||||
|
||||
DOM 兼容语料新增 `math-multiline-display` 及其 `.settled.txt`、`.streaming.txt`,同时固定了「列表项之后的零缩进 `$$` 不进入列表项内」。`apps/web/tests/math-rendering.e2e.ts` 在浏览器 lane 渲染多行写法。
|
||||
@@ -0,0 +1,22 @@
|
||||
# Bilingual-pair consistency record for 2026-11-01-locked-python-runtime-deploy.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-11-01-locked-python-runtime-deploy.md
|
||||
/agent-note-locked-python-runtime-deployment:
|
||||
en: 6a06f1078957df51
|
||||
zh: d7c2146381443ced
|
||||
/agent-note-locked-python-runtime-deployment/problem:
|
||||
en: 2293006c8bae6a1a
|
||||
zh: 10670f2d703777dd
|
||||
/agent-note-locked-python-runtime-deployment/decision:
|
||||
en: 223079c8e1e3a44e
|
||||
zh: c906266cb0b231bc
|
||||
/agent-note-locked-python-runtime-deployment/alternatives-considered:
|
||||
en: 978c94c7013056a5
|
||||
zh: afae764120582b6a
|
||||
/agent-note-locked-python-runtime-deployment/testing:
|
||||
en: d862dd609725c686
|
||||
zh: 8889c5d9002bbc68
|
||||
/agent-note-locked-python-runtime-deployment/consequences:
|
||||
en: 41827db476b30a43
|
||||
zh: 5a7652f310a69909
|
||||
@@ -0,0 +1,33 @@
|
||||
# Agent Note: Locked Python runtime deployment
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-11-01-locked-python-runtime-deploy.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Python executable packaging needs the same dependency versions and integrity records as the workspace lockfile. Legacy hoisted deployment ignores that lockfile and resolves compatible registry versions again: a manifest permitting `tslib@^2.8.1` can install `2.9.1` while the lockfile records `2.8.1`. Office archive preparation then rejects the missing locked integrity. A successful frozen workspace install does not constrain that later legacy deployment.
|
||||
|
||||
## Decision
|
||||
|
||||
The Python builder uses pnpm's default deployment, which derives a dedicated production lockfile from the workspace lockfile and installs it frozen. This replaces only the deployment strategy in the [single-executable distribution decision](../architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md); its runtime architecture and carrier layout remain active.
|
||||
|
||||
Deployment retains the source lockfile's peer settings, including `autoInstallPeers: true`. Frozen installation preserves already resolved versions and peer relationships instead of resolving new registry peers. Forcing `autoInstallPeers: false` conflicts with the derived lockfile settings and fails frozen installation. The resulting tree need not equal the legacy tree resolved with peers disabled; selected dependencies must come from the locked graph. `verify-runtime-closure.ts` independently requires explicit workspace peers and preset plugins before deployment, and `verifyStagedDependencies` rejects missing direct dependency directories after materialization and deduplication.
|
||||
|
||||
[`scripts/python-runtime-deploy.pnpmfile.mjs`](../../../../scripts/python-runtime-deploy.pnpmfile.mjs) translates reviewed local `file:` approvals to the exact absolute file URLs used in the derived lockfile. Approval, denial, pending decisions, and strict build validation remain unchanged. Default deployment executes approved installation scripts; their generated files can enter the staged payload. Workspace policy and the source lockfile remain unchanged.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep legacy deployment and add missing integrity records.** This would accept versions the source lockfile did not select and repeat the failure after another compatible release.
|
||||
|
||||
**Disable automatic peer installation during default deployment.** This conflicts with the source lockfile settings. Re-resolving a separate graph would give up the source lockfile's authority.
|
||||
|
||||
**Disable deployment scripts or approve local packages by name.** Skipping scripts omits required postinstall preparation. Name-only approvals do not authorize non-semver local file dependencies; exact identity translation preserves the existing review decisions.
|
||||
|
||||
## Testing
|
||||
|
||||
A real pnpm comparison with the same manifest and lockfile installs `[email protected]` through legacy deployment and retains `2.8.1` through default deployment. The Linux executable and locally built installed-wheel SDK smokes pass. Focused tests reject missing dependency directories and deployment flag regressions, preserve approval states, and exercise both metadata path bases. Cross-platform packaging and installed-wheel validation remain owned by CI.
|
||||
|
||||
## Consequences
|
||||
|
||||
Runtime versions remain traceable to the workspace lockfile, and incomplete direct dependency trees fail before executable packaging. Installation script outputs and locked peer selections can differ from legacy deployment, so byte-identical payloads are not promised. SSH helper deployment has a separate closure manifest and native payload validation; its legacy deployment is outside this Python runtime repair.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Agent Note: Python 运行时按锁文件部署
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-11-01-locked-python-runtime-deploy.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Python 可执行文件打包需要使用与工作区锁文件相同的依赖版本和完整性记录。legacy hoisted 部署忽略该锁文件,重新解析兼容的注册表版本:允许 `tslib@^2.8.1` 的 manifest 可能安装 `2.9.1`,而锁文件记录的是 `2.8.1`。Office 归档准备随后因缺少锁定的完整性记录而拒绝构建。工作区的冻结安装成功,并不能约束后续的 legacy 部署。
|
||||
|
||||
## 决策
|
||||
|
||||
Python 构建器使用 pnpm 的默认部署,从工作区锁文件派生独立的生产锁文件,再冻结安装。本记录只替代[单文件可执行程序分发决策](../architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md)中的部署策略;其中的运行时架构和载体布局继续有效。
|
||||
|
||||
部署保留源锁文件的对等依赖设置,包括 `autoInstallPeers: true`。冻结安装保留已解析的版本和对等依赖关系,不会重新解析新的注册表对等依赖。强制指定 `autoInstallPeers: false` 与派生锁文件的设置冲突,会使冻结安装失败。生成的依赖树不必与关闭对等依赖解析的 legacy 依赖树相同,但所选依赖必须来自锁定的依赖图。`verify-runtime-closure.ts` 独立要求在部署前显式声明工作区对等依赖及 preset 插件;`verifyStagedDependencies` 在链接实体化与去重后拒绝缺失的直接依赖目录。
|
||||
|
||||
[`scripts/python-runtime-deploy.pnpmfile.mjs`](../../../../scripts/python-runtime-deploy.pnpmfile.mjs) 将已审阅的本地 `file:` 审批转换为派生锁文件使用的精确绝对文件 URL。批准、拒绝、未决定状态和严格构建校验均保持不变。默认部署执行已批准的安装脚本,其生成的文件可能进入暂存载荷。工作区策略和源锁文件保持不变。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**保留 legacy 部署并补全缺失的完整性记录。** 这会接受源锁文件没有选择的版本,并在另一个兼容版本发布后重复出现故障。
|
||||
|
||||
**在默认部署中关闭对等依赖自动安装。** 这与源锁文件的设置冲突;重新解析另一套依赖图会放弃源锁文件的决定权。
|
||||
|
||||
**禁用部署脚本,或按名称批准本地包。** 跳过脚本会遗漏必需的 postinstall 准备。仅按名称批准不能授权非 semver 的本地文件依赖;转换精确标识可以保留原有审批决定。
|
||||
|
||||
## 测试
|
||||
|
||||
真实 pnpm 对照使用相同 manifest 和锁文件,legacy 部署安装 `[email protected]`,默认部署保留 `2.8.1`。Linux 可执行文件及本地构建并安装的 wheel SDK 冒烟测试通过。定向测试拒绝缺失的依赖目录和部署参数回退,保留审批状态,并覆盖两种 metadata 路径基准。跨平台打包及安装后 wheel 校验仍由 CI 负责。
|
||||
|
||||
## 影响
|
||||
|
||||
运行时版本可以追溯到工作区锁文件,不完整的直接依赖树会在可执行文件打包前失败。安装脚本产物和锁定的对等依赖选择可能与 legacy 部署不同,因此不承诺载荷逐字节相同。SSH helper 部署使用独立的闭包 manifest 和原生载荷校验,其 legacy 部署不属于本次 Python 运行时修复范围。
|
||||
+2
-2
@@ -9,8 +9,8 @@
|
||||
en: a72071eeedb77402
|
||||
zh: 818d16ce429fd610
|
||||
/agent-note-continuable-subagents/decision:
|
||||
en: 3420195435e02a8f
|
||||
zh: 6156eedb3c23d360
|
||||
en: 6ba1bf6184794aa8
|
||||
zh: 71b4e41556dafb93
|
||||
/agent-note-continuable-subagents/decision/materialization-and-public-operations:
|
||||
en: c3a83ff748a02143
|
||||
zh: 468bf62c8ddabf70
|
||||
|
||||
@@ -18,6 +18,8 @@ Parent Agents need to send later work to the same live child without changing it
|
||||
|
||||
## Decision
|
||||
|
||||
Human control addressing and permissions are superseded by [target Session user controls](../architecture/2026-10-22-subagent-target-user-controls.md); model-message adjacency and parent-owned runtime restrictions are superseded by [tree-wide communication](../architecture/2026-10-16-tree-wide-agent-communication.md). The direct-parent authorization statements below do not govern those current operations.
|
||||
|
||||
The shared activation entry point, external execution ownership, and caller-versus-parent result delivery are governed by [Unified subagent activations](../simplification/2026-09-17-unified-subagent-activations.md). This record retains the independent rationale described below.
|
||||
|
||||
A continuable subagent has one durable Session and at most one process-local Activation:
|
||||
|
||||
@@ -18,6 +18,8 @@ parent Agent 还需要在不改变当前轮次的前提下,向同一个在线
|
||||
|
||||
## 决策
|
||||
|
||||
人工控制的地址和权限由[目标 Session 用户控制](../architecture/2026-10-22-subagent-target-user-controls.zh.md)取代;模型消息的相邻关系限制和父级运行时所有权限制由[整树通信](../architecture/2026-10-16-tree-wide-agent-communication.zh.md)取代。下文的直接父级授权表述不约束这些当前操作。
|
||||
|
||||
共享 activation 入口、外部执行所有权,以及调用方与父级之间的结果投递选择由[统一 subagent activation](../simplification/2026-09-17-unified-subagent-activations.zh.md) 决策拥有。本记录保留下述独立理由。
|
||||
|
||||
一个可继续 subagent 拥有一个持久化会话,并且至多拥有一个进程内激活:
|
||||
|
||||
@@ -6,35 +6,35 @@
|
||||
en: c914d8148bfc4bf9
|
||||
zh: a8115f87c0ac6cae
|
||||
/agent-note-durable-agent-teams-over-continuable-children/problem:
|
||||
en: bab62798c2dc07b4
|
||||
zh: fe42c5abf9127d37
|
||||
en: 567b68ab8eb4b5b2
|
||||
zh: 31a40aa22821d63a
|
||||
/agent-note-durable-agent-teams-over-continuable-children/decision:
|
||||
en: 35650a30529c01f8
|
||||
zh: 4f9ee01b568d2fed
|
||||
en: affeddb8788ea73b
|
||||
zh: 73615d3ccbd71871
|
||||
/agent-note-durable-agent-teams-over-continuable-children/profile-delegation:
|
||||
en: 4835fb8c0c0d8a0d
|
||||
zh: 5f9438a0d58104c4
|
||||
en: f4c8d8760c7ebcf4
|
||||
zh: 32ccca202a7769ac
|
||||
/agent-note-durable-agent-teams-over-continuable-children/team-identity:
|
||||
en: f82812d6ac0ac78d
|
||||
zh: 7dd80a6ad13674ca
|
||||
en: d79f4425f2c1929f
|
||||
zh: 6586410db4a02cd1
|
||||
/agent-note-durable-agent-teams-over-continuable-children/provisioning-and-recovery:
|
||||
en: 4345ac9a6ac7ad04
|
||||
zh: 1d19ecbb128a75a3
|
||||
en: 624e9d42af1af859
|
||||
zh: 020dcbb26211ea67
|
||||
/agent-note-durable-agent-teams-over-continuable-children/mailbox-and-task-transactions:
|
||||
en: bfc3063536837c0e
|
||||
zh: 6b8e69ba85cc2bb1
|
||||
en: bfa07b5bc6354360
|
||||
zh: c82119c0a5ea7f01
|
||||
/agent-note-durable-agent-teams-over-continuable-children/shared-checkout-boundary:
|
||||
en: 34920da68b5763e0
|
||||
zh: 7604392594e4b8a8
|
||||
en: 384f75638b0f1fb6
|
||||
zh: b7ab1191784d3d27
|
||||
/agent-note-durable-agent-teams-over-continuable-children/web-projection:
|
||||
en: e6348054f024725b
|
||||
zh: 8740636e99940308
|
||||
en: 1dc1386f5033e83b
|
||||
zh: 533803da51f3716f
|
||||
/agent-note-durable-agent-teams-over-continuable-children/alternatives-considered:
|
||||
en: 8cb2fd8839f5f8b7
|
||||
zh: e302ca03e569de65
|
||||
en: 087ffae4744a129b
|
||||
zh: 842dc1ded7f8d9c3
|
||||
/agent-note-durable-agent-teams-over-continuable-children/testing:
|
||||
en: 092ac3c2b6f552aa
|
||||
zh: c688c1bec5ad8a07
|
||||
en: 2ff7adaaaa65fad7
|
||||
zh: 9c7dd682abe988d1
|
||||
/agent-note-durable-agent-teams-over-continuable-children/consequences:
|
||||
en: da9cd22b1185a793
|
||||
zh: e20d7ef9226f237f
|
||||
en: c071fa2ccdb911b5
|
||||
zh: 3b8cf0d3303a4d40
|
||||
@@ -10,47 +10,41 @@ The subagent seam supplies fresh/fork providers, durable child Sessions, FIFO fo
|
||||
|
||||
All same-process Agents also share one checkout. Filesystem edit tools can reject an observed stale version, but Bash, formatters, generators, and external writers bypass that fence. Treating a teammate name or task owner as a file lock would hide rather than solve this concurrency boundary.
|
||||
|
||||
Agent Teams needs an explicit opt-in composition while its public contracts remain experimental. The default tool catalog and simple-task behavior must remain unchanged, while an explicitly requested Team must survive child Activation settlement and mailbox delivery races long enough for the Lead to aggregate the result before process teardown.
|
||||
Team collaboration requires explicit opt-in; [Session selection](../architecture/2026-10-20-session-team-mode.md) owns its granularity and default composition. The default tool catalog and simple-task behavior must remain unchanged, while an explicitly requested Team must survive child Activation settlement and mailbox delivery races long enough for the Lead to aggregate the result before process teardown.
|
||||
|
||||
## Decision
|
||||
|
||||
Every ordinary runtime root is the implicit Lead of a Team identified by that root's `SessionId`. The Team has no creation event: its Lead pseudo-row exists by identity, while durable state begins with the first member, message, or task event. A roster is flat and contains at most the configured number of immutable lowercase-kebab-case names. Each teammate is a continuable direct child with a reserved Session id; only the Lead creates or interrupts teammates. Ordinary provider-owned subagents outside the roster are not Team members, and an ordinary fork is a new root whose inherited Team records are excluded by their ancestor `TeamId`.
|
||||
Every ordinary root and its descendants form the Team identified by the root `SessionId`; durable task state begins with the first task event. [Subagent teammates](../architecture/2026-10-15-agent-teams-as-subagents.md) owns the single agent identity. [Inbox-only waiting](../simplification/2026-10-29-inbox-only-agent-wait.md) keeps task-board changes independent of inbox waiting. An ordinary Session fork leads a new tree and excludes inherited Team authority.
|
||||
|
||||
The implementation is split into `@deepseek-ai/dsh-experimental-agent-team`, which owns `ctx.agentTeams` and durable semantics, and `@deepseek-ai/dsh-experimental-tool-agent-team`, which owns scoped schemas and model guidance. Every Team tool declares its complete result schema and renders that value as compact JSON, so the compiler checks each `execute` against what the model is promised and no result spends tokens on indentation. Deployments mount both plugins explicitly and may disable legacy continuable controls with the same model-visible names. The shared policy announces Team availability and describes coordination responsibilities. Both packages are public members of `packages/experimental/`; the [experimental package reference](../../../../packages/experimental/AGENTS.md) owns publication, dependency isolation, and promotion.
|
||||
The implementation is split into `@deepseek-ai/dsh-agent-team`, which owns `ctx.agentTeams` and durable semantics, and `@deepseek-ai/dsh-tool-agent-team`, which owns scoped schemas and model guidance. Every Team tool declares its complete result schema and renders that value as compact JSON, so the compiler checks each `execute` against what the model is promised and no result spends tokens on indentation. The base bundle mounts both plugins; Team registers no tool that shares a name with the subagent controls. The shared policy announces Team availability and describes coordination responsibilities. Both packages belong to the [subagent family](../../../../packages/subagent/README.md); the root Session’s saved selection gates the task tools and policy. The [Session-mode upgrade guide](../../../../docs/upgrade-guide/v0.2.1-rc.2/session-team-mode/guide.md) describes profile migration.
|
||||
|
||||
The Lead must wait for required work before its final answer. Process teardown remains the final lifecycle owner and drains continuation Activations; a Team task owner is durable state and is not automatically released by idle, interruption, or process exit.
|
||||
|
||||
## Profile delegation
|
||||
|
||||
The [Team profile](../../../../packages/experimental/agent-team-profile/README.md) disables `subagent` and `subagent_fork` together with the overlapping global controls. Direct model delegation uses `spawn_teammate` with fresh or fork context, keeping those children in the durable roster. Workflow remains available through the base profile’s fresh `spawn` provider for scripted orchestration; it cannot inherit a teammate’s conversation identity through the model tool. The Subagent services and providers remain shared infrastructure. Ordinary Session forks retain their history; [current identity handling](../architecture/2026-10-13-team-identity-runtime-context.md) supplies the fork’s own role.
|
||||
The [base bundle](../../../../packages/bundle/base/README.md) inserts the Team rows and keeps the base delegation and control tools; teammates are started with those tools as described in [subagents](../architecture/2026-10-15-agent-teams-as-subagents.md). Ordinary Session forks retain their history; [current identity handling](../architecture/2026-10-13-team-identity-runtime-context.md) supplies the fork's own role.
|
||||
|
||||
## Team identity
|
||||
|
||||
Shared system policy and all tool schemas stay uniform across members; execution owns role restrictions. Team tools resolve the caller’s Team and accept member names, so the model needs no Team id. [Required runtime-context identity](../architecture/2026-10-13-team-identity-runtime-context.md) owns identity injection, retention, and fork handling.
|
||||
Shared system policy and tool schemas stay uniform across members; execution enforces role restrictions. Team tools resolve the caller’s Team and use Session ids as task owners. [Required runtime-context identity](../architecture/2026-10-13-team-identity-runtime-context.md) owns identity injection, retention, and fork handling.
|
||||
|
||||
## Provisioning and recovery
|
||||
|
||||
Creation first appends and flushes a `team/member` provisioning snapshot in the Lead Session, then starts the reserved continuable child through the selected fresh or fork provider. Failure before initial inbox acceptance appends a failed snapshot. Success flushes the child's accepted inbox item before appending active. Recovery recognizes that initial message while it is still pending or after it enters user-message history. Names are reserved by the first provisioning record and never reused, including after failure. Disposal closes admission, aborts and awaits admitted creation and send operations, then stops every live child recorded by the roster; a failed child remains cleanup-owned until its Activation exits, and cleanup rejection fails disposal.
|
||||
|
||||
A root recovery reconciles an unterminated provisioning record against the child's independently persisted Session. Matching direct-parent and continuable descriptors plus a recorded initial user message prove successful admission and produce active; absence, corruption, mismatched provider/lineage, or a missing admitted message produces failed. The creator re-reads the terminal phase under the same Lead-log serializer; if recovery marked failed while creation succeeded, it drains the child and reports a provisioning conflict instead of retaining an orphan. This avoids reconstructing an initial prompt that was never retained in the Team log and contains plugin-reload races.
|
||||
|
||||
Fresh children have no inherited conversation. Fork children capture the Lead's completed-turn prefix once and retain it as their own durable seed. The current delegation turn remains excluded, matching the existing fork provider contract.
|
||||
Teammate creation and cold resume belong to the subagent runtime ([subagent teammates](../architecture/2026-10-15-agent-teams-as-subagents.md)). Historical `team/member` records remain readable with their phases and descriptions; Team writes no new membership records.
|
||||
|
||||
## Mailbox and task transactions
|
||||
|
||||
The historical Lead-log mailbox remains a readable format: queued and delivered records keep their stable identity and sender, and Team payload validation still applies to them. Team no longer delivers or acknowledges them; new sends follow the [direct inbox decision](../simplification/2026-09-26-team-direct-inbox.md).
|
||||
The historical Lead-log mailbox remains readable in original Session logs. The [task-only projection decision](../simplification/2026-10-29-team-task-only-projection.md) supersedes mailbox projection, checkpoint retention, and Team payload validation. Historical queued messages are never delivered or acknowledged.
|
||||
|
||||
Unknown mailbox content stays as decoded JSON during event projection and checkpoint admission. Validation inspects its type without rebuilding its fields, because generic object parsing can omit a valid own `__proto__` key. Locally declared content variants undergo structural validation; opaque plugin fields do not gain Team semantics. Cache invalidation is necessary because parser correction alone cannot recover keys omitted from cached state. The [Team documentation](../../../../packages/experimental/agent-team/README.md) owns the restore behavior.
|
||||
|
||||
`send_message` uses Steer for running, idle, and cold targets. New sends return inbox acceptance. The [Team Steer messaging decision](../../archived/simplification/2026-08-30-team-send-message-steer.md) records the single-tool scheduling rationale.
|
||||
Member messages use the subagent `send_message` operation, which steers running, idle, and cold targets and returns inbox acceptance.
|
||||
|
||||
Shared tasks are complete snapshots with Team-local ids and monotonic revisions. Every mutation carries `expectedRevision`. Any member creates, reads, or claims a ready unowned task; the owner or Lead edits and transitions it, while only the Lead assigns another member. Numeric task ids remain within the safe-integer allocation range, and exhaustion fails without reusing an id. Dependencies must name non-deleted tasks and form a complete DAG. Deleted tasks are retained tombstones. `writeScopes` are normalized path prefixes that produce overlap diagnostics but never block claim or authorize a write.
|
||||
|
||||
`wait_agent` blocks on one roster, mailbox, task, or live-status edge registered after the call starts instead of encouraging model polling. It does not replay an earlier edge, so callers re-read authoritative state after wakeup or timeout. Lead-only interruption cancels the current turn with inbox preservation and does not alter mailbox or task ownership.
|
||||
`wait_agent` belongs to ordinary subagent controls and waits for the exact caller’s next-step inbox, including already-pending input. Task commits do not end waits, deliver messages, or start idle turns. Agents send messages when another member needs to act on a task change. Ancestors may interrupt descendants without changing task ownership.
|
||||
|
||||
## Shared checkout boundary
|
||||
|
||||
Members start in the Lead’s current directory and observe shared filesystem writes immediately. Later directory changes stay local to each Session. The policy tells members to partition tasks, record advisory write scopes, order dependent work, and let the Lead inspect the final diff and run tests. A filesystem stale-version rejection requires rereading and rebasing the intended change. No equivalent guarantee is claimed for Bash, formatters, code generation, or direct external writes.
|
||||
Members start in the Lead’s current directory and observe shared filesystem writes immediately. Later directory changes stay local to each Session. The task policy tells members to claim ready tasks, use advisory write scopes and dependencies, and complete tasks after doing the work. A filesystem stale-version rejection requires rereading and rebasing the intended change. No equivalent guarantee is claimed for Bash, formatters, code generation, or direct external writes.
|
||||
|
||||
The Team domain does not infer branches, create worktrees, merge changes, or silently change cwd. A deployment or caller can use [explicit worktree creation](2026-09-13-explicit-worktree-creation.md) separately. This preserves the existing same-world subagent and sandbox contracts.
|
||||
|
||||
@@ -58,36 +52,36 @@ The Team domain does not infer branches, create worktrees, merge changes, or sil
|
||||
|
||||
The Web panel reads the Lead Session's `agentTeam` wire projection because the existing projection stream keeps browser state current without a separate Team read API. Both Lead and teammate conversations select values from the shared cross-Session store. The panel owns no projection reads: Session loading, cached list values, and control baselines supply the data.
|
||||
|
||||
The wire view carries durable roster phases, non-deleted task views with shared readiness and overlap derivation, and `failure` beside the last valid state. Live activity comes from Session status. Model selection has its own durable projection; the panel displays the shared store's available values without opening member conversations. A missing cached value remains absent until Session loading or a live update supplies it.
|
||||
The task panel uses non-deleted task views with readiness and overlap diagnostics, plus `failure` beside the last valid state. Historical roster fields remain decodable but have no current UI or assignment authority. Ordinary subagent navigation owns member discovery; task owner labels reuse Session and catalog metadata.
|
||||
|
||||
Publication requires `apply` to return a new state object per applied Team event and replace only the touched collection. Mailbox-only events retain the view reference and publish nothing. The existing carrier sends each changed complete view to every connected browser; the package README records that cost.
|
||||
Publication requires `apply` to return a new state per applied event and replace only the touched collection. Mailbox-only events retain the client view and publish nothing. The existing carrier sends each changed complete view to connected browsers; the package README records that cost.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Extend direct-child subagent tools with peer ids.** Rejected because parent/child authority and Team peer membership are different domains. Adding peer access to the continuation seam would weaken its exact-parent authorization and still leave roster and tasks without a persistence owner.
|
||||
**Keep a separate Team communication authority.** The later [tree-wide communication decision](../architecture/2026-10-16-tree-wide-agent-communication.md) places routing and authorization in the subagent runtime; a task board cannot grant another route.
|
||||
|
||||
**Store mail in each target Session before Lead-log enqueue.** Rejected because target materialization and acceptance can fail after the Team has committed the send. The always-live Lead Session is the transaction home; target recording is the acknowledgement and de-duplication boundary.
|
||||
**Keep a durable Team mailbox.** Target admission and task commit are independent operations. The historical Team mailbox remains readable, while current messaging uses the target inbox. Task commits neither deliver messages nor end waits.
|
||||
|
||||
**Treat task ownership or write scopes as locks.** Rejected because external writers bypass them, crashed owners remain durable, and path-prefix overlap cannot prove semantic independence. False mutual exclusion is more dangerous than an explicit warning.
|
||||
|
||||
**Create isolated worktrees automatically.** Rejected because worktree creation, branch naming, merge policy, ignored files, build artifacts, and cleanup are deployment choices. It also changes the same-world behavior existing subagents and sandboxes expose.
|
||||
|
||||
**Enable Teams in the default catalog.** Rejected because scoped Team controls would shadow same-named legacy globals and unsolicited delegation would add latency and token cost to simple tasks. An opt-in profile bundle inserts Team and disables the legacy controls without adding Team packages to shipped dependency graphs.
|
||||
**Enable Teams in the default catalog.** Rejected because unsolicited coordination and Team tool schemas would add latency and token cost to simple tasks. An opt-in profile bundle inserts Team without adding Team packages to shipped dependency graphs.
|
||||
|
||||
**Use an in-memory board and mailbox.** Roster and task state must survive settlement, HMR, and interruption. Historical mailbox records stay readable; message ownership follows the target inbox.
|
||||
**Use an in-memory board and mailbox.** Task state must survive settlement, HMR, and interruption. Historical mailbox records stay readable; message ownership follows the target inbox.
|
||||
|
||||
**Return Team tool results as untyped JSON.** Rejected because an undeclared result type lets `execute` drift from the value the model is promised without a compiler error, and it invites indentation that costs tokens on every roster, task, and receipt. Each Team tool therefore declares its complete result schema and one shared helper renders it compactly.
|
||||
**Return Team tool results as untyped JSON.** Rejected because an undeclared result type lets `execute` drift from the value the model is promised without a compiler error, and it invites indentation that costs tokens on every task and wait result. Each Team tool therefore declares its complete result schema and one shared helper renders it compactly.
|
||||
|
||||
## Testing
|
||||
|
||||
Package tests cover Team authority, provisioning durability, task transitions, direct inbox acceptance, sender attribution, lifecycle cancellation, and historical mailbox readability. The keyless Team profile snapshot pins the policy, tools, and workflow; the CLI composition test checks persisted Team and child logs.
|
||||
Package tests cover task transitions, tree-based assignment, historical roster and mailbox decoding, and task commits that leave inbox waits pending. Ordinary subagent tests cover inbox waiting; recorded Team compositions cover identity, task policy, and tools.
|
||||
|
||||
Model-facing member identities and availability follow the [tool projection reference](../../../../packages/experimental/tool-agent-team/README.md); service residency and durable identity remain distinct.
|
||||
Identity and inbox waiting follow [subagent controls](../../../../packages/subagent/tool-subagent-control/README.md); task policy follows the [Team tool reference](../../../../packages/team/tool-agent-team/README.md).
|
||||
|
||||
## Consequences
|
||||
|
||||
The Lead Session retains whole task/member snapshots and historical mailbox records. This favors inspectable recovery over compact deltas; deleted and delivered history remains append-only until broader Session retention applies.
|
||||
The Lead Session retains whole task snapshots and historical member and mailbox records. This favors inspectable recovery over compact deltas; deleted and delivered history remains append-only until broader Session retention applies.
|
||||
|
||||
An active roster member can be non-resident, so `inactive` is not failure and sends can incur cold-resume latency. Admission failures reject new sends. Historical pending mail is never delivered. Failed provisioning permanently consumes its name and member slot.
|
||||
A teammate can be non-resident, so `inactive` is not failure and sends can incur cold-resume latency. Historical pending mail is never delivered.
|
||||
|
||||
Coordination reduces likely checkout conflicts but cannot eliminate writes outside filesystem compare-and-set tools. The final diff and tests remain the Lead's integration boundary.
|
||||
@@ -10,47 +10,41 @@ subagent seam 已提供 fresh/fork provider、持久 child Session、FIFO foll
|
||||
|
||||
同进程 Agent 还共享一个 checkout。文件系统 edit 工具可以拒绝已观察到的陈旧版本,但 Bash、formatter、generator 与外部 writer 会绕过该屏障。把 teammate name 或 task owner 当作文件锁只会掩盖而不是解决该并发边界。
|
||||
|
||||
Agent Teams 的公开约定仍处于实验阶段,因此需要显式启用的组合。默认工具目录与简单任务行为必须保持不变;而显式请求的 Team 必须能跨越 child Activation settlement 与 mailbox 投递竞争,使 Lead 在进程 teardown 前汇总结果。
|
||||
Team 协作需要显式开启;[Session 选择](../architecture/2026-10-20-session-team-mode.zh.md)负责选择粒度与默认组合。默认工具目录与简单任务行为必须保持不变;而显式请求的 Team 必须能跨越 child Activation settlement 与 mailbox 投递竞争,使 Lead 在进程 teardown 前汇总结果。
|
||||
|
||||
## Decision
|
||||
|
||||
每个普通运行时 Root 都是一个隐式 Team 的 Lead,Team id 等于该 Root 的 `SessionId`。Team 没有 creation event:Lead pseudo-row 由身份直接存在,持久状态从第一条 member、message 或 task event 开始。roster 是扁平结构,最多包含配置数量、不可变且采用小写 kebab-case 的名字。每个 teammate 都是使用预留 Session id 的 continuable 直接 child;只有 Lead 可以创建或 interrupt teammate。roster 外由 provider 管理的普通 subagent 不是 Team member;普通 fork 是新的 Root,继承的 Team 记录会因 ancestor `TeamId` 被排除。
|
||||
每个普通根及其后代组成以根 `SessionId` 标识的 Team;持久任务状态从首个任务事件开始。[subagent 队友](../architecture/2026-10-15-agent-teams-as-subagents.zh.md)负责单一 agent 身份。[仅等待收件箱](../simplification/2026-10-29-inbox-only-agent-wait.zh.md)使任务板变更独立于收件箱等待。普通 Session fork 成为一棵新树的根,并排除继承的 Team 权限。
|
||||
|
||||
实现拆分为 `@deepseek-ai/dsh-experimental-agent-team` 与 `@deepseek-ai/dsh-experimental-tool-agent-team`:前者负责 `ctx.agentTeams` 和持久语义,后者负责 scoped schema 与模型指引。每个 Team 工具都声明完整的结果 schema,并把该值渲染为紧凑 JSON,因此编译器会检查每个 `execute` 是否符合对模型的承诺,也没有结果把 token 花在缩进上。部署显式挂载两个插件,并可禁用具有相同模型可见名称的旧 continuable control。共享策略声明 Team 可用,并说明协调责任。两个包都是 `packages/experimental/` 的公开成员;[实验性包参考](../../../../packages/experimental/AGENTS.md)负责发布、依赖隔离与 promotion。
|
||||
实现拆分为 `@deepseek-ai/dsh-agent-team` 与 `@deepseek-ai/dsh-tool-agent-team`:前者负责 `ctx.agentTeams` 和持久语义,后者负责 scoped schema 与模型指引。每个 Team 工具都声明完整的结果 schema,并把该值渲染为紧凑 JSON,因此编译器会检查每个 `execute` 是否符合对模型的承诺,也没有结果把 token 花在缩进上。基础 bundle 挂载两个插件;Team 不注册与 subagent control 同名的工具。共享策略声明 Team 可用,并说明协调责任。两个包属于 [subagent 家族](../../../../packages/subagent/README.zh.md);根 Session 保存的选择控制任务工具与策略。[Session 模式升级指南](../../../../docs/upgrade-guide/v0.2.1-rc.2/session-team-mode/guide.zh.md)说明 profile 迁移。
|
||||
|
||||
Lead 必须等待所需工作后才能给出最终答案。进程 teardown 仍是最终生命周期 owner,并会 drain continuation Activation;Team task owner 是持久状态,不会因 idle、interrupt 或进程退出自动释放。
|
||||
|
||||
## Profile delegation
|
||||
|
||||
[Team profile](../../../../packages/experimental/agent-team-profile/README.zh.md) 禁用 `subagent`、`subagent_fork` 以及名称重叠的全局控件。模型直接委派使用支持 fresh 或 fork 上下文的 `spawn_teammate`,使这些子代理进入持久 roster。Workflow 仍通过 base profile 的 fresh `spawn` 提供方执行脚本编排;经模型工具创建的 workflow 子代理不会继承 teammate 的对话身份。Subagent 服务和提供方仍是共享基础设施。普通 Session fork 保留历史;[当前身份处理](../architecture/2026-10-13-team-identity-runtime-context.zh.md)提供 fork 自己的角色。
|
||||
[基础 bundle](../../../../packages/bundle/base/README.zh.md) 插入 Team 行,并保留 base 的委派与控制工具;teammate 按[subagent](../architecture/2026-10-15-agent-teams-as-subagents.zh.md)所述使用这些工具启动。普通 Session fork 保留历史;[当前身份处理](../architecture/2026-10-13-team-identity-runtime-context.zh.md)提供 fork 自己的角色。
|
||||
|
||||
## Team identity
|
||||
|
||||
共享 system 策略和全部工具 schema 在成员间保持一致;执行时检查角色权限。Team 工具根据调用者确定 Team,并接受成员名字,因此模型不需要 Team id。[必需 runtime-context 身份](../architecture/2026-10-13-team-identity-runtime-context.zh.md)负责身份注入、保留与 fork 处理。
|
||||
成员共享统一系统策略和工具模式;执行时强制角色权限。Team 工具解析调用方所属 Team,并使用 Session id 作为任务负责人。[必需运行时身份](../architecture/2026-10-13-team-identity-runtime-context.zh.md)负责身份注入、保留和分叉处理。
|
||||
|
||||
## Provisioning and recovery
|
||||
|
||||
创建操作先在 Lead Session 中追加并 flush `team/member` provisioning 快照,再通过选定 fresh 或 fork provider 启动预留的 continuable child。初始 inbox 获准前的失败会追加 failed 快照;成功会先 flush child 中已接受的 inbox 条目,再追加 active。恢复会在初始消息仍处于 pending 或已进入用户消息历史时识别它。名字由第一条 provisioning 记录永久保留,包括失败后也不能复用。dispose 会关闭准入,中止并等待已获准的创建与发送操作,再停止 roster 记录的所有 live child;failed child 在 Activation 退出前仍由 cleanup 拥有,cleanup 拒绝会让 dispose 失败。
|
||||
|
||||
Root 恢复时会把未终结 provisioning 记录与独立持久 child Session 对账。直接 parent 与 continuable descriptor 匹配,并且已经记录初始用户消息,才能证明准入成功并转为 active;缺失、损坏、provider/lineage 不匹配或缺少已准入消息都会转为 failed。creator 会在同一 Lead 日志 serializer 内重读终态;如果 recovery 在创建成功时先标记 failed,creator 会 drain child 并报告 provisioning conflict,而不是遗留孤儿。这样既无需重建从未保存在 Team 日志中的初始 prompt,也能约束插件 reload 竞争。
|
||||
|
||||
fresh child 不继承对话。fork child 只捕获一次 Lead 已完成 turn 前缀,并保留为自己的持久 seed。当前 delegation turn 保持排除,与既有 fork provider 契约一致。
|
||||
队友创建和冷恢复属于子代理运行时([子代理队友](../architecture/2026-10-15-agent-teams-as-subagents.zh.md))。历史 `team/member` 记录保留阶段和描述并继续可读;Team 不写入新成员记录。
|
||||
|
||||
## Mailbox and task transactions
|
||||
|
||||
历史 Lead-log mailbox 仍是可读格式:queued 与 delivered 记录保留稳定身份与发送者,Team payload 校验仍适用于它们。Team 不再投递或确认这些记录;新发送遵循[直接 inbox 决策](../simplification/2026-09-26-team-direct-inbox.zh.md)。
|
||||
历史 Lead-log mailbox 仍可从原始 Session 日志读取。[仅投影任务的决策](../simplification/2026-10-29-team-task-only-projection.zh.md)取代邮箱投影、检查点保留和 Team 载荷校验。历史排队消息绝不再投递或确认。
|
||||
|
||||
事件投影和 checkpoint 准入期间,未知 mailbox 内容保持为已解码 JSON。校验只检查其 type,不重建字段,因为通用对象解析可能省略有效的自有 `__proto__` 键。本地声明的内容变体接受结构校验;不透明的插件字段不获得 Team 语义。缓存失效是必要的,因为只修正解析器无法恢复缓存状态中省略的键。[Team 文档](../../../../packages/experimental/agent-team/README.zh.md)拥有恢复行为的说明。
|
||||
|
||||
`send_message` 对 running、idle 和 cold target 使用 Steer。新发送返回 inbox 接收结果。[Team Steer 消息决策](../../archived/simplification/2026-08-30-team-send-message-steer.md)记录单一工具调度的理由。
|
||||
member 消息使用 subagent 的 `send_message` 操作,它对 running、idle 和 cold target 使用 Steer,并返回 inbox 接收结果。
|
||||
|
||||
共享 task 是带 Team-local id 与单调 revision 的完整快照。每次变更都携带 `expectedRevision`。任意 member 可以创建、读取或 claim ready 且无 owner 的任务;Owner 或 Lead 可以编辑和转换;只有 Lead 可以分配给另一个 member。数字 task id 保持在安全整数分配范围内;该范围耗尽时会失败,不会复用 id。依赖必须指向未删除任务,并形成完整 DAG。删除任务保留为 tombstone。`writeScopes` 是规范化路径前缀,只产生重叠诊断,绝不会阻止 claim 或授予写权限。
|
||||
|
||||
`wait_agent` 等待调用注册后发生的下一条 roster、mailbox、task 或实时 status 边,避免模型轮询。它不会回放更早的边,因此调用方需要在唤醒或超时后重新读取权威状态。仅限 Lead 的 interrupt 使用 inbox preservation 取消当前 turn,不改变 mailbox 或 task owner。
|
||||
`wait_agent` 属于普通 subagent 控制工具,等待当前调用者的下一步收件箱,包括已有待处理输入。任务提交不结束等待、不投递消息,也不启动空闲轮次。需要另一个成员处理任务变更时,Agent 应发送消息。祖先可中断后代,不改变任务归属。
|
||||
|
||||
## Shared checkout boundary
|
||||
|
||||
所有 member 创建时使用 Lead 的当前目录,并立即观察共享文件系统写入。后续目录变更仅影响各自 Session。策略要求 member 切分任务、记录提示性 write scope、为有序工作添加依赖,并由 Lead 检查最终 diff 和运行测试。文件系统 stale-version 拒绝后必须重新读取并 rebase 修改意图。Bash、formatter、codegen 与直接外部写入不具备等价保证。
|
||||
所有 member 创建时使用 Lead 的当前目录,并立即观察共享文件系统写入。后续目录变更仅影响各自 Session。任务策略要求 member 认领就绪任务、使用提示性 write scope 和依赖,并在完成工作后完成任务。文件系统 stale-version 拒绝后必须重新读取并 rebase 修改意图。Bash、formatter、codegen 与直接外部写入不具备等价保证。
|
||||
|
||||
Team 领域不会推断 branch、创建 worktree、merge 变更或静默改变 cwd。deployment 或调用方可以另外使用[显式工作树创建](2026-09-13-explicit-worktree-creation.zh.md)。这样保留既有 same-world subagent 与 sandbox 契约。
|
||||
|
||||
@@ -58,36 +52,36 @@ Team 领域不会推断 branch、创建 worktree、merge 变更或静默改变 c
|
||||
|
||||
Web panel 读取 Lead Session 的 `agentTeam` 传输投影,因为现有投影流能持续更新浏览器状态,无需独立的 Team 读取 API。Lead 与 teammate 会话均从共享的跨 Session store 选择值。面板不负责投影读取:Session 加载、列表缓存值与控制基线提供数据。
|
||||
|
||||
传输视图携带持久 roster phase、使用共享就绪与重叠派生逻辑的未删除任务视图,以及最后有效状态旁的 `failure`。实时活动来自 Session 状态。模型选择有自己的持久投影;面板展示共享 store 中可用的值,不打开成员会话。缺少的缓存值保持缺失,直到 Session 加载或实时更新提供它。
|
||||
任务面板使用包含就绪和重叠诊断的未删除任务视图,以及最后有效状态旁的 `failure`。历史名册字段仍可解码,但不拥有当前 UI 或分配权限。普通 subagent 导航负责成员发现;任务所有者标签复用 Session 和目录元数据。
|
||||
|
||||
发布要求 `apply` 对每个已应用的 Team 事件返回新的 state 对象,并只替换被触及的集合。仅邮箱的事件保留视图引用,不发布 frame。现有传输层将每个变化后的完整视图发送给所有已连接浏览器;包 README 记录了这项开销。
|
||||
发布要求 `apply` 为每个应用的事件返回新状态,且只替换触及的集合。仅邮箱事件保留客户端视图,不发布更新。现有载体向已连接浏览器发送变更后的完整视图;包 README 记录该成本。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**用 peer id 扩展 direct-child subagent tool。** 拒绝,因为 parent/child 权限与 Team peer membership 是不同领域。向 continuation seam 增加 peer access 会削弱 exact-parent authorization,仍无法为 roster 与 task 提供持久 owner。
|
||||
**保留独立 Team 通信权限。** 后续的[整树通信决策](../architecture/2026-10-16-tree-wide-agent-communication.zh.md)将路由和授权置于 subagent 运行时;任务板不能授予另一条路由。
|
||||
|
||||
**在 Lead 日志入队前把 mail 存入每个 target Session。** 拒绝,因为 Team 提交发送之后,target materialization 与准入仍可能失败。始终 live 的 Lead Session 是事务 owner;target recording 是 acknowledgement 与去重边界。
|
||||
**保留持久化 Team 邮箱。** 目标接纳和任务提交是独立操作。历史 Team 邮箱保持可读,当前通信使用目标收件箱。任务提交既不投递消息,也不结束等待。
|
||||
|
||||
**把 task ownership 或 write scope 当作锁。** 拒绝,因为外部 writer 会绕过它们,崩溃 owner 会持久保留,而路径前缀重叠不能证明语义独立。虚假的互斥保证比明确 warning 更危险。
|
||||
|
||||
**自动创建隔离 worktree。** 拒绝,因为 worktree 创建、branch 命名、merge 策略、ignored file、构建产物与 cleanup 都是 deployment 选择;它也会改变既有 subagent 与 sandbox 暴露的 same-world 行为。
|
||||
|
||||
**在默认工具目录中启用 Team。** 拒绝,因为 scoped Team control 会覆盖同名旧全局工具,主动 delegation 也会给简单任务增加延迟和 token 成本。opt-in profile bundle 会插入 Team 并禁用旧 control,同时不向随附依赖图添加 Team 包。
|
||||
**在默认工具目录中启用 Team。** 拒绝,因为主动协调与 Team 工具 schema 会给简单任务增加延迟和 token 成本。opt-in profile bundle 会插入 Team,同时不向随附依赖图添加 Team 包。
|
||||
|
||||
**使用内存 board 与 mailbox。** roster 与任务状态必须经受 settlement、HMR 与中断。历史 mailbox 记录保持可读;消息归属目标 inbox。
|
||||
**使用内存 board 与 mailbox。** 任务状态必须经受 settlement、HMR 与中断。历史 mailbox 记录保持可读;消息归属目标 inbox。
|
||||
|
||||
**让 Team 工具返回未类型化 JSON。** 拒绝,因为未声明的结果类型会让 `execute` 在没有编译错误的情况下偏离对模型的承诺,也会引入在每份 roster、task 与回执上都消耗 token 的缩进。因此每个 Team 工具都声明完整的结果 schema,并由一个共享 helper 紧凑渲染。
|
||||
**让 Team 工具返回未类型化 JSON。** 拒绝,因为未声明的结果类型会让 `execute` 在没有编译错误的情况下偏离对模型的承诺,也会引入在每份 task 与 wait 结果上都消耗 token 的缩进。因此每个 Team 工具都声明完整的结果 schema,并由一个共享 helper 紧凑渲染。
|
||||
|
||||
## Testing
|
||||
|
||||
包测试覆盖 Team 权限、provisioning 持久性、任务转换、直接 inbox 接收、发送者归属、生命周期取消和历史 mailbox 可读性。无密钥 Team profile 快照固定 policy、工具与工作流;CLI 组合测试检查持久 Team 和 child 日志。
|
||||
包测试覆盖任务转换、基于树的分配、历史名册和邮箱解码,以及任务提交后收件箱等待保持未完成。普通 subagent 测试覆盖收件箱等待;录制 Team 组合覆盖身份、任务策略和工具。
|
||||
|
||||
模型可见的成员身份与可用状态遵循[工具投影说明](../../../../packages/experimental/tool-agent-team/README.zh.md);服务驻留状态和持久身份仍保持区分。
|
||||
身份和收件箱等待遵循 [subagent 控制工具](../../../../packages/subagent/tool-subagent-control/README.zh.md);任务策略遵循 [Team 工具参考](../../../../packages/team/tool-agent-team/README.zh.md)。
|
||||
|
||||
## Consequences
|
||||
|
||||
Lead Session 保留完整 task/member 快照和历史 mailbox 记录。这优先支持可检查的恢复,而不是紧凑增量;deleted 与 delivered 历史在更广泛的 Session 保留策略生效前仍只追加。
|
||||
Lead Session 保留完整 task 快照以及历史 member 与 mailbox 记录。这优先支持可检查的恢复,而不是紧凑增量;deleted 与 delivered 历史在更广泛的 Session 保留策略生效前仍只追加。
|
||||
|
||||
active roster 成员可能不驻留,因此 `inactive` 不表示失败,发送可能产生冷恢复延迟。准入失败会拒绝新发送。历史 pending 邮件永远不会被投递。provisioning 失败永久占用其名称和成员槽位。
|
||||
teammate 可能不驻留,因此 `inactive` 不表示失败,发送可能产生冷恢复延迟。历史 pending 邮件永远不会被投递。
|
||||
|
||||
协调可以降低 checkout 冲突概率,但无法消除文件系统 CAS 工具之外的写入。最终 diff 与测试仍是 Lead 的集成边界。
|
||||
+2
-2
@@ -9,8 +9,8 @@
|
||||
en: 7926983089aebab2
|
||||
zh: 366e32db00674a83
|
||||
/agent-note-archiving-a-session-with-running-work/decision:
|
||||
en: 23adfbe72d671d6c
|
||||
zh: 6fec976acfa54720
|
||||
en: 703d8b6f837cc894
|
||||
zh: aabe24d201ca0d81
|
||||
/agent-note-archiving-a-session-with-running-work/alternatives-considered:
|
||||
en: 708268e1fe4b4b75
|
||||
zh: 64a8f6d6f333e090
|
||||
|
||||
@@ -14,8 +14,8 @@ Archive never hides running work. The registry owns the rule and declares one ca
|
||||
|
||||
- **Refuse by default.** `archiveSession(sessionId)` rejects a non-empty activity answer with `WorkspaceActiveSessionError`; the API maps it to `workspace/session-active`, whose details carry the families with item ids and labels. A caller that did not ask to stop work never hides it, whichever client or SDK it is.
|
||||
- **Stop on request, after the write.** `archiveSession(sessionId, { stopActivity: true })` skips the activity check, writes the archive, and then dispatches the stop event: the durable archive set is what the pre-step gate reads, so every wake a stop induces — a cancelled child's settlement, a queued follow-up — is already blocked. The stops are issued and never awaited to settlement, so the Remote returns once every provider's request is out and the user's row hides at once; a stop that fails is a logged warning, not a reason to undo the archive.
|
||||
- **The stop is the user's stop.** The Agent registry cancels the turn with `agent.cancel({ kind: 'user' })` — the stop button's path, so the aborted tool call settles as a `tool/result` and the turn ends as `aborted`, but without the button's `keepInbox`, so queued input is discarded with a logged inbox splice. The job registry seam kills the owned jobs, installed by its constructor for every implementation through the abstract `list` and `kill`. The Subagent runtime cancels running subagent descendants (durable lineage, subagent origin, any depth, never a fork) as their parent would. Each request is contained on its own, so one child or job that throws does not keep the others running. The Schedule plugin deletes every active Host task stored for the Session ([archive admission reads the stored tasks](2026-09-22-archive-admission-reads-stored-tasks.md)) inside one slot of the same serialized queue the tools use. It reads the stored rows rather than a runtime fold of the Session log, because a Host task outlives its Session's Agent and is the only place a due reminder can be seen. Each provider owns exactly the vocabulary it already has: the Agent registry knows turns, the job seam knows jobs, the Subagent runtime knows children, the Schedule plugin knows reminders; the API Session Controller keeps only the archived-lineage step gate. No new event type and no write inside another owner's turn, so a restored Session continues from a regularly ended log and unarchive brings nothing back that was stopped.
|
||||
- **Archived means no model step.** The API Session Controller's `agent/pre-step` listener rejects a step proposed for an archived Session or for a subagent descendant of one, which the loop ends as `blocked` without a request. This closes the window in which a late waking delivery — a subagent settlement, a queued follow-up, a turn started between the activity answer and the write — would run a hidden turn, and keeps a child whose cancel was ignored from working for a parent nobody can see; unarchiving lifts the gate for the lineage.
|
||||
- **The stop is the user's stop.** The Agent registry cancels the turn with `agent.cancel({ kind: 'user' })` — the stop button's path, so the aborted tool call settles as a `tool/result` and the turn ends as `aborted`, but without the button's `keepInbox`, so queued input is discarded with a logged inbox splice. The job registry seam kills the owned jobs, installed by its constructor for every implementation through the abstract `list` and `kill`. The Subagent runtime reports and cancels running descendants through each Activation’s verified ancestry, with live header ancestry for unmanaged Agents (any depth, never a fork). Releasing a creator does not erase its descendants’ membership. Each request is contained on its own, so one child or job that throws does not keep the others running. The Schedule plugin deletes every active Host task stored for the Session ([archive admission reads the stored tasks](2026-09-22-archive-admission-reads-stored-tasks.md)) inside one slot of the same serialized queue the tools use. It reads the stored rows rather than a runtime fold of the Session log, because a Host task outlives its Session's Agent and is the only place a due reminder can be seen. Each provider owns exactly the vocabulary it already has: the Agent registry knows turns, the job seam knows jobs, the Subagent runtime knows children, the Schedule plugin knows reminders; the API Session Controller keeps only the archived-lineage step gate. No new event type and no write inside another owner's turn, so a restored Session continues from a regularly ended log and unarchive brings nothing back that was stopped.
|
||||
- **Archived means no model step.** The API Session Controller checks archived Sessions and available header ancestry in `agent/pre-step`. The Subagent runtime checks each managed Agent’s retained creation ancestors in the same event; intermediate creators may be offline. Both reject the step, which the loop ends as `blocked` without a request. This closes the window in which a late waking delivery — a subagent settlement, a queued follow-up, a turn started between the activity answer and the write — would run a hidden turn, and keeps a child whose cancel was ignored from working for a parent nobody can see; unarchiving lifts the gate for the lineage.
|
||||
- **The refusal is the confirmation.** The sidebar sends the plain archive first; a quiet Session archives without a dialog as before. The `workspace/session-active` refusal opens the stop-and-archive dialog, whose list comes from the Host's answer rather than a client-side mirror of running state, so the dialog names exactly what the Host will stop. Confirming resends with `stopActivity`; the notice offers undo, which restores the Session without resuming the stopped work.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
+2
-2
@@ -14,8 +14,8 @@
|
||||
|
||||
- **默认拒绝。** `archiveSession(sessionId)` 对非空的活动答案以 `WorkspaceActiveSessionError` 拒绝;API 把它映射为 `workspace/session-active`,details 携带各族及各项的 id 与名称。没有要求停止工作的调用方绝不会把工作藏起来,无论它是哪个客户端或 SDK。
|
||||
- **按请求停止,先写后停。** `archiveSession(sessionId, { stopActivity: true })` 跳过活动检查,先写入归档,再派发停止事件:pre-step 门禁读取的正是持久化的归档集合,因此停止所引发的每一次唤醒——被取消子代理的结算、排队的 follow-up——都已被拦下。停止请求只发出、从不等待收敛,因此 Remote 在每个提供方的请求发出后即返回,用户的行随即隐藏;停止失败只是一条 warn 日志,不是撤销归档的理由。
|
||||
- **停止即用户的停止。** Agent 注册表用 `agent.cancel({ kind: 'user' })` 取消回合——与停止按钮同一条路径,因此被打断的工具调用以 `tool/result` 收口、回合以 `aborted` 结束,但不带按钮的 `keepInbox`,因此排队输入被丢弃并记录一次收件箱拼接。任务注册表接缝 kill 所属任务,由其构造函数只通过抽象的 `list` 与 `kill` 为每个实现安装。Subagent runtime 以父身份取消运行中的子代理子孙(持久化血缘、subagent 来源、任意深度、从不包括 fork)。每个请求各自兜住,一个抛错的子代理或任务不会让其余的继续跑。Schedule 插件在与工具相同的串行队列的一个槽位里删除该 Session 已存储的全部活动宿主任务([归档准入读取已存储的任务](2026-09-22-archive-admission-reads-stored-tasks.zh.md))。它读已存储的行,而不是对会话日志做 runtime fold,因为宿主任务比其 Session 的 Agent 活得更久,是到期提醒唯一能被看见的地方。每个提供方只拥有它本来就有的词汇:Agent 注册表认识回合,任务接缝认识任务,Subagent runtime 认识子代理,Schedule 插件认识提醒;API Session Controller 只保留已归档血缘的步骤门禁。没有新事件类型,也不在别的所有者的回合内写入,因此恢复后的 Session 从一份正常收尾的日志继续,取消归档不会带回任何被停止的东西。
|
||||
- **已归档即不跑模型步。** API Session Controller 的 `agent/pre-step` 监听器拒绝为已归档 Session 或其子代理子孙提出的步骤,loop 以 `blocked` 收口且不发请求。这封住了迟到的唤醒投递——子代理结算、排队的 follow-up、在活动作答与写入之间开始的回合——跑出隐藏回合的窗口,也让无视 cancel 的子代理无法替一个谁也看不见的父级干活;取消归档即为整条血缘解除门禁。
|
||||
- **停止即用户的停止。** Agent 注册表用 `agent.cancel({ kind: 'user' })` 取消回合——与停止按钮同一条路径,因此被打断的工具调用以 `tool/result` 收口、回合以 `aborted` 结束,但不带按钮的 `keepInbox`,因此排队输入被丢弃并记录一次收件箱拼接。任务注册表接缝 kill 所属任务,由其构造函数只通过抽象的 `list` 与 `kill` 为每个实现安装。Subagent runtime 通过各 Activation 已验证的祖先关系报告并取消运行中的后代,非托管 Agent 使用在线 header 血缘(任意深度,从不包括 fork)。释放创建者不会抹去后代的归属。每个请求各自兜住,一个抛错的子代理或任务不会让其余的继续跑。Schedule 插件在与工具相同的串行队列的一个槽位里删除该 Session 已存储的全部活动宿主任务([归档准入读取已存储的任务](2026-09-22-archive-admission-reads-stored-tasks.zh.md))。它读已存储的行,而不是对会话日志做 runtime fold,因为宿主任务比其 Session 的 Agent 活得更久,是到期提醒唯一能被看见的地方。每个提供方只拥有它本来就有的词汇:Agent 注册表认识回合,任务接缝认识任务,Subagent runtime 认识子代理,Schedule 插件认识提醒;API Session Controller 只保留已归档血缘的步骤门禁。没有新事件类型,也不在别的所有者的回合内写入,因此恢复后的 Session 从一份正常收尾的日志继续,取消归档不会带回任何被停止的东西。
|
||||
- **已归档即不跑模型步。** API Session Controller 在 `agent/pre-step` 中检查已归档 Session 和可用的 header 血缘。Subagent runtime 在同一事件中检查每个托管 Agent 保留的创建祖先;中间创建者可以离线。两者都会拒绝该步骤,loop 以 `blocked` 收口且不发请求。这封住了迟到的唤醒投递——子代理结算、排队的 follow-up、在活动作答与写入之间开始的回合——跑出隐藏回合的窗口,也让无视 cancel 的子代理无法替一个谁也看不见的父级干活;取消归档即为整条血缘解除门禁。
|
||||
- **拒绝即确认。** 侧栏先发普通归档;静止的 Session 一如既往无对话框直接归档。`workspace/session-active` 拒绝会打开"停止并归档"对话框,其清单来自宿主的答案而非客户端对运行状态的镜像,因此对话框列出的正是宿主将要停止的内容。确认后带 `stopActivity` 重发;提示提供撤销,撤销只恢复 Session,不会让被停止的工作继续。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: dc43ba84345e1405
|
||||
zh: dbc03572ab7710d2
|
||||
/agent-note-ship-optional-bundles-with-the-installation/decision:
|
||||
en: 2f20e356517715a6
|
||||
zh: 35c7ff8c4cd8a712
|
||||
en: 0f3e0daee65b6510
|
||||
zh: 2e9c8000da75fb6c
|
||||
/agent-note-ship-optional-bundles-with-the-installation/alternatives-considered:
|
||||
en: 95b48b61757f1f13
|
||||
zh: e9fb2d6ea492a4bc
|
||||
|
||||
@@ -14,7 +14,6 @@ The launcher names in `OPTIONAL_BUNDLES` (`packages/util/official-bundles/src/bu
|
||||
|
||||
Default-product isolation keeps its rules with one declared exception: an optional bundle's dependency graph is outside the default product. The static gate skips the `dependencies` edge from `@deepseek-ai/dsh` to a listed bundle and still rejects a runtime import, a shipped composition, a preset, or a default template that names it, an experimental dependency the list does not name, and a listed name that is not a runtime dependency or not a bundle. The workspace-constraints check accepts the same `dependencies` edges and no other runtime section, and the packed-install release check skips them from the installed entry package while requiring each listed bundle to be installed.
|
||||
|
||||
The [single-bundle reference](../../../../packages/experimental/agent-team-profile/README.md) supersedes the two Agent Teams selections with one `@deepseek-ai/dsh-experimental-agent-team-profile` selection. This note retains the installation-owned optional-bundle policy; the [optional-bundle decision for experimental capabilities](../architecture/2026-09-21-experimental-capabilities-as-optional-bundles.md) owns composition and display metadata; `OPTIONAL_BUNDLES` owns the current shipped list.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ Web 插件页只管理用户装进 profile 的组合包。像 Agent Teams 这样
|
||||
|
||||
默认产品隔离的规则保持不变,只声明一个例外:可选组合包的依赖图在默认产品之外。静态门禁跳过从 `@deepseek-ai/dsh` 到列表中组合包的 `dependencies` 边,仍然拒绝运行时 import、随附组合、preset 或默认模板对它的引用,拒绝列表没有点名的实验依赖,也拒绝不是运行时依赖或不是组合包的列表项。workspace 约束检查接受同样的 `dependencies` 边而不接受其他运行时依赖段;发布时的 packed-install 检查对已安装入口包跳过这些边,并要求列表中的每个组合包都已安装。
|
||||
|
||||
[单组合包参考](../../../../packages/experimental/agent-team-profile/README.zh.md)将两个 Agent Teams 选项替换为一个 `@deepseek-ai/dsh-experimental-agent-team-profile` 选项。本记录保留由安装方决定可选组合包的策略;[实验能力的可选 bundle 决策](../architecture/2026-09-21-experimental-capabilities-as-optional-bundles.zh.md)定义组合与显示元数据;当前随附名单由 `OPTIONAL_BUNDLES` 管理。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
# Bilingual-pair consistency record for 2026-09-26-team-direct-inbox.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-09-26-team-direct-inbox.md
|
||||
/agent-note-team-messages-use-the-target-inbox:
|
||||
en: ae17b789652784b5
|
||||
zh: fa462ace95c8cdaa
|
||||
/agent-note-team-messages-use-the-target-inbox/problem:
|
||||
en: 83296699f94c77d3
|
||||
zh: e681a426bf0a703a
|
||||
/agent-note-team-messages-use-the-target-inbox/decision:
|
||||
en: 8d5fed34f19c7302
|
||||
zh: 25417793d3797775
|
||||
/agent-note-team-messages-use-the-target-inbox/alternatives-considered:
|
||||
en: e847556d8e9f1071
|
||||
zh: bf1abe2f0e201eac
|
||||
/agent-note-team-messages-use-the-target-inbox/consequences:
|
||||
en: 24085acb09438dc8
|
||||
zh: 27c92e8de8cf7dbe
|
||||
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-10-29-inbox-only-agent-wait.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-10-29-inbox-only-agent-wait.md
|
||||
/agent-note-inbox-only-agent-waits:
|
||||
en: 8a451f9706af8dea
|
||||
zh: 9818b5abdee12a9c
|
||||
/agent-note-inbox-only-agent-waits/problem:
|
||||
en: 7a6e28745bda3305
|
||||
zh: e862166572cb9a9f
|
||||
/agent-note-inbox-only-agent-waits/decision:
|
||||
en: 553a1e166dd7e7e9
|
||||
zh: c05f16577fa6753c
|
||||
/agent-note-inbox-only-agent-waits/alternatives-considered:
|
||||
en: 64a44ad3884a90c3
|
||||
zh: 1fffe0c6fd78acbe
|
||||
/agent-note-inbox-only-agent-waits/consequences:
|
||||
en: 45b6d456c3ca226c
|
||||
zh: e9ebacd4b6bcaeca
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: Inbox-only Agent waits
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-10-29-inbox-only-agent-wait.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A task-board commit changes shared state but does not deliver input to a member. Ending every member's wait on each edit couples ordinary subagent controls to Team coordination and makes callers resume without a message explaining what needs attention.
|
||||
|
||||
## Decision
|
||||
|
||||
`wait_agent` observes only its exact caller's next-step inbox through the existing insertion event. It subscribes before checking pending input, consumes nothing, and removes listeners and timers on input, timeout, cancellation, caller disposal, or tool unload. Queued next-turn input, other Agents' activity, and task-board changes do not end the wait.
|
||||
|
||||
Task commands append and flush their revisions without notifying waits or writing inbox messages. A caller sends an explicit message when a member needs to act on an update. No Agent method, shared waiting service, or task-specific waiting event is introduced.
|
||||
|
||||
This supersedes only the wait-signal decision in [Team task boards](../architecture/2026-10-19-team-task-board-inbox.md). Its tree authority, durable task storage, historical non-authority, and separation from ordinary controls remain applicable. No Session event generation changes.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Notify all current waits after every task commit.** This avoids duplicate inbox state but still makes unrelated task edits interrupt every waiting member. Moving the notification behind a service method retains that behavior and coupling.
|
||||
|
||||
**Persist each task change as a message.** The task journal already preserves revisions; per-member notices add unread state and delivery recovery. Explicit messages communicate which updates require action.
|
||||
|
||||
**Add waiting to Agent or its inbox.** Existing input events and tool-owned cancellation already implement the required wait without extending the core API.
|
||||
|
||||
## Consequences
|
||||
|
||||
Task updates alone do not resume a waiting or idle member. Callers must communicate when work should continue. The tool returns no task contents; members query the board when needed. Task changes before or during a wait have the same effect: none.
|
||||
|
||||
Unit tests cover pending input, caller isolation, concurrent waits, cancellation, disposal, and task commits that leave waits pending. The recorded task-board scenario checks past and concurrent commits against timed-out waits, while completion notification remains ordinary inbox input.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: 仅等待收件箱的 Agent 等待
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-10-29-inbox-only-agent-wait.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
任务板提交改变共享状态,但不向成员投递输入。每次编辑都结束所有成员的等待,会使普通 subagent 控制依赖 Team 协调,并让调用者在没有消息说明需要关注什么时恢复执行。
|
||||
|
||||
## 决策
|
||||
|
||||
`wait_agent` 仅通过现有插入事件观察当前调用者自己的下一步收件箱。它先订阅再检查待处理输入,不消费输入,并在输入到达、超时、取消、调用者释放或工具卸载时移除监听器和定时器。排队的下一轮输入、其他 Agent 的活动和任务板变化不会结束等待。
|
||||
|
||||
任务命令追加并刷盘修订,不通知等待者,也不写入收件箱消息。需要成员处理更新时,调用者发送明确消息。不新增 Agent 方法、共享等待服务或任务专用等待事件。
|
||||
|
||||
这仅取代[Team 任务板](../architecture/2026-10-19-team-task-board-inbox.zh.md)中的等待信号决策。树权限、持久任务存储、历史记录不授予权限及与普通控制工具分离的决策仍然适用。不改变 Session 事件代次。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**每次任务提交后通知所有当前等待。** 这避免了重复收件箱状态,但仍让无关任务编辑打断每个等待成员。将通知移到服务方法后仍保留该行为和耦合。
|
||||
|
||||
**将每次任务变化持久化为消息。** 任务日志已经保留修订;逐成员通知增加未读状态和投递恢复。明确消息可以表达哪些更新需要行动。
|
||||
|
||||
**在 Agent 或收件箱中增加等待。** 现有输入事件和工具拥有的取消机制已能实现所需等待,无须扩展核心 API。
|
||||
|
||||
## 后果
|
||||
|
||||
仅更新任务不会恢复等待中或空闲的成员。需要继续工作时,调用者必须通信。工具不返回任务内容;成员按需查询任务板。等待前和等待期间发生的任务变化效果相同:都不影响等待。
|
||||
|
||||
单元测试覆盖已有输入、调用者隔离、并行等待、取消、释放,以及不结束等待的任务提交。任务板录制场景检查过去和并发提交下的等待超时,完成通知仍作为普通收件箱输入。
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-10-29-team-task-only-projection.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-10-29-team-task-only-projection.md
|
||||
/agent-note-team-projection-retains-only-tasks-and-mode:
|
||||
en: 030f2abb8b5137fe
|
||||
zh: e661455f374c8b1f
|
||||
/agent-note-team-projection-retains-only-tasks-and-mode/problem:
|
||||
en: 43bd1c0daacd2b86
|
||||
zh: a35efb4e84cd9288
|
||||
/agent-note-team-projection-retains-only-tasks-and-mode/decision:
|
||||
en: dd21640eb965f332
|
||||
zh: 345bc4df573cbadf
|
||||
/agent-note-team-projection-retains-only-tasks-and-mode/alternatives-considered:
|
||||
en: 96a0f421f9f92746
|
||||
zh: 314f4557f391f90d
|
||||
/agent-note-team-projection-retains-only-tasks-and-mode/consequences:
|
||||
en: 1c1b767b7905869f
|
||||
zh: 1241351bfc433b70
|
||||
@@ -0,0 +1,29 @@
|
||||
# Agent Note: Team projection retains only tasks and mode
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-10-29-team-task-only-projection.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The Team projection retained historical member, message, and delivery collections after subagent discovery and messaging replaced their runtime consumers. Its retired lifecycle and mailbox validators could still fail the current task board, while checkpoints retained message bodies with no current reader.
|
||||
|
||||
## Decision
|
||||
|
||||
Project only tasks, saved mode, and their recovery metadata. Ignore `team/member` and `team/message/*` payloads without rebuilding or validating their former state machines. This deliberately gives up historical teammate and mailbox state recovery and the associated validation failures. The user accepted that loss; task recovery remains required.
|
||||
|
||||
Checkpoint version 7 invalidates prior state, including failures caused by retired records. The registry rebuilds from the unchanged Session log. Historical event declarations, format migrations, message sources, and attachment export remain supported; no Session format bump or log conversion is needed.
|
||||
|
||||
This partially supersedes the projection and mailbox-validation decisions in [Agent Teams](../feature/2026-08-05-agent-teams.md). The [subagent identity](../architecture/2026-10-15-agent-teams-as-subagents.md) and [inbox-only wait](2026-10-29-inbox-only-agent-wait.md) decisions remain active, as do task durability and shared-checkout semantics.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Retain decode-only roster and mailbox projections.** No production consumer reads these collections. Keeping them preserves message history in checkpoints and lets obsolete validation stop current task operations.
|
||||
|
||||
**Migrate or delete historical log events.** The task board can ignore these records directly. Rewriting them adds migration work and would discard archival data that existing format readers and exports can still use.
|
||||
|
||||
## Consequences
|
||||
|
||||
Old teammate and mailbox state is unavailable through Team projection. Original log records remain readable, but neither grant authority nor trigger delivery. Reintroducing recovery requires a concrete consumer and an explicit decision about its retained data and failure scope.
|
||||
|
||||
Tests cover ignored legacy payloads, version-6 checkpoint invalidation, current checkpoint suffix replay, JSONL restart with task mutation, stable view references, and a recorded headless profile accepting a task after retired lifecycle and mailbox records. Task and mode validation still fail visibly.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Agent Note: Team 投影仅保留任务与模式
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-10-29-team-task-only-projection.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
子代理发现和通信替代运行时消费者后,Team 投影仍保留历史成员、消息与投递集合。已退役的生命周期和邮箱校验器仍可能使当前任务板失败,而检查点保留着没有当前读取方的消息正文。
|
||||
|
||||
## Decision
|
||||
|
||||
仅投影任务、保存的模式及其恢复元数据。忽略 `team/member` 和 `team/message/*` 载荷,不再重建或校验原有状态机。这明确放弃历史队友与邮箱状态恢复,以及相应的校验失败行为。用户接受这一能力损失;任务恢复仍为必要能力。
|
||||
|
||||
检查点版本 7 使旧状态失效,包括退役记录导致的失败。注册表从未改写的 Session 日志重建状态。历史事件声明、格式迁移、消息来源和附件导出仍受支持;无需提升 Session 格式版本或转换日志。
|
||||
|
||||
本决策部分取代 [Agent Teams](../feature/2026-08-05-agent-teams.zh.md) 的投影与邮箱校验决策。[子代理身份](../architecture/2026-10-15-agent-teams-as-subagents.zh.md)和[仅等待收件箱](2026-10-29-inbox-only-agent-wait.zh.md)决策仍有效,任务持久性与共享检出语义也保持有效。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**保留仅供解码的名册与邮箱投影。** 没有生产消费者读取这些集合。保留它们会在检查点中保存消息历史,并允许过时校验阻断当前任务操作。
|
||||
|
||||
**迁移或删除历史日志事件。** 任务板可以直接忽略这些记录。改写增加迁移工作,还会丢失现有格式读取器和导出功能仍可使用的归档数据。
|
||||
|
||||
## Consequences
|
||||
|
||||
Team 投影不再提供旧队友与邮箱状态。原始日志记录仍可读取,但不授予权限,也不触发投递。重新引入恢复需要具体消费者,并明确决定保留的数据和失败影响范围。
|
||||
|
||||
测试覆盖忽略旧载荷、版本 6 检查点失效、当前检查点后缀重放、JSONL 重启后修改任务、稳定视图引用,以及录制的 headless profile 在退役生命周期与邮箱记录之后接受任务。任务和模式校验仍可见地报错。
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: dsh-cua-macos-signing
|
||||
description: Proactively recommend a personal fixed signing key to macOS DSH developers using Computer Use from source, rebuilding the helper, or switching worktrees, even before they ask about repeated permissions. After they choose it, generate or reuse the key outside DSH home and configure its path. Also handles unlocking and removal; installed release users do not need this setup.
|
||||
---
|
||||
|
||||
# macOS CUA development signing
|
||||
|
||||
## Recognize developer use and recommend signing
|
||||
|
||||
When a macOS developer uses DSH Computer Use from a source checkout, proactively recommend a reusable personal signing identity. Relevant signals include launching DSH from source, rebuilding the helper, preparing another worktree, or investigating repeated permission prompts. The developer does not need to be editing CUA code, and does not need to ask about certificates first. Before a cross-worktree permission-retention test, establish that both builds select the same fixed identity.
|
||||
|
||||
Use the task context and available configuration to choose the recommendation. If an identity is already configured, recommend reusing it and unlock it when needed; do not propose another key for each worktree. If signing is unset or `ad-hoc`, briefly explain that code-hash changes can require fresh grants, recommend creating one local identity under `~/.cua-macos-keys/`, and offer to configure `cordis.patch.yml` to select it. Mention that the first macOS grant remains manual. Follow the authorization rule below before creating keys or editing configuration; a recommendation alone does not authorize setup.
|
||||
|
||||
For example: “Since you are using Computer Use from source and switching worktrees, I recommend creating one personal signing identity and reusing it across builds to reduce repeated authorization. I can generate it under `~/.cua-macos-keys/` and configure this DSH home to use it. You will still grant macOS permissions once.” Adapt the wording to the user's language and task.
|
||||
|
||||
Do not recommend developer keys merely because a user is using the installed DSH app, working on an unrelated repository, or developing without Computer Use. If they choose to keep ad-hoc signing, continue their task and do not repeat the recommendation without a relevant change in circumstances.
|
||||
|
||||
## Understand the signing choices
|
||||
|
||||
Source builds default to `sourceSigning: ad-hoc`. They need no developer certificate. An ad-hoc designated requirement includes the code hash, so changed binaries, clean rebuilds, or different worktrees can require fresh Accessibility and Screen Recording grants even when their sources are unchanged. For developers who want fewer repeated grants, configure one fixed certificate and use it across builds. The first grant is still manual; verify actual permission retention rather than claiming that matching signatures alone prove it.
|
||||
|
||||
The [macOS provider](../../../packages/experimental/cua-macos/README.md#use-this-package) owns the two configuration choices: `ad-hoc`, or `{ identity, keychain }`. The build consumes an existing certificate; it never creates or unlocks one. Developer key material stays outside DSH home. DSH home stores only the configuration selecting it.
|
||||
|
||||
## Create or reuse an identity
|
||||
|
||||
Run from a repository checkout on macOS with the selected Apple command-line tools. An explicit request to set up developer signing or reduce repeated development grants authorizes setup and the corresponding local configuration edit. For advice-only requests, explain the choices without creating an identity.
|
||||
|
||||
```sh
|
||||
python3 .agents/skills/dsh-cua-macos-signing/scripts/signing_identity.py setup
|
||||
```
|
||||
|
||||
The script creates or reuses `~/.cua-macos-keys/dev/signing.keychain-db`, stores its password and certificate fingerprint in the adjacent `identity.json` with mode `0600`, and keeps the directory private. It adds only its dedicated keychain to the user search list and enables `codesign` access without prompting for the login password. A sibling `dev.lock` serializes setup and removal across worktrees; custom directory names keep their full name before `.lock`. Supply `--directory <absolute identity directory>` when the developer chose another location outside DSH home and the checkout.
|
||||
|
||||
Setup claims only a new or empty directory and writes `.dsh-cua-macos-signing` as its ownership marker. Setup and removal refuse directories without that marker, unexpected entries, or symlinked identity files before changing keychains or identity files. Preserve unmarked identities from earlier tooling; do not add a marker automatically. They can still be selected as existing user keychains.
|
||||
|
||||
Setup prints only the certificate fingerprint and absolute keychain path as JSON. Keep the password record out of chat, logs, commits, and configuration. Other processes running as this user can read the stored password and use this development identity; this is the local developer setup's access model.
|
||||
|
||||
If an existing record is damaged or its keychain/certificate is missing, setup stops. Do not silently replace that identity: replacement changes the certificate and can require new grants. Follow the reported remove/setup commands only when identity replacement is authorized.
|
||||
|
||||
## Configure the source build
|
||||
|
||||
Resolve the DSH home used by the developer (`DSH_HOME`, otherwise `~/.dsh`). Merge the two printed values into its `cordis.patch.yml`, targeting the entry that loads `@deepseek-ai/dsh-experimental-cua-macos`. The shipped entry ID is `cua`:
|
||||
|
||||
```yaml
|
||||
- id: cua
|
||||
config:
|
||||
sourceSigning:
|
||||
identity: "<fingerprint printed by setup>"
|
||||
keychain: "/Users/<user>/.cua-macos-keys/dev/signing.keychain-db"
|
||||
```
|
||||
|
||||
Use the printed absolute path, not a literal `~`. Preserve unrelated patches and every existing field in this entry's effective config: a Cordis patch replaces the complete `config` object rather than deep-merging it. If the provider has `helperPath`, source signing is bypassed; remove that override only when the developer intends to test the source-built helper. Ensure the profile actually loads the provider before adding a patch for its ID.
|
||||
|
||||
Launch through the developer's existing `dsh` profile. The home-level patch shares the selection across profiles and worktrees that use that home. Other DSH homes can explicitly select the same certificate. Record the paths changed and the selected public fingerprint, never the password.
|
||||
|
||||
## Unlock and verify
|
||||
|
||||
After a restart or a manual keychain lock, rerun `setup`; it unlocks and reuses the existing identity. Do not ask the developer for the login-keychain password or store one. A source build does not automatically unlock the keychain and can fail or prompt if it is locked.
|
||||
|
||||
Grant Accessibility and Screen Recording to `DSH Computer Use Dev.app` in System Settings. Call `getCapabilities`, change and rebuild the Swift helper, then test another worktree with the same configuration. Report the observed permission flags for each step. Matching `codesign -d -r- <application>` requirements confirm signature continuity but do not replace the permission checks.
|
||||
|
||||
For a temporary, permission-free signing mechanism check, run:
|
||||
|
||||
```sh
|
||||
python3 .agents/skills/dsh-cua-macos-signing/scripts/signing_e2e.py
|
||||
```
|
||||
|
||||
It creates an isolated identity, tests reuse and unlocking, signs changed binaries, then removes its keychain and checks search-list restoration. Run it without concurrent keychain-list mutations.
|
||||
|
||||
## Remove an identity
|
||||
|
||||
First remove the selecting configuration or set `sourceSigning: ad-hoc` in each affected home. For an authorized removal, run:
|
||||
|
||||
```sh
|
||||
python3 .agents/skills/dsh-cua-macos-signing/scripts/signing_identity.py remove
|
||||
```
|
||||
|
||||
Use the same `--directory` for a custom location. Removal deletes only the marked identity’s files and its search-list entry, then removes the empty directory; the empty sibling lock remains so waiting processes still coordinate. Deleting DSH home alone leaves the developer identity intact. System Settings permission entries are separate; remove them only when requested.
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Real development signing keeps one designated requirement across changed builds.
|
||||
|
||||
Creates a temporary development identity, adds its keychain to the user search list for the duration
|
||||
of the check, and removes both afterwards. No system permission or password dialog is requested.
|
||||
"""
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import plistlib
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
NATIVE = Path(__file__).resolve().parents[4] / "packages/experimental/cua-macos/native"
|
||||
|
||||
|
||||
def module(name):
|
||||
spec = importlib.util.spec_from_file_location(name, NATIVE / "build_app.py" if name == "build_app" else Path(__file__).with_name("signing_identity.py"))
|
||||
loaded = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(loaded)
|
||||
return loaded
|
||||
|
||||
|
||||
def requirement(application):
|
||||
output = subprocess.run(["/usr/bin/codesign", "-d", "-r-", str(application)], check=True, capture_output=True, text=True)
|
||||
return next(line.split("=> ", 1)[1] for line in (output.stdout + output.stderr).splitlines() if "designated =>" in line)
|
||||
|
||||
|
||||
def main():
|
||||
builder, signing = module("build_app"), module("dev_signing")
|
||||
before = signing.user_keychains()
|
||||
with tempfile.TemporaryDirectory(prefix="cua-dev-signing-e2e-") as temporary:
|
||||
root = Path(temporary)
|
||||
directory = root / "keys/dev"
|
||||
try:
|
||||
command = [sys.executable, str(Path(__file__).with_name("signing_identity.py")), "setup", "--directory", str(directory)]
|
||||
configuration = json.loads(subprocess.run(command, check=True, capture_output=True, text=True).stdout)
|
||||
identity = signing.setup(directory)
|
||||
assert configuration == {"identity": identity.sha1, "keychain": str(identity.keychain.resolve())}
|
||||
assert signing.setup(directory) == identity
|
||||
sha1, keychain = identity.sha1, identity.keychain
|
||||
assert (directory / "identity.json").stat().st_mode & 0o777 == 0o600
|
||||
signing.security("lock-keychain", str(keychain))
|
||||
assert signing.setup(directory) == identity, "setup must unlock an existing developer keychain"
|
||||
requirements = []
|
||||
for status in (0, 1):
|
||||
(root / "main.c").write_text(f"int main(void) {{ return {status}; }}\n")
|
||||
subprocess.run(["/usr/bin/xcrun", "clang", "-o", str(root / "helper"), str(root / "main.c")], check=True)
|
||||
executable = builder.publish(root / "helper", root / f"checkout-{status}", sha1, NATIVE / "Info.plist", "dev", keychain)
|
||||
application = executable.parents[2]
|
||||
assert application.name == "DSH Computer Use Dev.app", application
|
||||
subprocess.run(["/usr/bin/codesign", "--verify", "--strict", str(application)], check=True)
|
||||
info = plistlib.loads((application / "Contents/Info.plist").read_bytes())
|
||||
assert info["CFBundleIdentifier"] == "com.deepseek.harness.computer-use.dev", info
|
||||
requirements.append((application, requirement(application)))
|
||||
(first, first_requirement), (second, second_requirement) = requirements
|
||||
assert first != second, "changed builds must publish separate generations"
|
||||
assert first_requirement == second_requirement, requirements
|
||||
assert "certificate leaf" in first_requirement and "cdhash" not in first_requirement, first_requirement
|
||||
subprocess.run(["/usr/bin/codesign", "--verify", f"-R={first_requirement}", str(second)], check=True)
|
||||
selected = json.loads((root / "checkout-1/current.json").read_text())["bundle"]
|
||||
assert second.parent.name == selected
|
||||
for index, (certificate, selected_keychain) in enumerate(((sha1, keychain), (signing.COMMON_NAME, keychain))):
|
||||
custom = builder.publish(root / "helper", root / f"custom-{index}", certificate, NATIVE / "Info.plist", "dev", selected_keychain)
|
||||
assert requirement(custom.parents[2]) == first_requirement
|
||||
print(f"PASS: developer setup reuses and unlocks its identity; configured signatures across changed builds share {first_requirement}")
|
||||
finally:
|
||||
try:
|
||||
signing.remove(directory)
|
||||
finally:
|
||||
# This fixture owns the temporary directory even if ownership validation fails.
|
||||
signing.clear_identity(directory)
|
||||
assert signing.user_keychains() == before, "the user keychain search list was not restored"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,258 @@
|
||||
"""Manage an explicitly requested developer identity outside DSH home.
|
||||
|
||||
macOS keeps Accessibility and Screen Recording grants for a bundle identifier together with the
|
||||
designated requirement of the signature that was granted. An ad-hoc signature's requirement is the
|
||||
code hash, so every changed build loses the grant. A certificate signature's requirement names the
|
||||
bundle identifier and the certificate, preserving the requirement across changed builds.
|
||||
|
||||
The identity lives in a dedicated keychain whose password is stored beside it with owner-only
|
||||
permissions, so developer setup can unlock it without a system dialog. Any process running as the same
|
||||
user can read that password and sign code that inherits the development grants; the grants belong
|
||||
to the development bundle identifier only, never to the release helper.
|
||||
"""
|
||||
import argparse
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
import fcntl
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import secrets
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
COMMON_NAME = "DSH Computer Use Dev (local)"
|
||||
OWNER_MARKER = ".dsh-cua-macos-signing"
|
||||
OWNER_VERSION = "1\n"
|
||||
SECURITY = "/usr/bin/security"
|
||||
OPENSSL = "/usr/bin/openssl"
|
||||
SETUP_COMMAND = "python3 .agents/skills/dsh-cua-macos-signing/scripts/signing_identity.py setup"
|
||||
REMOVE_COMMAND = "python3 .agents/skills/dsh-cua-macos-signing/scripts/signing_identity.py remove"
|
||||
CERTIFICATE_CONFIG = f"""[req]
|
||||
distinguished_name = subject
|
||||
x509_extensions = extensions
|
||||
prompt = no
|
||||
[subject]
|
||||
CN = {COMMON_NAME}
|
||||
[extensions]
|
||||
basicConstraints = critical,CA:false
|
||||
keyUsage = critical,digitalSignature
|
||||
extendedKeyUsage = critical,codeSigning
|
||||
"""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SigningIdentity:
|
||||
"""An unlocked certificate identity that codesign can use without user interaction."""
|
||||
sha1: str
|
||||
keychain: Path
|
||||
|
||||
|
||||
def private_command(command, **kwargs):
|
||||
"""Run signing tools without exposing secret arguments or their echoed output in diagnostics."""
|
||||
operation = f"{Path(command[0]).name} {command[1]}"
|
||||
try:
|
||||
return subprocess.run(command, check=True, capture_output=True, text=True, timeout=60, **kwargs).stdout
|
||||
except subprocess.CalledProcessError as error:
|
||||
raise RuntimeError(f"{operation} failed (exit {error.returncode})") from None
|
||||
except subprocess.TimeoutExpired:
|
||||
raise RuntimeError(f"{operation} timed out after 60 seconds") from None
|
||||
except OSError as error:
|
||||
raise RuntimeError(f"{operation} could not run (errno {error.errno})") from None
|
||||
|
||||
|
||||
def security(*args):
|
||||
return private_command([SECURITY, *args])
|
||||
|
||||
|
||||
@contextmanager
|
||||
def identity_lock(directory):
|
||||
"""Serialize setup and removal across worktrees; the lock inode survives identity removal."""
|
||||
directory.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
with os.fdopen(os.open(directory.parent / f"{directory.name}.lock", os.O_WRONLY | os.O_CREAT, 0o600), "w") as lock:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX)
|
||||
yield
|
||||
|
||||
|
||||
def user_keychains():
|
||||
"""Read the user keychain search list that codesign consults for identities."""
|
||||
return [line.strip().strip('"') for line in security("list-keychains", "-d", "user").splitlines() if line.strip()]
|
||||
|
||||
|
||||
def listed(keychain, keychains):
|
||||
"""Compare canonical paths; security reports /private/var for /var and resolves other links."""
|
||||
return any(os.path.realpath(path) == os.path.realpath(keychain) for path in keychains)
|
||||
|
||||
|
||||
def record_path(directory):
|
||||
return directory / "identity.json"
|
||||
|
||||
|
||||
def keychain_path(directory):
|
||||
return directory / "signing.keychain-db"
|
||||
|
||||
|
||||
def default_directory():
|
||||
"""Keep developer credentials independent of DSH profiles, homes, and worktrees."""
|
||||
return Path.home() / ".cua-macos-keys" / "dev"
|
||||
|
||||
|
||||
def owned_directory(directory, create=False):
|
||||
"""Refuse unowned or mixed-content directories before changing files or keychains."""
|
||||
marker = directory / OWNER_MARKER
|
||||
if create:
|
||||
directory.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
if not any(directory.iterdir()):
|
||||
directory.chmod(0o700)
|
||||
with os.fdopen(os.open(marker, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600), "w") as file:
|
||||
file.write(OWNER_VERSION)
|
||||
if not directory.exists():
|
||||
return
|
||||
if marker.is_symlink() or not marker.is_file() or marker.read_bytes() != OWNER_VERSION.encode():
|
||||
raise RuntimeError(f"Refusing unowned signing directory: {directory}; choose a new or empty dedicated directory")
|
||||
allowed = {OWNER_MARKER, record_path(directory).name, record_path(directory).with_suffix(".tmp").name, keychain_path(directory).name}
|
||||
for entry in directory.iterdir():
|
||||
# security creates empty .flXXXXXXXX locks and removes them with delete-keychain.
|
||||
system_lock = re.fullmatch(r"\.fl[0-9A-Fa-f]{8}", entry.name) and entry.is_file() and entry.stat().st_size == 0
|
||||
if (entry.name not in allowed and not system_lock) or entry.is_symlink() or not entry.is_file():
|
||||
raise RuntimeError(f"Refusing signing directory with unrelated entries: {directory}; preserve those entries before retrying")
|
||||
|
||||
|
||||
def load(directory):
|
||||
"""Unlock the configured identity before signing.
|
||||
|
||||
Returns None when setup has not run. Raises when setup ran but its keychain is missing or no
|
||||
longer on the search list, because signing would otherwise fail or prompt for a password.
|
||||
"""
|
||||
directory_argument = shlex.quote(str(directory))
|
||||
setup_command = f"{SETUP_COMMAND} --directory {directory_argument}"
|
||||
remove_command = f"{REMOVE_COMMAND} --directory {directory_argument}"
|
||||
record = record_path(directory)
|
||||
if not record.exists():
|
||||
return None
|
||||
try:
|
||||
fields = json.loads(record.read_text())
|
||||
valid = isinstance(fields, dict) and isinstance(fields.get("password"), str) and isinstance(fields.get("sha1"), str) and bool(fields["password"]) and bool(fields["sha1"])
|
||||
except (ValueError, OSError):
|
||||
valid = False
|
||||
if not valid:
|
||||
raise RuntimeError(f"Computer Use development signing record is invalid; run {remove_command}, then {setup_command}")
|
||||
keychain = keychain_path(directory)
|
||||
if not keychain.is_file():
|
||||
raise RuntimeError(f"Computer Use development signing keychain is missing: {keychain}; run {remove_command}, then {setup_command}")
|
||||
if not listed(keychain, user_keychains()):
|
||||
raise RuntimeError(f"Computer Use development signing keychain is not on the user keychain search list; run {setup_command}")
|
||||
# A locked keychain makes codesign show a password dialog, so unlock it explicitly every time.
|
||||
security("unlock-keychain", "-p", fields["password"], str(keychain))
|
||||
if fields["sha1"] not in security("find-identity", str(keychain)):
|
||||
raise RuntimeError(f"Computer Use development signing certificate is missing; run {remove_command}, then {setup_command}")
|
||||
return SigningIdentity(sha1=fields["sha1"], keychain=keychain)
|
||||
|
||||
|
||||
def create_certificate(directory, password):
|
||||
"""Create the key pair inside the dedicated keychain and return the certificate SHA-1."""
|
||||
keychain = keychain_path(directory)
|
||||
with tempfile.TemporaryDirectory(prefix=".certificate-", dir=directory) as temporary:
|
||||
work = Path(temporary)
|
||||
(work / "openssl.cnf").write_text(CERTIFICATE_CONFIG)
|
||||
export_password = secrets.token_hex(16)
|
||||
for command in (
|
||||
["req", "-x509", "-newkey", "rsa:2048", "-nodes", "-keyout", "key.pem", "-out", "certificate.pem",
|
||||
"-days", "3650", "-config", "openssl.cnf"],
|
||||
["pkcs12", "-export", "-inkey", "key.pem", "-in", "certificate.pem", "-out", "identity.p12",
|
||||
"-passout", f"pass:{export_password}"],
|
||||
):
|
||||
private_command([OPENSSL, *command], cwd=work)
|
||||
fingerprint = private_command([OPENSSL, "x509", "-in", "certificate.pem", "-noout", "-fingerprint", "-sha1"], cwd=work)
|
||||
security("create-keychain", "-p", password, str(keychain))
|
||||
security("unlock-keychain", "-p", password, str(keychain))
|
||||
# No arguments disables the inactivity timeout and lock on sleep.
|
||||
security("set-keychain-settings", str(keychain))
|
||||
security("import", str(work / "identity.p12"), "-k", str(keychain), "-P", export_password, "-T", "/usr/bin/codesign")
|
||||
# Without a partition list, codesign asks the user to allow access to the private key.
|
||||
security("set-key-partition-list", "-S", "apple-tool:,apple:", "-s", "-k", password, str(keychain))
|
||||
match = re.search(r"=([0-9A-F:]+)", fingerprint)
|
||||
if match is None:
|
||||
raise RuntimeError("openssl returned no certificate fingerprint")
|
||||
sha1 = match.group(1).replace(":", "")
|
||||
if sha1 not in security("find-identity", str(keychain)):
|
||||
raise RuntimeError("the imported development signing identity is not usable")
|
||||
return sha1
|
||||
|
||||
|
||||
def setup(directory):
|
||||
"""Create an identity only without a record; preserve recorded identities when their keychain is missing."""
|
||||
with identity_lock(directory):
|
||||
return setup_locked(directory)
|
||||
|
||||
|
||||
def setup_locked(directory):
|
||||
owned_directory(directory, create=True)
|
||||
keychain = keychain_path(directory)
|
||||
if record_path(directory).exists():
|
||||
if keychain.is_file():
|
||||
keychains = user_keychains()
|
||||
if not listed(keychain, keychains):
|
||||
security("list-keychains", "-d", "user", "-s", *keychains, str(keychain))
|
||||
return load(directory)
|
||||
clear_identity(directory)
|
||||
password = secrets.token_hex(32)
|
||||
sha1 = create_certificate(directory, password)
|
||||
security("list-keychains", "-d", "user", "-s", *user_keychains(), str(keychain))
|
||||
record = record_path(directory)
|
||||
with os.fdopen(os.open(record.with_suffix(".tmp"), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600), "w") as file:
|
||||
json.dump({"sha1": sha1, "password": password}, file)
|
||||
file.write("\n")
|
||||
os.replace(record.with_suffix(".tmp"), record)
|
||||
return load(directory)
|
||||
|
||||
|
||||
def remove(directory):
|
||||
"""Delete the keychain, its search-list entry and the stored password."""
|
||||
with identity_lock(directory):
|
||||
remove_locked(directory)
|
||||
|
||||
|
||||
def remove_locked(directory):
|
||||
owned_directory(directory)
|
||||
clear_identity(directory)
|
||||
if directory.exists():
|
||||
(directory / OWNER_MARKER).unlink()
|
||||
directory.rmdir()
|
||||
|
||||
|
||||
def clear_identity(directory):
|
||||
"""Remove only the identity files and its exact keychain search-list entry."""
|
||||
keychain = keychain_path(directory)
|
||||
keychains = user_keychains()
|
||||
if listed(keychain, keychains):
|
||||
security("list-keychains", "-d", "user", "-s", *[path for path in keychains if not listed(keychain, [path])])
|
||||
if keychain.exists():
|
||||
security("delete-keychain", str(keychain))
|
||||
record_path(directory).unlink(missing_ok=True)
|
||||
record_path(directory).with_suffix(".tmp").unlink(missing_ok=True)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Manage the local signing identity for source-built Computer Use applications.")
|
||||
parser.add_argument("action", choices=["setup", "remove"])
|
||||
parser.add_argument("--directory", type=Path, default=default_directory(), help="identity directory; defaults to ~/.cua-macos-keys/dev")
|
||||
args = parser.parse_args()
|
||||
directory = args.directory.expanduser().resolve()
|
||||
if args.action == "setup":
|
||||
identity = setup(directory)
|
||||
print(json.dumps({"identity": identity.sha1, "keychain": str(identity.keychain)}, indent=2))
|
||||
else:
|
||||
remove(directory)
|
||||
print("Removed the development signing identity; remove its sourceSigning configuration before the next build")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except RuntimeError as error:
|
||||
print(error, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,335 @@
|
||||
"""Development signing setup keeps one unlocked identity on the user keychain search list."""
|
||||
import contextlib
|
||||
import io
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
spec = importlib.util.spec_from_file_location("dev_signing", Path(__file__).with_name("signing_identity.py"))
|
||||
signing = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(signing)
|
||||
|
||||
|
||||
class FakeSecurity:
|
||||
"""Record security commands and model the user search list."""
|
||||
def __init__(self, keychains):
|
||||
self.keychains = list(keychains)
|
||||
self.commands = []
|
||||
|
||||
def __call__(self, *args):
|
||||
self.commands.append(args)
|
||||
if args[:3] == ("list-keychains", "-d", "user") and len(args) == 3:
|
||||
return "".join(f' "{path}"\n' for path in self.keychains)
|
||||
if args[:4] == ("list-keychains", "-d", "user", "-s"):
|
||||
self.keychains = list(args[4:])
|
||||
if args[0] == "delete-keychain":
|
||||
Path(args[1]).unlink()
|
||||
if args[0] == "find-identity":
|
||||
return "ABC123 NEW456"
|
||||
return ""
|
||||
|
||||
|
||||
class DevelopmentSigningTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
temporary = tempfile.TemporaryDirectory(prefix="cua-dev-signing-")
|
||||
self.addCleanup(temporary.cleanup)
|
||||
self.root = Path(temporary.name).resolve()
|
||||
self.directory = self.root / "signing"
|
||||
self.keychain = self.directory / "signing.keychain-db"
|
||||
self.login = "/Users/example/Library/Keychains/login.keychain-db"
|
||||
|
||||
def configure(self, listed=True):
|
||||
self.directory.mkdir(mode=0o700)
|
||||
(self.directory / signing.OWNER_MARKER).write_text(signing.OWNER_VERSION)
|
||||
self.keychain.write_bytes(b"keychain")
|
||||
(self.directory / "identity.json").write_text(json.dumps({"sha1": "ABC123", "password": "secret"}))
|
||||
return self.fake([self.login, str(self.keychain)] if listed else [self.login])
|
||||
|
||||
def fake(self, keychains):
|
||||
security = FakeSecurity(keychains)
|
||||
patcher = patch.object(signing, "security", security)
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
return security
|
||||
|
||||
def test_setup_and_remove_preserve_unowned_directory_contents(self):
|
||||
security = self.fake([self.login])
|
||||
self.directory.mkdir()
|
||||
unrelated = self.directory / "notes.txt"
|
||||
unrelated.write_text("keep this document")
|
||||
for operation in (signing.setup, signing.remove):
|
||||
with self.subTest(operation=operation.__name__):
|
||||
with self.assertRaisesRegex(RuntimeError, "Refusing unowned signing directory"):
|
||||
operation(self.directory)
|
||||
self.assertEqual(unrelated.read_text(), "keep this document")
|
||||
self.assertEqual(list(self.directory.iterdir()), [unrelated])
|
||||
self.assertEqual(security.commands, [])
|
||||
|
||||
def test_unrelated_files_in_an_owned_directory_block_setup_and_removal(self):
|
||||
security = self.configure()
|
||||
unrelated = self.directory / "notes.txt"
|
||||
unrelated.write_text("keep this document")
|
||||
before = {p.name: p.read_bytes() for p in self.directory.iterdir()}
|
||||
for operation in (signing.setup, signing.remove):
|
||||
with self.subTest(operation=operation.__name__):
|
||||
with self.assertRaisesRegex(RuntimeError, "unrelated entries"):
|
||||
operation(self.directory)
|
||||
self.assertEqual({p.name: p.read_bytes() for p in self.directory.iterdir()}, before)
|
||||
self.assertEqual(security.commands, [])
|
||||
|
||||
def test_system_keychain_locks_are_removed_by_security(self):
|
||||
security = self.configure()
|
||||
lock = self.directory / ".flD3051040"
|
||||
lock.touch()
|
||||
def remove_keychain(*args):
|
||||
result = security(*args)
|
||||
if args[0] == "delete-keychain":
|
||||
lock.unlink()
|
||||
return result
|
||||
with patch.object(signing, "security", side_effect=remove_keychain):
|
||||
signing.remove(self.directory)
|
||||
self.assertFalse(self.directory.exists())
|
||||
|
||||
def test_nonempty_files_with_a_system_lock_name_are_preserved(self):
|
||||
security = self.configure()
|
||||
lock = self.directory / ".flD3051040"
|
||||
lock.write_text("unrelated contents")
|
||||
for operation in (signing.setup, signing.remove):
|
||||
with self.assertRaisesRegex(RuntimeError, "unrelated entries"):
|
||||
operation(self.directory)
|
||||
self.assertEqual(lock.read_text(), "unrelated contents")
|
||||
self.assertEqual(security.commands, [])
|
||||
|
||||
def test_missing_or_invalid_marker_cannot_adopt_an_existing_identity(self):
|
||||
security = self.configure()
|
||||
marker = self.directory / signing.OWNER_MARKER
|
||||
for contents in (None, "different tool", "\ufffd"):
|
||||
if contents is None:
|
||||
marker.unlink()
|
||||
else:
|
||||
marker.write_text(contents)
|
||||
for operation in (signing.setup, signing.remove):
|
||||
with self.assertRaisesRegex(RuntimeError, "unowned signing directory"):
|
||||
operation(self.directory)
|
||||
self.assertTrue(self.keychain.is_file())
|
||||
self.assertEqual(security.commands, [])
|
||||
|
||||
def test_symlinked_identity_files_do_not_delete_another_directorys_data(self):
|
||||
security = self.configure()
|
||||
outside = self.root / "outside.keychain-db"
|
||||
outside.write_bytes(b"other identity")
|
||||
self.keychain.unlink()
|
||||
self.keychain.symlink_to(outside)
|
||||
for operation in (signing.setup, signing.remove):
|
||||
with self.assertRaisesRegex(RuntimeError, "unrelated entries"):
|
||||
operation(self.directory)
|
||||
self.assertEqual(outside.read_bytes(), b"other identity")
|
||||
self.assertEqual(security.commands, [])
|
||||
|
||||
def test_empty_directory_gets_an_ownership_marker_before_generation(self):
|
||||
self.fake([self.login])
|
||||
self.directory.mkdir()
|
||||
with patch.object(signing, "create_certificate", side_effect=RuntimeError("generation interrupted")):
|
||||
with self.assertRaisesRegex(RuntimeError, "generation interrupted"):
|
||||
signing.setup(self.directory)
|
||||
self.assertEqual((self.directory / signing.OWNER_MARKER).read_text(), signing.OWNER_VERSION)
|
||||
signing.remove(self.directory)
|
||||
self.assertFalse(self.directory.exists())
|
||||
|
||||
def test_removing_the_only_search_entry_leaves_no_unrelated_entries_to_restore(self):
|
||||
security = self.configure()
|
||||
security.keychains = [str(self.keychain)]
|
||||
signing.remove(self.directory)
|
||||
self.assertEqual(security.keychains, [])
|
||||
self.assertFalse(self.directory.exists())
|
||||
|
||||
def test_dotted_identity_directories_keep_separate_lock_files(self):
|
||||
directories = [self.root / name for name in ("dev", "dev.keys", "keys.v1", "keys.v2")]
|
||||
for directory in directories:
|
||||
with signing.identity_lock(directory):
|
||||
pass
|
||||
self.assertEqual({p.name for p in self.root.iterdir()}, {f"{p.name}.lock" for p in directories})
|
||||
|
||||
def test_default_identity_is_outside_dsh_home(self):
|
||||
home = self.root / "user"
|
||||
with patch.object(Path, "home", return_value=home):
|
||||
for value in ("", str(self.root / "custom-dsh-home")):
|
||||
with patch.dict(signing.os.environ, {"DSH_HOME": value}):
|
||||
self.assertEqual(signing.default_directory(), home / ".cua-macos-keys/dev")
|
||||
self.assertFalse(home.exists())
|
||||
|
||||
def test_setup_and_remove_target_the_explicit_identity_directory(self):
|
||||
directory = self.root / "keys/dev"
|
||||
identity = signing.SigningIdentity("ABC123", directory / "signing.keychain-db")
|
||||
for action in ("setup", "remove"):
|
||||
with patch.object(signing.sys, "argv", ["signing_identity.py", action, "--directory", str(directory)]), \
|
||||
patch.object(signing, action, return_value=identity) as operation, contextlib.redirect_stdout(io.StringIO()) as output:
|
||||
signing.main()
|
||||
operation.assert_called_once_with(directory)
|
||||
if action == "setup":
|
||||
self.assertEqual(json.loads(output.getvalue()), {"identity": "ABC123", "keychain": str(identity.keychain)})
|
||||
|
||||
def test_missing_setup_leaves_builds_unconfigured(self):
|
||||
self.fake([self.login])
|
||||
self.assertIsNone(signing.load(self.directory))
|
||||
|
||||
def test_load_unlocks_the_keychain_before_returning_the_identity(self):
|
||||
security = self.configure()
|
||||
identity = signing.load(self.directory)
|
||||
self.assertEqual((identity.sha1, identity.keychain), ("ABC123", self.keychain))
|
||||
self.assertIn(("unlock-keychain", "-p", "secret", str(self.keychain)), security.commands)
|
||||
|
||||
def test_listed_keychains_match_through_symbolic_links(self):
|
||||
(self.root / "alias").symlink_to(self.root, target_is_directory=True)
|
||||
self.configure()
|
||||
signing.security.keychains = [self.login, str(self.root / "alias/signing/signing.keychain-db")]
|
||||
self.assertEqual(signing.load(self.directory).sha1, "ABC123")
|
||||
|
||||
def test_unlisted_or_missing_keychains_fail_with_the_setup_command(self):
|
||||
security = self.configure(listed=False)
|
||||
with self.assertRaisesRegex(RuntimeError, "search list; run python3"):
|
||||
signing.load(self.directory)
|
||||
self.keychain.unlink()
|
||||
with self.assertRaisesRegex(RuntimeError, "keychain is missing"):
|
||||
signing.load(self.directory)
|
||||
self.assertFalse(any(command[0] == "unlock-keychain" for command in security.commands))
|
||||
|
||||
def test_setup_relists_an_existing_identity_without_creating_another(self):
|
||||
security = self.configure(listed=False)
|
||||
with patch.object(signing, "create_certificate") as create:
|
||||
self.assertEqual(signing.setup(self.directory).sha1, "ABC123")
|
||||
create.assert_not_called()
|
||||
self.assertEqual(security.keychains, [self.login, str(self.keychain)])
|
||||
|
||||
def test_setup_replaces_only_an_incomplete_owned_identity(self):
|
||||
security = self.configure()
|
||||
(self.directory / "identity.json").unlink()
|
||||
def create(directory, password):
|
||||
self.keychain.write_bytes(b"new keychain")
|
||||
return "NEW456"
|
||||
with patch.object(signing, "create_certificate", side_effect=create):
|
||||
identity = signing.setup(self.directory)
|
||||
self.assertEqual(identity.sha1, "NEW456")
|
||||
self.assertEqual(self.keychain.read_bytes(), b"new keychain")
|
||||
self.assertEqual(oct((self.directory / "identity.json").stat().st_mode & 0o777), "0o600")
|
||||
self.assertEqual(oct(self.directory.stat().st_mode & 0o777), "0o700")
|
||||
self.assertEqual(security.keychains, [self.login, str(self.keychain)])
|
||||
|
||||
def test_setup_preserves_the_record_when_the_keychain_is_missing(self):
|
||||
security = self.configure()
|
||||
record = self.directory / "identity.json"
|
||||
before = record.read_bytes()
|
||||
self.keychain.unlink()
|
||||
with patch.object(signing, "create_certificate") as create:
|
||||
with self.assertRaisesRegex(RuntimeError, "keychain is missing: .*; run .*remove --directory .*, then .*setup --directory "):
|
||||
signing.setup(self.directory)
|
||||
create.assert_not_called()
|
||||
self.assertEqual(record.read_bytes(), before)
|
||||
self.assertFalse(self.keychain.exists())
|
||||
self.assertEqual(security.commands, [])
|
||||
|
||||
def test_setup_cli_preserves_a_recorded_identity_missing_its_keychain(self):
|
||||
directory = self.root / "keys/dev"
|
||||
directory.mkdir(parents=True)
|
||||
(directory / signing.OWNER_MARKER).write_text(signing.OWNER_VERSION)
|
||||
record = directory / "identity.json"
|
||||
before = json.dumps({"sha1": "ABC123", "password": "secret-password"}).encode()
|
||||
record.write_bytes(before)
|
||||
result = subprocess.run([sys.executable, str(Path(signing.__file__)), "setup", "--directory", str(directory)],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
self.assertEqual(result.returncode, 1)
|
||||
self.assertEqual(len(result.stderr.splitlines()), 1)
|
||||
self.assertIn("keychain is missing", result.stderr)
|
||||
self.assertIn(f"run {signing.REMOVE_COMMAND} --directory {directory}, then {signing.SETUP_COMMAND} --directory {directory}", result.stderr)
|
||||
self.assertNotIn("Traceback", result.stderr)
|
||||
self.assertNotIn("secret-password", result.stderr)
|
||||
self.assertEqual(record.read_bytes(), before)
|
||||
self.assertFalse(signing.keychain_path(directory).exists())
|
||||
|
||||
def test_remove_restores_the_other_search_list_entries_and_deletes_the_password(self):
|
||||
security = self.configure()
|
||||
signing.remove(self.directory)
|
||||
self.assertEqual(security.keychains, [self.login])
|
||||
self.assertFalse(self.directory.exists())
|
||||
signing.remove(self.directory)
|
||||
self.assertEqual(security.keychains, [self.login])
|
||||
|
||||
def test_missing_certificate_requires_explicit_removal_instead_of_changing_identity(self):
|
||||
self.configure()
|
||||
(self.directory / "identity.json").write_text(json.dumps({"sha1": "MISSING", "password": "secret"}))
|
||||
with self.assertRaisesRegex(RuntimeError, "certificate is missing; run .*remove --directory .*, then .*setup --directory "):
|
||||
signing.setup(self.directory)
|
||||
self.assertTrue(self.keychain.is_file())
|
||||
|
||||
def test_invalid_identity_record_reports_recovery_without_its_contents(self):
|
||||
self.configure()
|
||||
(self.directory / "identity.json").write_text("secret invalid json")
|
||||
with self.assertRaisesRegex(RuntimeError, "record is invalid; run .*remove") as error:
|
||||
signing.setup(self.directory)
|
||||
self.assertNotIn("secret", str(error.exception))
|
||||
|
||||
def test_private_command_failures_never_echo_arguments_or_output(self):
|
||||
for tool, action in ((signing.SECURITY, "unlock-keychain"), (signing.OPENSSL, "pkcs12")):
|
||||
command = [tool, action, "-p", "secret-password"]
|
||||
for failure in (subprocess.TimeoutExpired(command, 60, stderr="secret-password"),
|
||||
subprocess.CalledProcessError(1, command, stderr="secret-password"),
|
||||
OSError(2, "secret-password")):
|
||||
with self.subTest(tool=tool, failure=type(failure).__name__), \
|
||||
patch.object(signing.subprocess, "run", side_effect=failure):
|
||||
with self.assertRaises(RuntimeError) as error:
|
||||
signing.private_command(command)
|
||||
self.assertNotIn("secret-password", str(error.exception))
|
||||
self.assertTrue(error.exception.__suppress_context__)
|
||||
|
||||
def test_setup_and_remove_hold_the_same_cross_process_lock(self):
|
||||
self.configure()
|
||||
probe = """import fcntl, os, sys
|
||||
with open(sys.argv[1], 'w') as lock:
|
||||
try:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except BlockingIOError:
|
||||
sys.exit(0)
|
||||
sys.exit(1)
|
||||
"""
|
||||
setup_locked, remove_locked = signing.setup_locked, signing.remove_locked
|
||||
def check_lock(operation):
|
||||
def run(directory):
|
||||
result = subprocess.run([sys.executable, "-c", probe, str(directory.parent / f"{directory.name}.lock")],
|
||||
capture_output=True, timeout=10)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
return operation(directory)
|
||||
return run
|
||||
with patch.object(signing, "setup_locked", side_effect=check_lock(setup_locked)), \
|
||||
patch.object(signing, "remove_locked", side_effect=check_lock(remove_locked)):
|
||||
signing.setup(self.directory)
|
||||
signing.remove(self.directory)
|
||||
with signing.identity_lock(self.directory):
|
||||
pass
|
||||
|
||||
def test_setup_cli_reports_signing_timeout_without_traceback_or_password(self):
|
||||
script = """import runpy, sys, subprocess
|
||||
from unittest.mock import patch
|
||||
from pathlib import Path
|
||||
test_home = Path(sys.argv[2])
|
||||
Path.home = lambda: test_home
|
||||
sys.argv = [sys.argv[1], 'setup', '--directory', sys.argv[2]]
|
||||
with patch('subprocess.run', side_effect=subprocess.TimeoutExpired(['security', 'unlock-keychain', '-p', 'secret-password'], 60)):
|
||||
runpy.run_path(sys.argv[0], run_name='__main__')
|
||||
"""
|
||||
# Fail at the first security command, before setup can modify any keychain.
|
||||
result = subprocess.run([sys.executable, "-c", script, str(Path(signing.__file__)), str(self.root / "home")],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
self.assertEqual(result.returncode, 1)
|
||||
self.assertEqual(len(result.stderr.splitlines()), 1)
|
||||
self.assertIn("timed out", result.stderr)
|
||||
self.assertNotIn("secret-password", result.stderr)
|
||||
self.assertNotIn("Traceback", result.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+1
-1
@@ -4,6 +4,6 @@ Run jobs on Windows runners (`windows-*` labels) under native `pwsh`. Native Win
|
||||
|
||||
The manual [Windows packaging workflow](workflows/windows-package.yml) uses the dedicated self-hosted Windows x64 label `dsh-win-package-trial` under native `pwsh`; preserve this routing separately from the automatic CI lanes.
|
||||
|
||||
The merge checks run from three workflows named `Policy`, which run on `pull_request_target` so their definitions come from master, and call the `deepseek-harness-check` action directly: `pr-check-on-head-branch.yml` on pull-request updates gives `Policy / Master Change` and `Policy / Pre-Merge Check`, `pr-check-after-approve.yml` on pull-request updates and submitted or dismissed reviews gives `Policy / Approve Check` and `Policy / Reviewer Evaluate`, and `pr-check-on-base-branch.yml` on master pushes re-runs the head-branch checks as `Policy / Recheck`. Branch rules require those check names, so keep the job names and the workflow name. Each check reads its mode from a repository variable: `MERGE_POLICY_MASTER_DIFF`, `MERGE_POLICY_PRE_MERGE`, `MERGE_POLICY_HEAD_DIFF`, and `MERGE_POLICY_REVIEW`, each `off` (skip), `report` (run without failing the job), or `block` (fail the job); a missing value or an unknown one counts as `report`. `Policy / Pre-Merge Check` merges the commit `Policy / Master Change` reports, so it runs only after that job and stays off while `MERGE_POLICY_MASTER_DIFF` is `off`; a merge it cannot do and a build that fails both follow its own mode, so `report` warns and skips the build while `block` fails the job. `Policy / Pre-Merge Check` runs `scripts/pre-merge-check.ts` under plain Node and knows nothing else about the merged tree: that script checks the released manifests against the root version, installs, and builds the Host and Client contracts, so change it in one pull request together with the build it orchestrates.
|
||||
The merge checks run from three workflows named `Policy`, which run on `pull_request_target` so their definitions come from master, and call the `deepseek-harness-check` action directly: `pr-check-on-head-branch.yml` on pull-request updates gives `Policy / Master Change` and `Policy / Pre-Merge Check`, `pr-check-after-approve.yml` on pull-request updates and submitted or dismissed reviews gives `Policy / Approve Check`, which also publishes the review verdict as the check run `Policy / Reviewer Check`, and `pr-check-on-base-branch.yml` on master pushes re-runs the head-branch checks as `Policy / Recheck`. Branch rules require those check names, so keep the job names and the workflow name. Each job reads its mode from a repository variable: `MERGE_POLICY_MASTER_DIFF`, `MERGE_POLICY_PRE_MERGE`, and `MERGE_POLICY_HEAD_DIFF`, each `off` (skip), `report` (run without failing the job), or `block` (fail the job); a missing value or an unknown one counts as `report`. `MERGE_POLICY_HEAD_DIFF` decides whether `Policy / Approve Check` runs and fails; while it runs, `Policy / Reviewer Check` is published whatever that mode says, with `success` when every file that needs review has one and `action_required` with the reviewers to ask otherwise. `Policy / Pre-Merge Check` merges the commit `Policy / Master Change` reports, so it runs only after that job and stays off while `MERGE_POLICY_MASTER_DIFF` is `off`; a merge it cannot do and a build that fails both follow its own mode, so `report` warns and skips the build while `block` fails the job. `Policy / Pre-Merge Check` runs `scripts/pre-merge-check.ts` under plain Node and knows nothing else about the merged tree: that script checks the released manifests against the root version, installs, and builds the Host and Client contracts, so change it in one pull request together with the build it orchestrates.
|
||||
|
||||
The manual Desktop publication workflow selects a successful package run and attempt, then routes each retained build through `DESKTOP_PUBLISH_RUNNERS` to its original machine's unique label. Keep Windows run steps on native PowerShell and Mac run steps on the default POSIX shell; shared entrypoints are Node scripts. Test and production jobs use separate `desktop-test` and `desktop-production` approval Environments after a credential-free preparation job.
|
||||
@@ -14,47 +14,15 @@ concurrency:
|
||||
jobs:
|
||||
approve:
|
||||
name: Approve Check
|
||||
# Each check runs only while its repository variable is neither empty nor `off`; otherwise the job is skipped.
|
||||
# Skipped while MERGE_POLICY_HEAD_DIFF is empty or `off`; otherwise the job fails by that mode and publishes
|
||||
# `Policy / Reviewer Check` whatever that mode says.
|
||||
if: ${{ vars.MERGE_POLICY_HEAD_DIFF != '' && vars.MERGE_POLICY_HEAD_DIFF != 'off' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
steps:
|
||||
# The head checkout is only the Git repository the check fetches into; it needs no files and runs no code.
|
||||
- uses: actions/[email protected]
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 1
|
||||
filter: blob:none
|
||||
sparse-checkout: README.md
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- uses: actions/[email protected]
|
||||
with:
|
||||
node-version: '24'
|
||||
package-manager-cache: false
|
||||
|
||||
- uses: deepseek-harness/deepseek-harness-check@master
|
||||
env:
|
||||
CHECK_MODE: ${{ vars.MERGE_POLICY_HEAD_DIFF }}
|
||||
with:
|
||||
check: head-diff
|
||||
base-branch: ${{ github.event.pull_request.base.ref }}
|
||||
head-sha: ${{ github.event.pull_request.head.sha }}
|
||||
pr-number: ${{ github.event.pull_request.number }}
|
||||
pr-author: ${{ github.event.pull_request.user.login }}
|
||||
|
||||
review:
|
||||
name: Reviewer Evaluate
|
||||
if: ${{ vars.MERGE_POLICY_REVIEW != '' && vars.MERGE_POLICY_REVIEW != 'off' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
# The check posts its verdict as a check run of its own.
|
||||
# The check posts the review verdict as a check run of its own.
|
||||
checks: write
|
||||
steps:
|
||||
# The head checkout is only the Git repository the check fetches into; it needs no files and runs no code.
|
||||
@@ -84,7 +52,7 @@ jobs:
|
||||
|
||||
- uses: deepseek-harness/deepseek-harness-check@master
|
||||
env:
|
||||
CHECK_MODE: ${{ vars.MERGE_POLICY_REVIEW }}
|
||||
CHECK_MODE: ${{ vars.MERGE_POLICY_HEAD_DIFF }}
|
||||
CHECK_TOKEN: ${{ steps.token.outputs.token }}
|
||||
with:
|
||||
check: review
|
||||
|
||||
@@ -37,6 +37,7 @@ packages/ @deepseek-ai/dsh-<pkg> workspaces at packages/<group>/<pkg>/
|
||||
compaction/ context compaction
|
||||
context/ request context
|
||||
subagent/ delegated agents
|
||||
team/ shared task coordination
|
||||
jobs/ background jobs
|
||||
bundle/ profile bundles
|
||||
workflow/ workflow execution
|
||||
|
||||
@@ -140,6 +140,10 @@ flowchart LR
|
||||
cfg --> plugin_dsh_base_command_compact
|
||||
plugin_dsh_base_subagent["subagent<br/>@deepseek-ai/dsh-subagent"]
|
||||
cfg --> plugin_dsh_base_subagent
|
||||
plugin_dsh_base_agent_team["agent-team<br/>@deepseek-ai/dsh-agent-team"]
|
||||
cfg --> plugin_dsh_base_agent_team
|
||||
plugin_dsh_base_tool_agent_team["tool-agent-team<br/>@deepseek-ai/dsh-tool-agent-team"]
|
||||
cfg --> plugin_dsh_base_tool_agent_team
|
||||
plugin_dsh_base_subagent_spawn_in_process["subagent-spawn-in-process<br/>@deepseek-ai/dsh-subagent-spawn-in-process"]
|
||||
cfg --> plugin_dsh_base_subagent_spawn_in_process
|
||||
plugin_dsh_base_subagent_fork_in_process["subagent-fork-in-process<br/>@deepseek-ai/dsh-subagent-fork-in-process"]
|
||||
@@ -272,6 +276,8 @@ flowchart LR
|
||||
| `compaction-basic` | `@deepseek-ai/dsh-compaction-basic` |
|
||||
| `command-compact` | `@deepseek-ai/dsh-command-compact` |
|
||||
| `subagent` | `@deepseek-ai/dsh-subagent` |
|
||||
| `agent-team` | `@deepseek-ai/dsh-agent-team` |
|
||||
| `tool-agent-team` | `@deepseek-ai/dsh-tool-agent-team` |
|
||||
| `subagent-spawn-in-process` | `@deepseek-ai/dsh-subagent-spawn-in-process` |
|
||||
| `subagent-fork-in-process` | `@deepseek-ai/dsh-subagent-fork-in-process` |
|
||||
| `tool-subagent-control` | `@deepseek-ai/dsh-tool-subagent-control` |
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
"@deepseek-ai/dsh-compaction-basic": "workspace:*",
|
||||
"@deepseek-ai/dsh-compaction-tool-result-pruner": "workspace:*",
|
||||
"@deepseek-ai/dsh-cordis-client-runner": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-agent-team-profile": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-inspector-profile": "workspace:*",
|
||||
"@deepseek-ai/dsh-fs-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-goal": "workspace:*",
|
||||
@@ -128,9 +127,9 @@
|
||||
"@deepseek-ai/dsh-bash-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-credentials-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-deepseek-llm-api-extensions": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-agent-team": "workspace:*",
|
||||
"@deepseek-ai/dsh-agent-team": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-ptc-runtime-python": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-tool-agent-team": "workspace:*",
|
||||
"@deepseek-ai/dsh-tool-agent-team": "workspace:*",
|
||||
"@deepseek-ai/dsh-fs-observation-policy": "workspace:*",
|
||||
"@deepseek-ai/dsh-fs-sandbox": "workspace:*",
|
||||
"@deepseek-ai/dsh-host-frontend-static": "workspace:*",
|
||||
|
||||
@@ -17,7 +17,7 @@ function records(content: string): Record<string, unknown>[] {
|
||||
}
|
||||
|
||||
describe('dsh run with Agent Teams enabled', () => {
|
||||
it('runs two teammates, direct peer messages, dependent tasks, waiting, and final aggregation', async () => {
|
||||
it('runs two teammates, sibling messages, dependent tasks, waiting, and final aggregation', async () => {
|
||||
const cwd = await mkdtemp(join(tmpdir(), 'dsh-agent-team-headless-'))
|
||||
try {
|
||||
const home = join(cwd, '.dsh')
|
||||
@@ -27,15 +27,12 @@ describe('dsh run with Agent Teams enabled', () => {
|
||||
await writeFile(join(profileDir, 'package.json'), JSON.stringify({
|
||||
name: 'dsh-profile-headless',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@deepseek-ai/dsh-experimental-agent-team-profile': 'workspace:^',
|
||||
},
|
||||
dependencies: {},
|
||||
dsh: {
|
||||
profile: {
|
||||
bundles: [
|
||||
'@deepseek-ai/dsh-base',
|
||||
'@deepseek-ai/dsh-headless',
|
||||
'@deepseek-ai/dsh-experimental-agent-team-profile',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -91,8 +88,8 @@ describe('dsh run with Agent Teams enabled', () => {
|
||||
const workflowChild = parsed.find(log => log.some(record => record.type === 'user/message'
|
||||
&& JSON.stringify((record.data as { content: unknown }).content) === JSON.stringify([{ type: 'text', text: 'TEAM_WORKFLOW_CHILD' }])))
|
||||
expect(workflowChild).toBeDefined()
|
||||
expect(workflowChild!.find(record => record.type === 'subagent/descriptor')?.data)
|
||||
.toMatchObject({ mode: 'continuable', provider: 'spawn' })
|
||||
const workflowDescriptor = workflowChild!.find(record => record.type === 'subagent/descriptor')?.data as Record<string, unknown>
|
||||
expect(workflowDescriptor).toMatchObject({ mode: 'continuable', provider: 'spawn' })
|
||||
expect(workflowChild!.filter(record => record.type === 'user/message'
|
||||
&& (record.data as { source: { kind: string } }).source.kind === 'user').map(record => record.data))
|
||||
.toEqual([expect.objectContaining({ content: [{ type: 'text', text: 'TEAM_WORKFLOW_CHILD' }] })])
|
||||
@@ -102,7 +99,15 @@ describe('dsh run with Agent Teams enabled', () => {
|
||||
})
|
||||
expect(root).toBeDefined()
|
||||
const eventTypes = root!.map(record => record.type)
|
||||
expect(eventTypes.filter(type => type === 'team/member')).toHaveLength(4)
|
||||
expect(eventTypes).not.toContain('team/member')
|
||||
const catalog = root!.filter(record => record.type === 'subagent/catalog').map(record => record.data)
|
||||
expect(catalog).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ version: 0, childId: workflowChild![0]?.id }),
|
||||
expect.objectContaining({ version: 0, label: 'Own deterministic implementation.' }),
|
||||
expect.objectContaining({ version: 0, label: 'Own deterministic research.' }),
|
||||
]))
|
||||
for (const entry of catalog) expect(entry).not.toHaveProperty('name')
|
||||
expect(eventTypes).not.toContain('subagent-name/bound')
|
||||
expect(eventTypes).not.toContain('team/message/queued')
|
||||
expect(eventTypes).not.toContain('team/message/delivered')
|
||||
const peerMessages = parsed.flatMap(log => log.filter(record => record.type === 'user/message'
|
||||
|
||||
@@ -171,8 +171,8 @@ describe.skipIf(!existsSync(join(root, 'apps/cli/lib/bin.js')))('Official option
|
||||
it('keeps the default host and minimal tool catalogs while every optional bundle is off', async () => {
|
||||
const minimal = await agent('minimal')
|
||||
const standard = await agent()
|
||||
expect(names()).toEqual(['working_directory'])
|
||||
expect(names(minimal.agent)).toEqual([process.platform === 'win32' ? 'pwsh' : 'bash', 'read_image', 'working_directory'])
|
||||
expect(names()).toEqual(['team_task_create', 'team_task_get', 'team_task_list', 'team_task_update', 'working_directory'])
|
||||
expect(names(minimal.agent)).toEqual([process.platform === 'win32' ? 'pwsh' : 'bash', 'read_image', 'team_task_create', 'team_task_get', 'team_task_list', 'team_task_update', 'working_directory'])
|
||||
for (const name of ['str_replace_editor', 'session_search', 'terminal_open', 'ralph']) expect(names(standard.agent)).not.toContain(name)
|
||||
expect((await ctx.skills.list({ cwd: workspace, scope: standard.agent })).some(skill => skill.name === 'dsh-badge')).toBe(false)
|
||||
})
|
||||
@@ -246,7 +246,7 @@ describe.skipIf(!existsSync(join(root, 'apps/cli/lib/bin.js')))('Official option
|
||||
expect(toolResult(owner.agent, 'badge-on')).toContain('dsh-badge.png')
|
||||
const minimal = await agent('minimal')
|
||||
expect((await ctx.skills.list({ cwd: workspace, scope: minimal.agent })).map(skill => skill.name)).toContain('dsh-badge')
|
||||
expect(names(minimal.agent)).toEqual([process.platform === 'win32' ? 'pwsh' : 'bash', 'read_image', 'working_directory'])
|
||||
expect(names(minimal.agent)).toEqual([process.platform === 'win32' ? 'pwsh' : 'bash', 'read_image', 'team_task_create', 'team_task_get', 'team_task_list', 'team_task_update', 'working_directory'])
|
||||
await select(bundles.badge, false)
|
||||
expect((await load('badge-after')).isError).toBe(true)
|
||||
})
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
{"type":"step/start","data":{"turn":1,"step":1}}
|
||||
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Create a durable two-round goal for the ACP snapshot, inspect it, then report readiness."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\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":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:identity","text":"You are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Create a durable two-round goal","messageSeqs":[8],"source":{"kind":"fallback"}}}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{"type":"step/start","data":{"turn":1,"step":1}}
|
||||
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Create a durable goal for the wrap-up snapshot, then report readiness."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\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":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:identity","text":"You are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Create a durable goal for","messageSeqs":[8],"source":{"kind":"fallback"}}}
|
||||
|
||||
@@ -117,6 +117,7 @@ it('pins Messages Files offload and inline fallback in assembled requests', asyn
|
||||
},
|
||||
})
|
||||
expect(result.stderr).toBe('')
|
||||
if (result.sessionId === undefined) throw new Error('image-offload scenario did not create a session')
|
||||
expect(requests).toHaveLength(2)
|
||||
expect(fileRequests).toEqual([{ method: 'POST', path: filesPath, bytes: 69 }])
|
||||
const attachmentDigest = 'b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640'
|
||||
@@ -139,10 +140,13 @@ it('pins Messages Files offload and inline fallback in assembled requests', asyn
|
||||
const fileImage = { type: 'image', source: { type: 'file', file_id: 'file-api-snapshot-1' } }
|
||||
const offloadedImage = `[image omitted to fit request image limits; ${attachmentId}.${normalizedAccess}]`
|
||||
const imageHandle = `Image ${attachmentId}; request preview 1x1px.${normalizedAccess}`
|
||||
const runtimeContext = (cwd: string): string => 'Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\n'
|
||||
const runtimeContext = (cwd: string, sessionId: string): string => 'Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\n'
|
||||
+ `Current working directory: ${JSON.stringify(cwd)}.\n\n`
|
||||
+ 'Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\n'
|
||||
+ 'Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).'
|
||||
+ 'Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\n'
|
||||
+ `You are the root agent. Your agent id is ${JSON.stringify(sessionId)}.\n\n`
|
||||
+ 'To message other agents, use send_message({ agent_id: "<agent-id>", message: "..." }).\n'
|
||||
+ 'Use list_agents({}) to find your collaborator agents and their ids.'
|
||||
const messages = requests[0]?.messages as { content?: unknown }[] | undefined
|
||||
const offloaded = messages?.find(message => JSON.stringify(message.content).includes('[image omitted'))
|
||||
expect(offloaded?.content).toEqual([
|
||||
@@ -152,7 +156,7 @@ it('pins Messages Files offload and inline fallback in assembled requests', asyn
|
||||
{ type: 'text', text: imageHandle },
|
||||
fileImage,
|
||||
{ type: 'text', text: ', then use read_image on red.png and reply with DONE.' },
|
||||
{ type: 'text', text: runtimeContext(result.cwd) },
|
||||
{ type: 'text', text: runtimeContext(result.cwd, result.sessionId) },
|
||||
])
|
||||
|
||||
const followup = requests[1]?.messages as Array<{ role: string; content: Array<Record<string, unknown>> }>
|
||||
@@ -188,6 +192,7 @@ it('pins Messages Files offload and inline fallback in assembled requests', asyn
|
||||
},
|
||||
})
|
||||
expect(fallback.stderr).toBe('')
|
||||
if (fallback.sessionId === undefined) throw new Error('image-offload fallback scenario did not create a session')
|
||||
expect(fileRequests).toEqual([
|
||||
{ method: 'POST', path: filesPath, bytes: 69 },
|
||||
{ method: 'POST', path: filesPath, bytes: 69 },
|
||||
@@ -203,7 +208,7 @@ it('pins Messages Files offload and inline fallback in assembled requests', asyn
|
||||
{ type: 'text', text: `Image ${attachmentId}; request preview 1x1px.${fallbackAccess}` },
|
||||
{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: image } },
|
||||
{ type: 'text', text: ', then use read_image on red.png and reply with DONE.' },
|
||||
{ type: 'text', text: runtimeContext(fallback.cwd) },
|
||||
{ type: 'text', text: runtimeContext(fallback.cwd, fallback.sessionId) },
|
||||
])
|
||||
} finally {
|
||||
await new Promise<void>(resolve => server.close(() => { resolve() }))
|
||||
|
||||
@@ -88,7 +88,7 @@ async function exercise(ctx, config) {
|
||||
// A transitive repository package the registry served, absent from npm at this version.
|
||||
const localDependency = JSON.parse(await readFile(join(config.profile, 'node_modules', config.localDependency, 'package.json'), 'utf8')).version
|
||||
const provider = name.endsWith('-codex') ? 'codex' : 'claude-code'
|
||||
const providerEnabled = ctx.subagents.list().includes(provider)
|
||||
const providerEnabled = ctx.subagents.getProvider(provider) !== undefined
|
||||
const nativeDir = join(config.profile, 'node_modules', config.nativePackages[name])
|
||||
const binaryName = `${provider === 'codex' ? 'codex' : 'claude'}${process.platform === 'win32' ? '.exe' : ''}`
|
||||
const nativeBinary = (await readdir(nativeDir, { recursive: true })).find(path => basename(path) === binaryName)
|
||||
|
||||
@@ -21,33 +21,7 @@
|
||||
root: './.sessions'
|
||||
compression: none
|
||||
|
||||
- id: tool-subagent-control
|
||||
disabled: true
|
||||
|
||||
- id: tool-subagent-report
|
||||
disabled: true
|
||||
|
||||
- id: tool-subagent
|
||||
disabled: true
|
||||
|
||||
- id: tool-subagent-fork
|
||||
disabled: true
|
||||
|
||||
- insert:
|
||||
- id: agent-team
|
||||
name: '@deepseek-ai/dsh-experimental-agent-team'
|
||||
config:
|
||||
maxMembers: 8
|
||||
maxTasks: 256
|
||||
maxMessageBytes: 65536
|
||||
disposalTimeoutMs: 5000
|
||||
|
||||
- id: tool-agent-team
|
||||
name: '@deepseek-ai/dsh-experimental-tool-agent-team'
|
||||
config:
|
||||
freshProvider: spawn
|
||||
forkProvider: fork
|
||||
|
||||
- id: team-fixture-llm
|
||||
name: './tests/fixtures/team-llm.mjs'
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"Probe strict-schema fillers against missing-goal revision 1, then create a durable goal to finish the snapshot proof and inspect it."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"Probe strict-schema fillers against miss","messageSeqs":[8],"source":{"kind":"fallback"}}}}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
{"type":"step/start","data":{"turn":1,"step":1}}
|
||||
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Prove the product headless profile path with one real tool round trip."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\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":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:identity","text":"You are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"cli-mock","model":"cli-mock","reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true},"tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"cli-mock","model":"cli-mock"}}
|
||||
{"type":"session/title","data":{"title":"Prove the product headless profile","messageSeqs":[8],"source":{"kind":"fallback"}}}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"say pong"}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":"{{tools}}"},"reason":"initial"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":1000000}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"say pong","messageSeqs":[8],"source":{"kind":"fallback"}}}}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"say pong"}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":"{{tools}}"},"reason":"initial"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":1000000}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"say pong","messageSeqs":[8],"source":{"kind":"fallback"}}}}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"retry the transient provider failure"}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"retry the transient provider failure","messageSeqs":[8],"source":{"kind":"fallback"}}}}
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
{"type":"step/start","data":{"turn":2,"step":1}}
|
||||
{"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Continue safely from the interrupted operation."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Perform one side-effecting remote mutati","messageSeqs":[3],"source":{"kind":"fallback"}}}
|
||||
|
||||
+2
-2
@@ -16,13 +16,13 @@
|
||||
{"type":"step/start","data":{"turn":2,"step":1}}
|
||||
{"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Call list_agents once and report what it shows."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Start a background job.","messageSeqs":[3],"source":{"kind":"fallback"}}}
|
||||
{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"list-once","name":"list_agents","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"list-once","name":"list_agents","args":["{}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"list-once","name":"list_agents","arguments":"{}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":2,"step":1,"callId":"list-once","name":"list_agents","arguments":"{}"}}
|
||||
{"type":"tool/result","data":{"turn":2,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"list-once"},"toolCallId":"list-once","content":[{"type":"text","text":"{{sessionId}} [inactive] — descriptorless child"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[22],"surfaceOp":"append"}
|
||||
{"type":"tool/result","data":{"turn":2,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"list-once"},"toolCallId":"list-once","content":[{"type":"text","text":"{{sessionId}} [running] — root agent\n{{sessionId}} [inactive] parent={{sessionId}} depth=1 — descriptorless child"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[22],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":2,"step":1}}
|
||||
{"type":"step/start","data":{"turn":2,"step":2}}
|
||||
{"type":"assistant/message","data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"The catalogued subagent is ready. PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["The catalogued subagent is ready. PARENT_DONE"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"The catalogued subagent is ready. PARENT_DONE"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
|
||||
|
||||
+3
-3
@@ -2,13 +2,13 @@
|
||||
{"type":"subagent/descriptor","data":{"version":3,"mode":"continuable","provider":"spawn","label":"Delegated write probe","agentProvider":"deepseek-official","agentModel":"deepseek-v4-flash","agentReasoningEffort":"low"}}
|
||||
{"type":"sandbox/mode","data":{"mode":"read-only","source":"delegation"}}
|
||||
{"type":"approval/policy","data":{"policy":"never","source":"delegation"}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE."},{"type":"text","text":"Your parent agent id is \"{{sessionId}}\". Before you finish, send your result to that agent with send_message({ agent_id: \"{{sessionId}}\", message: \"<self-contained result>\" }). The parent shares your workspace but does not automatically receive your transcript, tool output, or reasoning. Send earlier messages as well when a finding changes what the parent should do next; sending a message does not end your turn."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}}
|
||||
{"type":"turn/start","data":{"turn":1}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","data":{"turn":1,"step":1}}
|
||||
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE."},{"type":"text","text":"Your parent agent id is \"{{sessionId}}\". Before you finish, send your result to that agent with send_message({ agent_id: \"{{sessionId}}\", message: \"<self-contained result>\" }). The parent shares your workspace but does not automatically receive your transcript, tool output, or reasoning. Send earlier messages as well when a finding changes what the parent should do next; sending a message does not end your turn."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a subagent delegated by agent \"{{sessionId}}\".\nYour agent id is \"{{sessionId}}\".\nThe root agent of your collaboration tree is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids.\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:identity","text":"You are a subagent delegated by agent \"{{sessionId}}\".\nYour agent id is \"{{sessionId}}\".\nThe root agent of your collaboration tree is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","reasoningEffort":"low"},"tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Use the write tool exactly","messageSeqs":[8],"source":{"kind":"fallback"}}}
|
||||
|
||||
+6
-6
@@ -16,20 +16,20 @@
|
||||
{"type":"step/start","data":{"turn":2,"step":1}}
|
||||
{"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Delegate the write probe to a subagent."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","reasoningEffort":"low"},"tools":["apply_patch","bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"resume","startsSeries":true}}
|
||||
{"type":"developer/message","data":{"turn":2,"step":1,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"apply_patch"},{"type":"tool-addition","toolName":"bash"},{"type":"tool-addition","toolName":"create_goal"},{"type":"tool-addition","toolName":"edit"},{"type":"tool-addition","toolName":"get_goal"},{"type":"tool-addition","toolName":"glob"},{"type":"tool-addition","toolName":"grep"},{"type":"tool-addition","toolName":"interrupt_agent"},{"type":"tool-addition","toolName":"job_kill"},{"type":"tool-addition","toolName":"job_list"},{"type":"tool-addition","toolName":"job_output"},{"type":"tool-addition","toolName":"list_agents"},{"type":"tool-addition","toolName":"read"},{"type":"tool-addition","toolName":"read_image"},{"type":"tool-addition","toolName":"send_message"},{"type":"tool-addition","toolName":"skill"},{"type":"tool-addition","toolName":"subagent"},{"type":"tool-addition","toolName":"subagent_fork"},{"type":"tool-addition","toolName":"todo_write"},{"type":"tool-addition","toolName":"update_goal"},{"type":"tool-addition","toolName":"web_fetch"},{"type":"tool-addition","toolName":"web_search"},{"type":"tool-addition","toolName":"workflow"},{"type":"tool-addition","toolName":"working_directory"},{"type":"tool-addition","toolName":"write"}],"role":"developer","id":"{{sessionId}}"},"headerSeq":18},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","reasoningEffort":"low"},"tools":["apply_patch","bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","wait_agent","web_fetch","web_search","workflow","working_directory","write"]},"reason":"resume","startsSeries":true}}
|
||||
{"type":"developer/message","data":{"turn":2,"step":1,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"apply_patch"},{"type":"tool-addition","toolName":"bash"},{"type":"tool-addition","toolName":"create_goal"},{"type":"tool-addition","toolName":"edit"},{"type":"tool-addition","toolName":"get_goal"},{"type":"tool-addition","toolName":"glob"},{"type":"tool-addition","toolName":"grep"},{"type":"tool-addition","toolName":"interrupt_agent"},{"type":"tool-addition","toolName":"job_kill"},{"type":"tool-addition","toolName":"job_list"},{"type":"tool-addition","toolName":"job_output"},{"type":"tool-addition","toolName":"list_agents"},{"type":"tool-addition","toolName":"read"},{"type":"tool-addition","toolName":"read_image"},{"type":"tool-addition","toolName":"send_message"},{"type":"tool-addition","toolName":"skill"},{"type":"tool-addition","toolName":"subagent"},{"type":"tool-addition","toolName":"subagent_fork"},{"type":"tool-addition","toolName":"todo_write"},{"type":"tool-addition","toolName":"update_goal"},{"type":"tool-addition","toolName":"wait_agent"},{"type":"tool-addition","toolName":"web_fetch"},{"type":"tool-addition","toolName":"web_search"},{"type":"tool-addition","toolName":"workflow"},{"type":"tool-addition","toolName":"working_directory"},{"type":"tool-addition","toolName":"write"}],"role":"developer","id":"{{sessionId}}"},"headerSeq":18},"surfaceOp":"append"}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Tighten this session to read-only.","messageSeqs":[3],"source":{"kind":"fallback"}}}
|
||||
{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"delegate-write","name":"subagent","arguments":"{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"delegate-write","name":"subagent","args":["{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"delegate-write","name":"subagent","arguments":"{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":2,"step":1,"callId":"delegate-write","name":"subagent","arguments":"{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}}
|
||||
{"type":"subagent/catalog","data":{"version":0,"childId":"{{sessionId}}","childCreatedAt":0,"mode":"continuable","label":"Delegated write probe"}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{sessionId}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_DENIED [sandbox: file access denied under read-only mode]"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} finished and will do no further work unless you send it more.","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"}]}}
|
||||
{"type":"tool/result","data":{"turn":2,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"delegate-write"},"toolCallId":"delegate-write","content":[{"type":"text","text":"started subagent {{sessionId}}"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[23],"surfaceOp":"append"}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{sessionId}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_DENIED [sandbox: file access denied under read-only mode]"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} finished and will do no further work unless you send it more.","stopReason":"completed","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"}]}}
|
||||
{"type":"tool/result","data":{"turn":2,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"delegate-write"},"toolCallId":"delegate-write","content":[{"type":"text","text":"started subagent {{sessionId}}"}],"isError":false,"id":"{{sessionId}}"},"meta":{"kind":"activation","subagentId":"{{sessionId}}"}},"sourceEventSeqs":[23],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":2,"step":1}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","data":{"turn":2,"step":2}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Background subagent {{sessionId}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_DENIED [sandbox: file access denied under read-only mode]"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} finished and will do no further work unless you send it more.","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Background subagent {{sessionId}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_DENIED [sandbox: file access denied under read-only mode]"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} finished and will do no further work unless you send it more.","stopReason":"completed","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"The delegated child was denied by the sandbox. PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["The delegated child was denied by the sandbox. PARENT_DONE"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"The delegated child was denied by the sandbox. PARENT_DONE"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":2,"step":2}}
|
||||
{"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}}
|
||||
+3
-3
@@ -2,13 +2,13 @@
|
||||
{"type":"subagent/descriptor","data":{"version":3,"mode":"continuable","provider":"spawn","label":"Return child result","agentProvider":"deepseek-official","agentModel":"deepseek-v4-flash"}}
|
||||
{"type":"sandbox/mode","data":{"mode":"workspace-write","source":"delegation"}}
|
||||
{"type":"approval/policy","data":{"policy":"never","source":"delegation"}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message."},{"type":"text","text":"Your parent agent id is \"{{sessionId}}\". Before you finish, send your result to that agent with send_message({ agent_id: \"{{sessionId}}\", message: \"<self-contained result>\" }). The parent shares your workspace but does not automatically receive your transcript, tool output, or reasoning. Send earlier messages as well when a finding changes what the parent should do next; sending a message does not end your turn."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}}
|
||||
{"type":"turn/start","data":{"turn":1}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","data":{"turn":1,"step":1}}
|
||||
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message."},{"type":"text","text":"Your parent agent id is \"{{sessionId}}\". Before you finish, send your result to that agent with send_message({ agent_id: \"{{sessionId}}\", message: \"<self-contained result>\" }). The parent shares your workspace but does not automatically receive your transcript, tool output, or reasoning. Send earlier messages as well when a finding changes what the parent should do next; sending a message does not end your turn."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a subagent delegated by agent \"{{sessionId}}\".\nYour agent id is \"{{sessionId}}\".\nThe root agent of your collaboration tree is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids.\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:identity","text":"You are a subagent delegated by agent \"{{sessionId}}\".\nYour agent id is \"{{sessionId}}\".\nThe root agent of your collaboration tree is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Reply with exactly CHILD_RESULT and","messageSeqs":[8],"source":{"kind":"fallback"}}}
|
||||
|
||||
+4
-4
@@ -4,7 +4,7 @@
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"Start one continuable subagent and acknowledge its completion notice. Do not call list_agents or send_message."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"Start one continuable subagent and","messageSeqs":[8],"source":{"kind":"fallback"}}}}
|
||||
@@ -13,15 +13,15 @@
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":15,"time":0,"data":{"turn":1,"step":1,"callId":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":16,"time":0,"data":{"title":"Subagent settlement","messageSeqs":[8],"source":{"kind":"provider","provider":"session-title-first-prompt-llm","model":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"subagent/catalog","seq":17,"time":0,"data":{"version":0,"childId":"{{sessionId}}","childCreatedAt":0,"mode":"continuable","label":"Return child result"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":18,"time":0,"data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"start-child"},"toolCallId":"start-child","content":[{"type":"text","text":"started subagent {{sessionId}}"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":18,"time":0,"data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"start-child"},"toolCallId":"start-child","content":[{"type":"text","text":"started subagent {{sessionId}}"}],"isError":false,"id":"{{sessionId}}"},"meta":{"kind":"activation","subagentId":"{{sessionId}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":19,"time":0,"data":{"turn":1,"step":1}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":20,"time":0,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{sessionId}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_RESULT"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} finished and will do no further work unless you send it more.","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"}]}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":20,"time":0,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{sessionId}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_RESULT"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} finished and will do no further work unless you send it more.","stopReason":"completed","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"}]}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":21,"time":0,"data":{"turn":1,"step":2}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":22,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["STARTED"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"STARTED"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":23,"time":0,"data":{"turn":1,"step":2}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":24,"time":0,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":25,"time":0,"data":{"turn":1,"step":3}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":26,"time":0,"data":{"content":[{"type":"text","text":"Background subagent {{sessionId}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_RESULT"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} finished and will do no further work unless you send it more.","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":26,"time":0,"data":{"content":[{"type":"text","text":"Background subagent {{sessionId}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_RESULT"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} finished and will do no further work unless you send it more.","stopReason":"completed","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":27,"time":0,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_RECEIVED_SETTLEMENT_NOTICE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["PARENT_RECEIVED_SETTLEMENT_NOTICE"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_RECEIVED_SETTLEMENT_NOTICE"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":28,"time":0,"data":{"turn":1,"step":3}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":29,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}}
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
{"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Acknowledge the current workspace instruction."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"<system-reminder>\nUpdated instructions from: AGENTS.md\n\nThis file changed after it was loaded. Use the following content instead of the previously loaded instructions from this file.\n\nNew workspace instruction after offline edit.\n\n</system-reminder>"}],"source":{"kind":"agent-instructions","form":"instructions","changes":[{"action":"replace","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"d8375b516f158718bd3463bc8eb7ed42c011b29f"}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Remember the workspace instruction.","messageSeqs":[3],"source":{"kind":"fallback"}}}
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
{"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Acknowledge the current workspace instruction."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"<system-reminder>\nThis complete workspace instruction baseline replaces all earlier workspace instruction baselines. The following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nCurrent AGENTS rule.\n\n\nInstructions from: CLAUDE.md\n\nCurrent CLAUDE rule.\n\n</system-reminder>"}],"source":{"kind":"agent-instructions","form":"instructions","baseline":true,"baselineIdentity":"{\"projectRoot\":\"\",\"projectRootMarkers\":[\".git\"],\"maxBytes\":65536,\"maxSourceBytes\":1048576,\"instructionFileCandidates\":[\"AGENTS.md\",\"CLAUDE.md\"],\"localInstructionFileCandidates\":[\"AGENTS.local.md\",\"CLAUDE.local.md\"]}","changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"7f53d2327837129750aef117f9754a001c46cf68"},{"action":"set","scope":".\u0000CLAUDE.md","path":"CLAUDE.md","digest":"5b1e9e3fd759eee6b43ceff899e47fb10c64701a"}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"<agent-id>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Remember the workspace instruction.","messageSeqs":[3],"source":{"kind":"fallback"}}}
|
||||
|
||||
+59
-28
@@ -15,6 +15,12 @@ function latestAssistantCalls(messages) {
|
||||
return assistant?.content.filter(block => block.type === 'tool-call').map(block => block.name) ?? []
|
||||
}
|
||||
|
||||
function hasNewCoordinationInput(messages) {
|
||||
const toolIndex = messages.findLastIndex(message => message.role === 'tool')
|
||||
return messages.some((message, index) => index > toolIndex && message.role === 'user'
|
||||
&& ['agent-message', 'subagent-settled'].includes(message.source?.kind))
|
||||
}
|
||||
|
||||
function hasTaskAction(messages, action) {
|
||||
return messages.some(message => message.role === 'assistant'
|
||||
&& message.content.some((block) => {
|
||||
@@ -63,6 +69,11 @@ function textChunks(text) {
|
||||
|
||||
function researcher(messages) {
|
||||
const names = calls(messages)
|
||||
if (!names.includes('list_agents')) return toolChunks([{ name: 'list_agents', args: {} }])
|
||||
const sibling = messages.filter(message => message.role === 'tool')
|
||||
.flatMap(message => message.content.filter(block => block.type === 'text').flatMap(block => block.text.split('\n')))
|
||||
.find(line => line.endsWith('— Own deterministic implementation.'))?.split(' ')[0]
|
||||
if (sibling === undefined) throw new Error('list_agents did not expose the implementation sibling')
|
||||
if (!names.includes('team_task_create')) {
|
||||
return toolChunks([{ name: 'team_task_create', args: {
|
||||
subject: 'Research', description: 'Collect the deterministic finding.', write_scopes: ['research'],
|
||||
@@ -76,13 +87,13 @@ function researcher(messages) {
|
||||
if (!names.includes('send_message')) {
|
||||
return toolChunks([
|
||||
{ name: 'team_task_update', args: { task_id: 'task-1', expected_revision: 2, action: 'complete' } },
|
||||
{ name: 'send_message', args: { target: 'implementer', message: 'Research complete: use the deterministic finding.' } },
|
||||
{ name: 'send_message', args: { agent_id: sibling, message: 'Research complete: use the deterministic finding.' } },
|
||||
])
|
||||
}
|
||||
return textChunks('Research teammate complete.')
|
||||
}
|
||||
|
||||
function implementer(messages) {
|
||||
function implementer(messages, parentId) {
|
||||
const names = calls(messages)
|
||||
const last = latestAssistantCalls(messages)
|
||||
const text = latestToolText(messages)
|
||||
@@ -98,7 +109,7 @@ function implementer(messages) {
|
||||
write_scopes: ['implementation'],
|
||||
} }])
|
||||
}
|
||||
if (last.includes('wait_agent')) {
|
||||
if (last.includes('wait_agent') || hasNewCoordinationInput(messages)) {
|
||||
return toolChunks([{ name: 'team_task_get', args: { task_id: 'task-1' } }])
|
||||
}
|
||||
return toolChunks([{ name: 'wait_agent', args: { timeout_ms: 10000 } }])
|
||||
@@ -109,10 +120,10 @@ function implementer(messages) {
|
||||
task_id: 'task-2', expected_revision: 1, action: 'claim',
|
||||
} }])
|
||||
}
|
||||
if (last.includes('team_task_get')) {
|
||||
return toolChunks([{ name: 'team_task_get', args: { task_id: 'task-1' } }])
|
||||
if (last.includes('team_task_get') && !hasNewCoordinationInput(messages)) {
|
||||
return toolChunks([{ name: 'wait_agent', args: { timeout_ms: 10000 } }])
|
||||
}
|
||||
if (last.includes('wait_agent')) {
|
||||
if (last.includes('wait_agent') || hasNewCoordinationInput(messages)) {
|
||||
return toolChunks([{ name: 'team_task_get', args: { task_id: 'task-1' } }])
|
||||
}
|
||||
return toolChunks([{ name: 'team_task_get', args: { task_id: 'task-1' } }])
|
||||
@@ -123,7 +134,7 @@ function implementer(messages) {
|
||||
if (!names.includes('send_message')) {
|
||||
return toolChunks([
|
||||
{ name: 'team_task_update', args: { task_id: 'task-2', expected_revision: 2, action: 'complete' } },
|
||||
{ name: 'send_message', args: { target: 'lead', message: 'Implementation complete and verified.' } },
|
||||
{ name: 'send_message', args: { agent_id: parentId, message: 'Implementation complete and verified.' } },
|
||||
])
|
||||
}
|
||||
return textChunks('Implementation teammate complete.')
|
||||
@@ -138,26 +149,22 @@ function lead(messages) {
|
||||
script: 'return await agent("TEAM_WORKFLOW_CHILD");',
|
||||
} }])
|
||||
}
|
||||
const spawned = names.filter(name => name === 'spawn_teammate').length
|
||||
const spawned = names.filter(name => name === 'subagent').length
|
||||
if (spawned === 0) {
|
||||
return toolChunks([{
|
||||
name: 'spawn_teammate',
|
||||
name: 'subagent',
|
||||
args: {
|
||||
name: 'implementer',
|
||||
description: 'Own deterministic implementation.',
|
||||
prompt: 'IMPLEMENTER_MARK: wait for research, complete dependent task 2, report to lead.',
|
||||
context: 'fresh',
|
||||
},
|
||||
}])
|
||||
}
|
||||
if (spawned === 1) {
|
||||
return toolChunks([{
|
||||
name: 'spawn_teammate',
|
||||
name: 'subagent',
|
||||
args: {
|
||||
name: 'researcher',
|
||||
description: 'Own deterministic research.',
|
||||
prompt: 'RESEARCHER_MARK: complete research task 1, message implementer, then finish.',
|
||||
context: 'fresh',
|
||||
},
|
||||
}])
|
||||
}
|
||||
@@ -165,35 +172,38 @@ function lead(messages) {
|
||||
if (last.includes('team_task_list')) {
|
||||
const completed = result.match(/"status":"completed"/gu)?.length ?? 0
|
||||
if (completed >= 2) return toolChunks([{ name: 'list_agents', args: {} }])
|
||||
return toolChunks([{ name: 'team_task_list', args: {} }])
|
||||
return toolChunks([{ name: hasNewCoordinationInput(messages) ? 'team_task_list' : 'wait_agent',
|
||||
args: hasNewCoordinationInput(messages) ? {} : { timeout_ms: 10000 } }])
|
||||
}
|
||||
if (last.includes('list_agents')) {
|
||||
const inactive = result.match(/"status":"inactive"/gu)?.length ?? 0
|
||||
const inactive = result.split('\n')
|
||||
.filter(row => row.includes('— Own deterministic ') && row.includes('[inactive]')).length
|
||||
if (inactive >= 2) return textChunks('TEAM_WORKFLOW_OK: both teammates and dependent tasks completed.')
|
||||
return toolChunks([{ name: 'list_agents', args: {} }])
|
||||
return toolChunks([{ name: hasNewCoordinationInput(messages) ? 'list_agents' : 'wait_agent',
|
||||
args: hasNewCoordinationInput(messages) ? {} : { timeout_ms: 10000 } }])
|
||||
}
|
||||
if (last.includes('wait_agent')) return toolChunks([{ name: 'team_task_list', args: {} }])
|
||||
if (last.includes('wait_agent') || hasNewCoordinationInput(messages)) return toolChunks([{ name: 'team_task_list', args: {} }])
|
||||
return toolChunks([{ name: 'wait_agent', args: { timeout_ms: 10000 } }])
|
||||
}
|
||||
|
||||
class TeamFixtureAdapter extends LlmAdapter {
|
||||
async * stream(options) {
|
||||
const tools = options.tools.map(tool => tool.name)
|
||||
if (tools.includes('subagent') || tools.includes('subagent_fork')) {
|
||||
throw new Error('Team profile exposes a direct subagent tool')
|
||||
}
|
||||
const initial = options.messages.findLast(message => message.role === 'user' && message.source.kind === 'user')
|
||||
const workflowChild = initial?.content[0]?.text === 'TEAM_WORKFLOW_CHILD'
|
||||
const context = options.messages.findLast(message => message.source?.kind === 'runtime-context')
|
||||
const identity = context?.source.sections?.find(section => section.name === 'team:identity')?.text
|
||||
if (!workflowChild && (!tools.includes('spawn_teammate') || !tools.includes('workflow'))) {
|
||||
throw new Error('Team profile is missing teammate or workflow tools')
|
||||
const section = name => context?.source.sections?.find(candidate => candidate.name === name)?.text
|
||||
const role = section('subagent:identity')
|
||||
const parentId = role?.match(/You are a subagent delegated by agent "([^"]+)"/u)?.[1]
|
||||
const task = initial?.content.filter(block => block.type === 'text').map(block => block.text).join('\n')
|
||||
if (!workflowChild && (!tools.includes('subagent') || !tools.includes('workflow') || !tools.includes('team_task_create'))) {
|
||||
throw new Error('Team profile is missing delegation, workflow, or task tools')
|
||||
}
|
||||
let chunks
|
||||
if (workflowChild) chunks = textChunks('Fresh workflow child complete.')
|
||||
else if (identity?.startsWith('You are teammate "researcher".')) chunks = researcher(options.messages)
|
||||
else if (identity?.startsWith('You are teammate "implementer".')) chunks = implementer(options.messages)
|
||||
else if (identity?.startsWith('You are the Team Lead named "lead".')) chunks = lead(options.messages)
|
||||
else if (task?.startsWith('RESEARCHER_MARK:')) chunks = researcher(options.messages)
|
||||
else if (task?.startsWith('IMPLEMENTER_MARK:')) chunks = implementer(options.messages, parentId)
|
||||
else if (role?.startsWith('You are the root agent. Your agent id is')) chunks = lead(options.messages)
|
||||
else throw new Error('Team runtime context is missing a recognized identity')
|
||||
for (const chunk of chunks) {
|
||||
options.signal?.throwIfAborted()
|
||||
@@ -205,9 +215,30 @@ class TeamFixtureAdapter extends LlmAdapter {
|
||||
/** Cordis plugin name. */
|
||||
export const name = 'team-fixture-llm'
|
||||
/** LLM registry dependency. */
|
||||
export const inject = ['llm']
|
||||
export const inject = ['llm', 'tools', 'agentTeams']
|
||||
|
||||
/** Register the keyless adapter on the shipped default provider route. */
|
||||
export function apply(ctx) {
|
||||
ctx.on('agent/created', async ({ agent }) => {
|
||||
if (agent.session.header.parentSession === undefined) await ctx.agentTeams.setEnabled(agent, true)
|
||||
})
|
||||
const implementerWaiting = Promise.withResolvers()
|
||||
const leadWaiting = Promise.withResolvers()
|
||||
for (const gate of [implementerWaiting, leadWaiting]) void gate.promise.catch(() => undefined)
|
||||
ctx.effect(() => () => {
|
||||
implementerWaiting.reject(new Error('Team fixture disposed before implementer wait'))
|
||||
leadWaiting.reject(new Error('Team fixture disposed before lead wait'))
|
||||
})
|
||||
ctx.on('tools/execute', async (exec, next) => {
|
||||
if (exec.name === 'team_task_create' && exec.arguments.subject === 'Research') {
|
||||
await Promise.all([implementerWaiting.promise, leadWaiting.promise])
|
||||
}
|
||||
const result = next()
|
||||
if (exec.name === 'wait_agent') {
|
||||
if (exec.agent.session.header.origin === 'subagent') implementerWaiting.resolve()
|
||||
else leadWaiting.resolve()
|
||||
}
|
||||
return result
|
||||
})
|
||||
ctx.llm.registerAdapter(['deepseek-official'], new TeamFixtureAdapter())
|
||||
}
|
||||
@@ -1093,7 +1093,7 @@ describe('headless stream-json snapshots', () => {
|
||||
}
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('runs a keyless Agent Team with peer mail, dependent tasks, waiting, and Lead aggregation', async () => {
|
||||
it('runs a keyless Agent Team with described teammates, sibling messages, dependent tasks, waiting, and Lead aggregation', async () => {
|
||||
let projection: unknown
|
||||
const result = await runLoaderSmoke({
|
||||
label: 'Agent Teams headless snapshot',
|
||||
@@ -1125,8 +1125,8 @@ describe('headless stream-json snapshots', () => {
|
||||
expect(workflowRows.filter(row => row.type === 'user/message'
|
||||
&& ((row.data as JsonObject).source as JsonObject).kind === 'user').map(row => row.data))
|
||||
.toEqual([expect.objectContaining({ content: [{ type: 'text', text: 'TEAM_WORKFLOW_CHILD' }] })])
|
||||
const members = rows.filter(row => row.type === 'team/member')
|
||||
.map(row => ((row.data as JsonObject).member as JsonObject))
|
||||
const teammates = rows.filter(row => row.type === 'subagent/catalog')
|
||||
.flatMap(row => typeof (row.data as JsonObject).label === 'string' && ((row.data as JsonObject).label as string).startsWith('Own deterministic ') ? [(row.data as JsonObject).label as string] : [])
|
||||
const tasks = rows.filter(row => row.type === 'team/task')
|
||||
.map(row => ((row.data as JsonObject).task as JsonObject))
|
||||
const latestTasks = Object.values(Object.fromEntries(tasks.map(task => [String(task.subject), task])))
|
||||
@@ -1179,21 +1179,51 @@ describe('headless stream-json snapshots', () => {
|
||||
&& ((row.data as JsonObject).source as JsonObject).kind === 'user')
|
||||
if (initial === undefined) throw new Error('Teammate Session has no initial task')
|
||||
const content = (initial.data as JsonObject).content as JsonObject[]
|
||||
expect(content).toHaveLength(1)
|
||||
// The task block is followed by the standard return guidance naming the Lead.
|
||||
expect(content).toHaveLength(2)
|
||||
const context = parseJsonl(log.content).find(row => row.type === 'user/message'
|
||||
&& ((row.data as JsonObject).source as JsonObject).kind === 'runtime-context')
|
||||
const source = (context?.data as JsonObject)?.source as JsonObject
|
||||
const sections = source?.sections as JsonObject[] | undefined
|
||||
const identity = sections?.find(section => section.name === 'team:identity')?.text
|
||||
if (typeof identity !== 'string') throw new Error('Teammate runtime context has no identity')
|
||||
return [identity]
|
||||
const leadId = parent.header.id as string
|
||||
const text = (name: string) => {
|
||||
const value = sections?.find(section => section.name === name)?.text
|
||||
if (typeof value !== 'string') throw new Error(`Teammate runtime context has no ${name}`)
|
||||
return value.replaceAll(log.header.id as string, '<self>').replaceAll(leadId, '<lead>')
|
||||
}
|
||||
expect(sections?.some(section => section.name === 'team:identity')).toBe(false)
|
||||
return [text('subagent:identity')]
|
||||
}).sort()
|
||||
const allRows = logs.flatMap(log => parseJsonl(log.content))
|
||||
const messageSources = allRows.flatMap((row) => {
|
||||
const data = row.data as JsonObject
|
||||
if (row.type === 'user/message') return [(data.source as JsonObject).kind]
|
||||
if (row.type === 'agent/inbox/spliced') {
|
||||
return (data.inserted as JsonObject[]).map(message => (message.source as JsonObject).kind)
|
||||
}
|
||||
return []
|
||||
})
|
||||
expect(messageSources).not.toContain('team-task-change')
|
||||
const waitIds = new Set(allRows.filter(row => row.type === 'tool/call'
|
||||
&& (row.data as JsonObject).name === 'wait_agent').map(row => (row.data as JsonObject).callId))
|
||||
const waitResults = allRows.filter((row) => {
|
||||
if (row.type !== 'tool/result') return false
|
||||
const message = (row.data as JsonObject).message as JsonObject
|
||||
return waitIds.has((message.source as JsonObject).callId)
|
||||
})
|
||||
expect(waitResults).toHaveLength(waitIds.size)
|
||||
for (const row of waitResults) {
|
||||
const message = (row.data as JsonObject).message as JsonObject
|
||||
expect(message.isError).not.toBe(true)
|
||||
const content = message.content as JsonObject[]
|
||||
expect(JSON.parse(content.filter(block => block.type === 'text').map(block => block.text).join('')))
|
||||
.toEqual({ timedOut: false })
|
||||
}
|
||||
projection = {
|
||||
sessions: logs.length,
|
||||
workflowStopReason: (rows.find(row => row.type === 'tool-workflow/run-end')?.data as JsonObject)?.stopReason,
|
||||
memberEdges: members.length,
|
||||
identities,
|
||||
activeMembers: members.filter(member => member.phase === 'active').map(member => member.name).sort(),
|
||||
teammates: [...teammates].sort(),
|
||||
tasks: latestTasks.map(task => ({
|
||||
subject: task.subject,
|
||||
revision: task.revision,
|
||||
@@ -1203,7 +1233,7 @@ describe('headless stream-json snapshots', () => {
|
||||
deliveredMessages: rows.filter(row => row.type === 'team/message/delivered').length,
|
||||
waited: rows.some(row => row.type === 'tool/call'
|
||||
&& (row.data as JsonObject).name === 'wait_agent'),
|
||||
checkedRoster: rows.some(row => row.type === 'tool/call'
|
||||
checkedAgents: rows.some(row => row.type === 'tool/call'
|
||||
&& (row.data as JsonObject).name === 'list_agents'),
|
||||
steerEvidence: {
|
||||
nextStepInbox: steeredInboxIndex >= 0,
|
||||
@@ -1221,25 +1251,22 @@ describe('headless stream-json snapshots', () => {
|
||||
})
|
||||
expect(projection).toMatchInlineSnapshot(`
|
||||
{
|
||||
"activeMembers": [
|
||||
"implementer",
|
||||
"researcher",
|
||||
],
|
||||
"checkedRoster": true,
|
||||
"checkedAgents": true,
|
||||
"deliveredMessages": 0,
|
||||
"identities": [
|
||||
"You are teammate "implementer".
|
||||
Your Team Lead is named "lead".
|
||||
To message your Team Lead, use send_message({ target: "lead", message: "..." }).
|
||||
Use list_agents({}) to find your teammates and their names.
|
||||
To message another teammate, use send_message({ target: "<teammate name>", message: "..." }).",
|
||||
"You are teammate "researcher".
|
||||
Your Team Lead is named "lead".
|
||||
To message your Team Lead, use send_message({ target: "lead", message: "..." }).
|
||||
Use list_agents({}) to find your teammates and their names.
|
||||
To message another teammate, use send_message({ target: "<teammate name>", message: "..." }).",
|
||||
"You are a subagent delegated by agent "<lead>".
|
||||
Your agent id is "<self>".
|
||||
The root agent of your collaboration tree is "<lead>".
|
||||
|
||||
To message other agents, use send_message({ agent_id: "<agent-id>", message: "..." }).
|
||||
Use list_agents({}) to find your collaborator agents and their ids.",
|
||||
"You are a subagent delegated by agent "<lead>".
|
||||
Your agent id is "<self>".
|
||||
The root agent of your collaboration tree is "<lead>".
|
||||
|
||||
To message other agents, use send_message({ agent_id: "<agent-id>", message: "..." }).
|
||||
Use list_agents({}) to find your collaborator agents and their ids.",
|
||||
],
|
||||
"memberEdges": 4,
|
||||
"queuedMessages": 0,
|
||||
"sessions": 4,
|
||||
"steerEvidence": {
|
||||
@@ -1260,6 +1287,10 @@ describe('headless stream-json snapshots', () => {
|
||||
"subject": "Research",
|
||||
},
|
||||
],
|
||||
"teammates": [
|
||||
"Own deterministic implementation.",
|
||||
"Own deterministic research.",
|
||||
],
|
||||
"waited": true,
|
||||
"workflowStopReason": "completed",
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Assembled-app regression: a parent-owned catalog event remains sufficient
|
||||
* for direct discovery when the persisted child has no descriptor event.
|
||||
* for descendant discovery when the persisted child has no descriptor event.
|
||||
*/
|
||||
|
||||
import { readFile, readdir, writeFile } from 'node:fs/promises'
|
||||
@@ -42,7 +42,7 @@ async function expectSession(actual: string, expectedPath: string): Promise<void
|
||||
|
||||
/**
|
||||
* Seed a completed parent turn with one catalogued cold child whose own log
|
||||
* has no descriptor event. Direct discovery must not read that child log.
|
||||
* has no descriptor event. Descendant discovery requires no activation descriptor.
|
||||
*/
|
||||
async function seedCataloguedDescriptorlessChild(root: string, cwd: string): Promise<void> {
|
||||
const ctx = new Context()
|
||||
@@ -106,7 +106,7 @@ async function seedCataloguedDescriptorlessChild(root: string, cwd: string): Pro
|
||||
}
|
||||
|
||||
describe('parent-owned cold child catalog snapshot', () => {
|
||||
it('discovers a catalogued child without reading its descriptor through the assembled headless app', async () => {
|
||||
it('discovers a descriptorless catalogued child with its tree position through the assembled headless app', async () => {
|
||||
let cwd = ''
|
||||
const result = await runLoaderSmoke({
|
||||
label: 'subagent diagnostic headless stream-json snapshot',
|
||||
@@ -131,9 +131,8 @@ describe('parent-owned cold child catalog snapshot', () => {
|
||||
const parent = logs.find(content => content.includes('"subagent-diagnostic-parent"'))
|
||||
if (parent === undefined) throw new Error('missing persisted parent log')
|
||||
|
||||
// THE model-visible fact: the parent catalog supplies discovery even
|
||||
// though consulting the descriptor-less child would classify it corrupt.
|
||||
expect(parent).toContain(`${childId} [inactive] — descriptorless child`)
|
||||
// Discovery reads child catalogs without requiring activation descriptors.
|
||||
expect(parent).toContain(`${childId} [inactive] parent=${parentId} depth=1 — descriptorless child`)
|
||||
expect(parent).not.toContain('[diagnostic: corrupt]')
|
||||
|
||||
const context: NormalizeContext = { sessionIds: [parentId, childId], cwd }
|
||||
|
||||
@@ -55,7 +55,7 @@ export async function withDefaultWeb(
|
||||
await writeFile(patch, JSON.stringify([{ insert: [{
|
||||
id: 'runtime-roster-observer',
|
||||
name: pathToFileURL(join(root, 'fixtures/runtime-roster-observer.js')).href,
|
||||
config: { negativeControl: pathToFileURL(join(repoRoot, 'packages/experimental/client-ui-agent-team/lib/index.js')).href },
|
||||
config: { negativeControl: pathToFileURL(join(repoRoot, 'packages/experimental/client-ui-voice-input/lib/index.js')).href },
|
||||
}] }]) + '\n')
|
||||
const launch = resolveExampleLaunch({
|
||||
srcBin: join(repoRoot, 'apps/cli/src/bin.ts'),
|
||||
|
||||
@@ -7,7 +7,7 @@ import { modulePackage } from './runtime-roster.ts'
|
||||
import { experimentalRuntimeReferences } from './runtime-roster-isolation.ts'
|
||||
import { withDefaultWeb, webGet } from './default-web-process.ts'
|
||||
|
||||
const experimentalName = '@deepseek-ai/dsh-experimental-client-ui-agent-team'
|
||||
const experimentalName = '@deepseek-ai/dsh-experimental-client-ui-voice-input'
|
||||
|
||||
it('boots default Web with the shipped Schedule rows, without experimental modules, time context, or an active built-in Browser', async (test) => {
|
||||
await withDefaultWeb(test, async ({ url, request }) => {
|
||||
@@ -37,6 +37,9 @@ it('boots default Web with the shipped Schedule rows, without experimental modul
|
||||
// The shipped composition mounts the Host Schedule service and its task
|
||||
// page; the clock stays preset-level, so neither plane carries a row for it.
|
||||
expect(roster.entries).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ name: '@deepseek-ai/dsh-agent-team', state: FiberState.ACTIVE }),
|
||||
expect.objectContaining({ name: '@deepseek-ai/dsh-tool-agent-team', state: FiberState.ACTIVE }),
|
||||
expect.objectContaining({ name: '@deepseek-ai/dsh-client-ui-agent-team', state: FiberState.ACTIVE }),
|
||||
expect.objectContaining({ name: '@deepseek-ai/dsh-schedule', state: FiberState.ACTIVE }),
|
||||
expect.objectContaining({ name: '@deepseek-ai/dsh-client-ui-schedule', state: FiberState.ACTIVE }),
|
||||
]))
|
||||
@@ -50,7 +53,7 @@ it('boots default Web with the shipped Schedule rows, without experimental modul
|
||||
const mounted = contaminated.plugins.filter(plugin => plugin.modules.some(url => modulePackage(url) === experimentalName))
|
||||
expect(mounted).toEqual([expect.objectContaining({ state: FiberState.ACTIVE })])
|
||||
expect(experimentalRuntimeReferences(contaminated)).toEqual(expect.arrayContaining([
|
||||
expect.stringContaining('/packages/experimental/client-ui-agent-team/'),
|
||||
expect.stringContaining('/packages/experimental/client-ui-voice-input/'),
|
||||
]))
|
||||
})
|
||||
})
|
||||
@@ -248,9 +248,9 @@ beforeAll(async () => {
|
||||
}, 120_000)
|
||||
|
||||
describe('the shipped Web composition', () => {
|
||||
it('exposes only the directory tool in the global layer', () => {
|
||||
// Directory selection is global; action tools belong to their Agent presets.
|
||||
expect(toolNames(ctx)).toEqual(['working_directory'])
|
||||
it('registers the directory and conditional Team tools in the global layer', () => {
|
||||
// Team registrations are shared; session mode controls their step-time availability.
|
||||
expect(toolNames(ctx)).toEqual(['team_task_create', 'team_task_get', 'team_task_list', 'team_task_update', 'working_directory'])
|
||||
})
|
||||
|
||||
it('keeps the token meter and its context-meter projections on the host plane', async () => {
|
||||
@@ -304,7 +304,7 @@ describe('the shipped Web composition', () => {
|
||||
'get_goal', 'interrupt_agent', 'job_kill', 'job_list', 'job_output', 'list_agents', 'list_subagent_models', 'plugin_manager', 'present', 'read', 'read_image',
|
||||
'schedule_create', 'schedule_edit', 'schedule_list', 'schedule_view',
|
||||
'send_message', 'skill',
|
||||
'subagent', 'subagent_fork', 'todo_write', 'update_goal', 'web_fetch', 'web_search',
|
||||
'subagent', 'subagent_fork', 'team_task_create', 'team_task_get', 'team_task_list', 'team_task_update', 'todo_write', 'update_goal', 'wait_agent', 'web_fetch', 'web_search',
|
||||
'workflow', 'working_directory', 'write',
|
||||
])
|
||||
expect(ctx.commands.find(handle.agent, 'goal')).toBeDefined()
|
||||
@@ -408,7 +408,7 @@ describe('the shipped Web composition', () => {
|
||||
expect(assembly.sections).toEqual([
|
||||
{ name: 'deployment:persona-prefix', text: MINIMAL_PROMPT },
|
||||
])
|
||||
expect(assembly.tools.map(tool => tool.name)).toEqual(['bash', 'read_image', 'working_directory'])
|
||||
expect(assembly.tools.map(tool => tool.name)).toEqual(['bash', 'read_image', 'team_task_create', 'team_task_get', 'team_task_list', 'team_task_update', 'working_directory'])
|
||||
expect(assembly.tools.find(tool => tool.name === 'bash')?.description).toBe(MINIMAL_BASH_DESCRIPTION)
|
||||
expect(ctx.commands.find(handle.agent, 'goal')).toBeUndefined()
|
||||
// serviceFor reports preset-owned providers; unisolated consumers inherit the host fs.
|
||||
@@ -485,7 +485,7 @@ describe('the shipped Web composition', () => {
|
||||
const last = adapter.requests.at(-1)
|
||||
expect(last?.messages.flatMap(message => message.content)
|
||||
.some(block => block.type === 'text' && block.text.includes('Preserve the requested project task.'))).toBe(true)
|
||||
expect(toolNames(ctx, handle.agent)).toEqual(['bash', 'read_image', 'working_directory'])
|
||||
expect(toolNames(ctx, handle.agent)).toEqual(['bash', 'read_image', 'team_task_create', 'team_task_get', 'team_task_list', 'team_task_update', 'working_directory'])
|
||||
} finally {
|
||||
await handle.dispose()
|
||||
}
|
||||
@@ -529,7 +529,7 @@ describe('the shipped Web composition', () => {
|
||||
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
|
||||
})
|
||||
try {
|
||||
expect(toolNames(ctx, minimal.agent)).toEqual(['bash', 'read_image', 'working_directory'])
|
||||
expect(toolNames(ctx, minimal.agent)).toEqual(['bash', 'read_image', 'team_task_create', 'team_task_get', 'team_task_list', 'team_task_update', 'working_directory'])
|
||||
expect(toolNames(ctx, full.agent).length).toBeGreaterThan(10)
|
||||
expect(ctx.agentPresets.serviceFor(minimal.agent, 'compaction'))
|
||||
.not.toBe(ctx.agentPresets.serviceFor(full.agent, 'compaction'))
|
||||
@@ -538,7 +538,7 @@ describe('the shipped Web composition', () => {
|
||||
|
||||
// Tearing the minimal session down leaves the full one whole.
|
||||
expect(toolNames(ctx, full.agent).length).toBeGreaterThan(10)
|
||||
expect(toolNames(ctx)).toEqual(['working_directory'])
|
||||
expect(toolNames(ctx)).toEqual(['team_task_create', 'team_task_get', 'team_task_list', 'team_task_update', 'working_directory'])
|
||||
} finally {
|
||||
await full.dispose()
|
||||
}
|
||||
@@ -697,7 +697,7 @@ describe('the shipped Web composition', () => {
|
||||
// stays the preset's choice — minimal mounts no `tool-skill`, so its
|
||||
// tool table has no loader even though the global layer is readable.
|
||||
expect((await ctx.skills.list({ scope: handle.agent })).map(skill => skill.name)).toContain('dsh-badge')
|
||||
expect(toolNames(ctx, handle.agent)).toEqual(['bash', 'read_image', 'working_directory'])
|
||||
expect(toolNames(ctx, handle.agent)).toEqual(['bash', 'read_image', 'team_task_create', 'team_task_get', 'team_task_list', 'team_task_update', 'working_directory'])
|
||||
} finally {
|
||||
await handle.dispose()
|
||||
}
|
||||
@@ -790,7 +790,7 @@ describe.each(['web', 'desktop'] as const)('%s native subagent bundles', (surfac
|
||||
const productCtx = await bootSelected(installed)
|
||||
const spawn = vi.spyOn(productCtx.subprocess, 'spawn')
|
||||
try {
|
||||
expect(productCtx.subagents.list().filter(name => (products as readonly string[]).includes(name)).sort())
|
||||
expect(products.filter(name => productCtx.subagents.getProvider(name) !== undefined).sort())
|
||||
.toEqual([...installed].sort())
|
||||
const expected = installed.map(toolName).sort()
|
||||
for (const preset of ['standard', 'cordis', 'ptc', 'minimal']) {
|
||||
@@ -881,6 +881,8 @@ describe('a user preset declared from the shipped cordis rows', () => {
|
||||
// The `Tool` provider is the one built over the host context: it
|
||||
// answers with the shared registry's view of the REQUESTING agent,
|
||||
// so the copy sees its own preset's tools, not the host's empty set.
|
||||
const spills = vi.spyOn(copyCtx.spillStore, 'saveText')
|
||||
onTestFinished(() => { spills.mockRestore() })
|
||||
const queried = await executeToolFixture(copyCtx, {
|
||||
callId: ToolCallId('copied-preset-inspect-tools'),
|
||||
name: 'cordis_inspect_query',
|
||||
@@ -889,11 +891,8 @@ describe('a user preset declared from the shipped cordis rows', () => {
|
||||
agent: copied.agent,
|
||||
})
|
||||
expect(queried.isError).toBe(false)
|
||||
// The whole-table answer can pass the inline token budget once the
|
||||
// preset's tool table grows, and the spill policy then replaces its
|
||||
// middle with a gap plus a spill footer. What this case proves is the
|
||||
// provider's answer, so assert the names it must carry.
|
||||
const queriedText = resultText(queried)
|
||||
// The tool catalog may exceed the inline budget; assert its complete stored text.
|
||||
const queriedText = spills.mock.calls.at(-1)?.[0].content ?? resultText(queried)
|
||||
for (const toolName of ['bash', 'cordis_inspect_list', 'cordis_inspect_query', 'plugin_manager']) {
|
||||
expect(queriedText).toContain(`"name": "${toolName}"`)
|
||||
}
|
||||
@@ -1088,7 +1087,7 @@ describe('the default preset as a user setting', () => {
|
||||
})
|
||||
try {
|
||||
// An omitted preset id resolves the stored default instead of the composed default.
|
||||
expect(toolNames(ctx, handle.agent)).toEqual(['bash', 'read_image', 'working_directory'])
|
||||
expect(toolNames(ctx, handle.agent)).toEqual(['bash', 'read_image', 'team_task_create', 'team_task_get', 'team_task_list', 'team_task_update', 'working_directory'])
|
||||
} finally {
|
||||
await handle.dispose()
|
||||
}
|
||||
@@ -1134,7 +1133,7 @@ describe('a profile patch stored before Developer tools owned preset selection',
|
||||
setup: agentCtx => legacy.agentPresets.mount(agentCtx).then(() => undefined),
|
||||
})
|
||||
try {
|
||||
expect(toolNames(legacy, handle.agent)).toEqual(['bash', 'read_image', 'working_directory'])
|
||||
expect(toolNames(legacy, handle.agent)).toEqual(['bash', 'read_image', 'team_task_create', 'team_task_get', 'team_task_list', 'team_task_update', 'working_directory'])
|
||||
} finally {
|
||||
await handle.dispose()
|
||||
}
|
||||
|
||||
Loaded 100 of 1349 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user