diff --git a/.agents/notes/archived/manifest.json b/.agents/notes/archived/manifest.json index 2dbf920a69..e42398340b 100644 --- a/.agents/notes/archived/manifest.json +++ b/.agents/notes/archived/manifest.json @@ -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", diff --git a/.agents/notes/archived/simplification/2026-09-26-team-direct-inbox.i18n.yaml b/.agents/notes/archived/simplification/2026-09-26-team-direct-inbox.i18n.yaml new file mode 100644 index 0000000000..50788468df --- /dev/null +++ b/.agents/notes/archived/simplification/2026-09-26-team-direct-inbox.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/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 diff --git a/.agents/notes/implemented/simplification/2026-09-26-team-direct-inbox.md b/.agents/notes/archived/simplification/2026-09-26-team-direct-inbox.md similarity index 99% rename from .agents/notes/implemented/simplification/2026-09-26-team-direct-inbox.md rename to .agents/notes/archived/simplification/2026-09-26-team-direct-inbox.md index 58d18d0ecf..9b90d7ae30 100644 --- a/.agents/notes/implemented/simplification/2026-09-26-team-direct-inbox.md +++ b/.agents/notes/archived/simplification/2026-09-26-team-direct-inbox.md @@ -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) diff --git a/.agents/notes/implemented/simplification/2026-09-26-team-direct-inbox.zh.md b/.agents/notes/archived/simplification/2026-09-26-team-direct-inbox.zh.md similarity index 99% rename from .agents/notes/implemented/simplification/2026-09-26-team-direct-inbox.zh.md rename to .agents/notes/archived/simplification/2026-09-26-team-direct-inbox.zh.md index 6928631c8f..8838fb4a28 100644 --- a/.agents/notes/implemented/simplification/2026-09-26-team-direct-inbox.zh.md +++ b/.agents/notes/archived/simplification/2026-09-26-team-direct-inbox.zh.md @@ -1,6 +1,7 @@ # Agent Note: Team 消息使用目标 inbox Status: implemented +Archived: 2026-10-15 [English](2026-09-26-team-direct-inbox.md) | 中文 diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml index 061ddeed50..95484d284b 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md index 34d346c96a..a70ab115be 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md @@ -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--` 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--` 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` 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. diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md index 6e1e2c51b6..e1ff276504 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md @@ -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--` 写入 `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--` 写入 `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` 标签流水线,构建一个 SDK wheel 包和 5 个原生运行时 wheel 包,再由单个串行任务校验并将这 6 个文件发布到项目的 PyPI 注册表。[`python/sdk-runtime` README](../../../../python/sdk-runtime/README.zh.md)负责 Windows 目标及对 Windows arm64 的明确排除。 diff --git a/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.i18n.yaml index 260adf6610..97e7927b58 100644 --- a/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.md b/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.md index 4efb9d334e..a4cfbc91cb 100644 --- a/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.md +++ b/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.md @@ -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 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 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. diff --git a/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md b/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md index b7ed157f54..d907eaeac0 100644 --- a/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md @@ -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 sent a message:`,因此来源信息不会偏离权限。 +所有方向使用同一种持久 source。服务从确切在线发送方推导 `senderSessionId`,并以 `Agent 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 地址,而请求头系统提示词与工具顺序保持可复用。 diff --git a/.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.i18n.yaml index ddf55f9063..f8de1ed3b1 100644 --- a/.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.md b/.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.md index 16cd668f65..336151fa83 100644 --- a/.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.md +++ b/.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.md @@ -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. diff --git a/.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.zh.md b/.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.zh.md index 3d4131af01..063f0490bb 100644 --- a/.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.zh.md @@ -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 指定归属。 diff --git a/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.i18n.yaml index dc596b4fae..5494e130cf 100644 --- a/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.md b/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.md index 8e986aaa2f..ff06f52dc1 100644 --- a/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.md +++ b/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.md @@ -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. diff --git a/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.zh.md b/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.zh.md index 371ae5cc3a..2fa2f9b07f 100644 --- a/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.zh.md +++ b/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.zh.md @@ -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)继续负责应用许可。 diff --git a/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.i18n.yaml index 65205260c3..f1c602359c 100644 --- a/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.md b/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.md index 39794f02d3..fb8e92e811 100644 --- a/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.md +++ b/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.md @@ -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. diff --git a/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.zh.md b/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.zh.md index d0e333a71e..f3ebb3e6b3 100644 --- a/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.zh.md +++ b/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.zh.md @@ -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 工作流通过各自的分发组合固定身份行为;委派任务保留最终结果返回指导。 diff --git a/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.i18n.yaml new file mode 100644 index 0000000000..282acab92a --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md b/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md new file mode 100644 index 0000000000..8cab7023e0 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md @@ -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. diff --git a/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.zh.md b/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.zh.md new file mode 100644 index 0000000000..8c113d288a --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.zh.md @@ -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 返回指导。 diff --git a/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.i18n.yaml new file mode 100644 index 0000000000..ee52c30615 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.md b/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.md new file mode 100644 index 0000000000..30dee6781a --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.md @@ -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. diff --git a/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.zh.md b/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.zh.md new file mode 100644 index 0000000000..58d075e9a6 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.zh.md @@ -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 + +发现输出随整棵树增长。冷投递需要一个容量名额和可读的创建历史。中间创建者可以离线。完成通知仅投递给在线创建者,因此依赖后代结果的工作必须显式等待。列表是快照,不保证后续发送成功。外部执行和一次性执行不能接收后续消息。 diff --git a/.agents/notes/implemented/architecture/2026-10-19-team-task-board-inbox.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-19-team-task-board-inbox.i18n.yaml new file mode 100644 index 0000000000..aa751903bf --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-19-team-task-board-inbox.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/architecture/2026-10-19-team-task-board-inbox.md b/.agents/notes/implemented/architecture/2026-10-19-team-task-board-inbox.md new file mode 100644 index 0000000000..818298684c --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-19-team-task-board-inbox.md @@ -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. diff --git a/.agents/notes/implemented/architecture/2026-10-19-team-task-board-inbox.zh.md b/.agents/notes/implemented/architecture/2026-10-19-team-task-board-inbox.zh.md new file mode 100644 index 0000000000..3a88ce8d19 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-19-team-task-board-inbox.zh.md @@ -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 中存储重复通知。 + +验证区分已有收件箱输入和任务提交,隔离调用者,排除下一轮输入及状态变化,并覆盖等待清理和任务持久性。分配验证保留任意深度和历史记录无权限场景。 diff --git a/.agents/notes/implemented/architecture/2026-10-20-session-team-mode.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-20-session-team-mode.i18n.yaml new file mode 100644 index 0000000000..afbff73943 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-20-session-team-mode.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/architecture/2026-10-20-session-team-mode.md b/.agents/notes/implemented/architecture/2026-10-20-session-team-mode.md new file mode 100644 index 0000000000..2449e1e4d0 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-20-session-team-mode.md @@ -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. diff --git a/.agents/notes/implemented/architecture/2026-10-20-session-team-mode.zh.md b/.agents/notes/implemented/architecture/2026-10-20-session-team-mode.zh.md new file mode 100644 index 0000000000..5b8baec113 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-20-session-team-mode.zh.md @@ -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 迁移和插件开关移除。产品隔离检查使用另一个无关实验性插件作为负对照。 diff --git a/.agents/notes/implemented/architecture/2026-10-22-subagent-target-user-controls.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-22-subagent-target-user-controls.i18n.yaml new file mode 100644 index 0000000000..82eae5a35d --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-22-subagent-target-user-controls.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/architecture/2026-10-22-subagent-target-user-controls.md b/.agents/notes/implemented/architecture/2026-10-22-subagent-target-user-controls.md new file mode 100644 index 0000000000..064dbb1e17 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-22-subagent-target-user-controls.md @@ -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. diff --git a/.agents/notes/implemented/architecture/2026-10-22-subagent-target-user-controls.zh.md b/.agents/notes/implemented/architecture/2026-10-22-subagent-target-user-controls.zh.md new file mode 100644 index 0000000000..455a511778 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-22-subagent-target-user-controls.zh.md @@ -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 中断场景通过交付组合验证重命名后的端点和输入停放行为。 diff --git a/.agents/notes/implemented/architecture/2026-10-22-subagent-tool-owned-communication.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-22-subagent-tool-owned-communication.i18n.yaml new file mode 100644 index 0000000000..81682a6981 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-22-subagent-tool-owned-communication.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/architecture/2026-10-22-subagent-tool-owned-communication.md b/.agents/notes/implemented/architecture/2026-10-22-subagent-tool-owned-communication.md new file mode 100644 index 0000000000..2222cd5dd7 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-22-subagent-tool-owned-communication.md @@ -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. diff --git a/.agents/notes/implemented/architecture/2026-10-22-subagent-tool-owned-communication.zh.md b/.agents/notes/implemented/architecture/2026-10-22-subagent-tool-owned-communication.zh.md new file mode 100644 index 0000000000..21242507b7 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-22-subagent-tool-owned-communication.zh.md @@ -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 测试帮助函数;生产代码不再仅为暴露该帮助函数而保留适配器。 diff --git a/.agents/notes/implemented/architecture/2026-10-29-cua-dev-signing-identity.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-29-cua-dev-signing-identity.i18n.yaml new file mode 100644 index 0000000000..de5a17b10d --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-29-cua-dev-signing-identity.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/architecture/2026-10-29-cua-dev-signing-identity.md b/.agents/notes/implemented/architecture/2026-10-29-cua-dev-signing-identity.md new file mode 100644 index 0000000000..5f721474fc --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-29-cua-dev-signing-identity.md @@ -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. diff --git a/.agents/notes/implemented/architecture/2026-10-29-cua-dev-signing-identity.zh.md b/.agents/notes/implemented/architecture/2026-10-29-cua-dev-signing-identity.zh.md new file mode 100644 index 0000000000..9491738cad --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-29-cua-dev-signing-identity.zh.md @@ -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 验证复用、解锁、不同输出目录的签名要求和移除后的搜索列表恢复。 diff --git a/.agents/notes/implemented/bug-fix/2026-09-17-multiline-display-dollar-math.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-09-17-multiline-display-dollar-math.i18n.yaml deleted file mode 100644 index e17980cb32..0000000000 --- a/.agents/notes/implemented/bug-fix/2026-09-17-multiline-display-dollar-math.i18n.yaml +++ /dev/null @@ -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 diff --git a/.agents/notes/implemented/bug-fix/2026-09-17-multiline-display-dollar-math.md b/.agents/notes/implemented/bug-fix/2026-09-17-multiline-display-dollar-math.md deleted file mode 100644 index 0362895314..0000000000 --- a/.agents/notes/implemented/bug-fix/2026-09-17-multiline-display-dollar-math.md +++ /dev/null @@ -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. diff --git a/.agents/notes/implemented/bug-fix/2026-09-17-multiline-display-dollar-math.zh.md b/.agents/notes/implemented/bug-fix/2026-09-17-multiline-display-dollar-math.zh.md deleted file mode 100644 index ed0ef3f46a..0000000000 --- a/.agents/notes/implemented/bug-fix/2026-09-17-multiline-display-dollar-math.zh.md +++ /dev/null @@ -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 渲染多行写法。 diff --git a/.agents/notes/implemented/bug-fix/2026-11-01-locked-python-runtime-deploy.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-11-01-locked-python-runtime-deploy.i18n.yaml new file mode 100644 index 0000000000..618dcc06e2 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-11-01-locked-python-runtime-deploy.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/bug-fix/2026-11-01-locked-python-runtime-deploy.md b/.agents/notes/implemented/bug-fix/2026-11-01-locked-python-runtime-deploy.md new file mode 100644 index 0000000000..a1763148c8 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-11-01-locked-python-runtime-deploy.md @@ -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 `tslib@2.9.1` 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. diff --git a/.agents/notes/implemented/bug-fix/2026-11-01-locked-python-runtime-deploy.zh.md b/.agents/notes/implemented/bug-fix/2026-11-01-locked-python-runtime-deploy.zh.md new file mode 100644 index 0000000000..8b24c383e7 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-11-01-locked-python-runtime-deploy.zh.md @@ -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 部署安装 `tslib@2.9.1`,默认部署保留 `2.8.1`。Linux 可执行文件及本地构建并安装的 wheel SDK 冒烟测试通过。定向测试拒绝缺失的依赖目录和部署参数回退,保留审批状态,并覆盖两种 metadata 路径基准。跨平台打包及安装后 wheel 校验仍由 CI 负责。 + +## 影响 + +运行时版本可以追溯到工作区锁文件,不完整的直接依赖树会在可执行文件打包前失败。安装脚本产物和锁定的对等依赖选择可能与 legacy 部署不同,因此不承诺载荷逐字节相同。SSH helper 部署使用独立的闭包 manifest 和原生载荷校验,其 legacy 部署不属于本次 Python 运行时修复范围。 diff --git a/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.i18n.yaml b/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.i18n.yaml index 6566e1a57e..ef8bb48089 100644 --- a/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md b/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md index e76aae22fe..f5d178fec7 100644 --- a/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md +++ b/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md @@ -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: diff --git a/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.zh.md b/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.zh.md index ce543b4efa..44f51e1d9e 100644 --- a/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.zh.md +++ b/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.zh.md @@ -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 拥有一个持久化会话,并且至多拥有一个进程内激活: diff --git a/.agents/notes/implemented/feature/2026-08-05-agent-teams.i18n.yaml b/.agents/notes/implemented/feature/2026-08-05-agent-teams.i18n.yaml index 68ef489fe0..dd2778d632 100644 --- a/.agents/notes/implemented/feature/2026-08-05-agent-teams.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-05-agent-teams.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/feature/2026-08-05-agent-teams.md b/.agents/notes/implemented/feature/2026-08-05-agent-teams.md index b93e2716f9..2056882e73 100644 --- a/.agents/notes/implemented/feature/2026-08-05-agent-teams.md +++ b/.agents/notes/implemented/feature/2026-08-05-agent-teams.md @@ -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. diff --git a/.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md b/.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md index f38c6f68c4..3fbb92daf3 100644 --- a/.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md +++ b/.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md @@ -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 的集成边界。 diff --git a/.agents/notes/implemented/feature/2026-09-21-archive-stops-running-session-work.i18n.yaml b/.agents/notes/implemented/feature/2026-09-21-archive-stops-running-session-work.i18n.yaml index bf652c02d6..db15a4f23d 100644 --- a/.agents/notes/implemented/feature/2026-09-21-archive-stops-running-session-work.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-21-archive-stops-running-session-work.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/feature/2026-09-21-archive-stops-running-session-work.md b/.agents/notes/implemented/feature/2026-09-21-archive-stops-running-session-work.md index 4bc3bab644..fcaba90418 100644 --- a/.agents/notes/implemented/feature/2026-09-21-archive-stops-running-session-work.md +++ b/.agents/notes/implemented/feature/2026-09-21-archive-stops-running-session-work.md @@ -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 diff --git a/.agents/notes/implemented/feature/2026-09-21-archive-stops-running-session-work.zh.md b/.agents/notes/implemented/feature/2026-09-21-archive-stops-running-session-work.zh.md index c7a912fa65..c511810b1d 100644 --- a/.agents/notes/implemented/feature/2026-09-21-archive-stops-running-session-work.zh.md +++ b/.agents/notes/implemented/feature/2026-09-21-archive-stops-running-session-work.zh.md @@ -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,不会让被停止的工作继续。 ## 考虑过的替代方案 diff --git a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.i18n.yaml b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.i18n.yaml index 9e538f26c6..970eaf9557 100644 --- a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.i18n.yaml +++ b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md index 74f824fc88..aaa055f7b5 100644 --- a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md +++ b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md @@ -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 diff --git a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.zh.md b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.zh.md index 129f76c454..6135f08763 100644 --- a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.zh.md +++ b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.zh.md @@ -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` 管理。 ## 考虑过的替代方案 diff --git a/.agents/notes/implemented/simplification/2026-09-26-team-direct-inbox.i18n.yaml b/.agents/notes/implemented/simplification/2026-09-26-team-direct-inbox.i18n.yaml deleted file mode 100644 index f830391e86..0000000000 --- a/.agents/notes/implemented/simplification/2026-09-26-team-direct-inbox.i18n.yaml +++ /dev/null @@ -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 diff --git a/.agents/notes/implemented/simplification/2026-10-29-inbox-only-agent-wait.i18n.yaml b/.agents/notes/implemented/simplification/2026-10-29-inbox-only-agent-wait.i18n.yaml new file mode 100644 index 0000000000..3c03e11100 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-10-29-inbox-only-agent-wait.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/simplification/2026-10-29-inbox-only-agent-wait.md b/.agents/notes/implemented/simplification/2026-10-29-inbox-only-agent-wait.md new file mode 100644 index 0000000000..80e46f63e6 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-10-29-inbox-only-agent-wait.md @@ -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. diff --git a/.agents/notes/implemented/simplification/2026-10-29-inbox-only-agent-wait.zh.md b/.agents/notes/implemented/simplification/2026-10-29-inbox-only-agent-wait.zh.md new file mode 100644 index 0000000000..6bf2e39562 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-10-29-inbox-only-agent-wait.zh.md @@ -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。 + +## 后果 + +仅更新任务不会恢复等待中或空闲的成员。需要继续工作时,调用者必须通信。工具不返回任务内容;成员按需查询任务板。等待前和等待期间发生的任务变化效果相同:都不影响等待。 + +单元测试覆盖已有输入、调用者隔离、并行等待、取消、释放,以及不结束等待的任务提交。任务板录制场景检查过去和并发提交下的等待超时,完成通知仍作为普通收件箱输入。 diff --git a/.agents/notes/implemented/simplification/2026-10-29-team-task-only-projection.i18n.yaml b/.agents/notes/implemented/simplification/2026-10-29-team-task-only-projection.i18n.yaml new file mode 100644 index 0000000000..cf5fb34bc3 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-10-29-team-task-only-projection.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/simplification/2026-10-29-team-task-only-projection.md b/.agents/notes/implemented/simplification/2026-10-29-team-task-only-projection.md new file mode 100644 index 0000000000..57a85f4105 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-10-29-team-task-only-projection.md @@ -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. diff --git a/.agents/notes/implemented/simplification/2026-10-29-team-task-only-projection.zh.md b/.agents/notes/implemented/simplification/2026-10-29-team-task-only-projection.zh.md new file mode 100644 index 0000000000..221abe771b --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-10-29-team-task-only-projection.zh.md @@ -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 在退役生命周期与邮箱记录之后接受任务。任务和模式校验仍可见地报错。 diff --git a/.agents/skills/dsh-cua-macos-signing/SKILL.md b/.agents/skills/dsh-cua-macos-signing/SKILL.md new file mode 100644 index 0000000000..6d28147f29 --- /dev/null +++ b/.agents/skills/dsh-cua-macos-signing/SKILL.md @@ -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 ` 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: "" + keychain: "/Users//.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- ` 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. diff --git a/.agents/skills/dsh-cua-macos-signing/scripts/signing_e2e.py b/.agents/skills/dsh-cua-macos-signing/scripts/signing_e2e.py new file mode 100644 index 0000000000..71c36752d5 --- /dev/null +++ b/.agents/skills/dsh-cua-macos-signing/scripts/signing_e2e.py @@ -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() diff --git a/.agents/skills/dsh-cua-macos-signing/scripts/signing_identity.py b/.agents/skills/dsh-cua-macos-signing/scripts/signing_identity.py new file mode 100644 index 0000000000..69ab639a9d --- /dev/null +++ b/.agents/skills/dsh-cua-macos-signing/scripts/signing_identity.py @@ -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) diff --git a/.agents/skills/dsh-cua-macos-signing/scripts/test_signing_identity.py b/.agents/skills/dsh-cua-macos-signing/scripts/test_signing_identity.py new file mode 100644 index 0000000000..949926f98c --- /dev/null +++ b/.agents/skills/dsh-cua-macos-signing/scripts/test_signing_identity.py @@ -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() diff --git a/.github/AGENTS.md b/.github/AGENTS.md index bde3142e27..57830f1af0 100644 --- a/.github/AGENTS.md +++ b/.github/AGENTS.md @@ -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. diff --git a/.github/workflows/pr-check-after-approve.yml b/.github/workflows/pr-check-after-approve.yml index 1255ae9373..092b5b957d 100644 --- a/.github/workflows/pr-check-after-approve.yml +++ b/.github/workflows/pr-check-after-approve.yml @@ -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/checkout@v7.0.1 - 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/setup-node@v7.0.0 - 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 diff --git a/AGENTS.md b/AGENTS.md index 69dbd7d69e..db164319da 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,6 +37,7 @@ packages/ @deepseek-ai/dsh- workspaces at packages/// compaction/ context compaction context/ request context subagent/ delegated agents + team/ shared task coordination jobs/ background jobs bundle/ profile bundles workflow/ workflow execution diff --git a/apps/cli/composition.md b/apps/cli/composition.md index 21150774cc..96b711a333 100644 --- a/apps/cli/composition.md +++ b/apps/cli/composition.md @@ -140,6 +140,10 @@ flowchart LR cfg --> plugin_dsh_base_command_compact plugin_dsh_base_subagent["subagent
@deepseek-ai/dsh-subagent"] cfg --> plugin_dsh_base_subagent + plugin_dsh_base_agent_team["agent-team
@deepseek-ai/dsh-agent-team"] + cfg --> plugin_dsh_base_agent_team + plugin_dsh_base_tool_agent_team["tool-agent-team
@deepseek-ai/dsh-tool-agent-team"] + cfg --> plugin_dsh_base_tool_agent_team plugin_dsh_base_subagent_spawn_in_process["subagent-spawn-in-process
@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
@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` | diff --git a/apps/cli/package.json b/apps/cli/package.json index a77b89b9f2..3df78de1fb 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -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:*", diff --git a/apps/cli/tests/agent-team-headless.e2e.ts b/apps/cli/tests/agent-team-headless.e2e.ts index dd4e456252..e38a33777d 100644 --- a/apps/cli/tests/agent-team-headless.e2e.ts +++ b/apps/cli/tests/agent-team-headless.e2e.ts @@ -17,7 +17,7 @@ function records(content: string): Record[] { } 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 + 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' diff --git a/apps/cli/tests/optional-bundle-transitions.e2e.ts b/apps/cli/tests/optional-bundle-transitions.e2e.ts index b5a5eecbaf..8db33a3a87 100644 --- a/apps/cli/tests/optional-bundle-transitions.e2e.ts +++ b/apps/cli/tests/optional-bundle-transitions.e2e.ts @@ -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) }) diff --git a/apps/cli/tests/profiles/acp/tests/goal-expected/goal-round-driver/session.expected.jsonl b/apps/cli/tests/profiles/acp/tests/goal-expected/goal-round-driver/session.expected.jsonl index 19495490be..8467970106 100644 --- a/apps/cli/tests/profiles/acp/tests/goal-expected/goal-round-driver/session.expected.jsonl +++ b/apps/cli/tests/profiles/acp/tests/goal-expected/goal-round-driver/session.expected.jsonl @@ -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: \"\", 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: \"\", 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"}}} diff --git a/apps/cli/tests/profiles/acp/tests/goal-expected/goal-wrapup/session.expected.jsonl b/apps/cli/tests/profiles/acp/tests/goal-expected/goal-wrapup/session.expected.jsonl index 7e3a02fce9..a4ac54b8c1 100644 --- a/apps/cli/tests/profiles/acp/tests/goal-expected/goal-wrapup/session.expected.jsonl +++ b/apps/cli/tests/profiles/acp/tests/goal-expected/goal-wrapup/session.expected.jsonl @@ -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: \"\", 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: \"\", 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"}}} diff --git a/apps/cli/tests/profiles/acp/tests/image-offload.expected.e2e.ts b/apps/cli/tests/profiles/acp/tests/image-offload.expected.e2e.ts index 259ba7341e..63715e4638 100644 --- a/apps/cli/tests/profiles/acp/tests/image-offload.expected.e2e.ts +++ b/apps/cli/tests/profiles/acp/tests/image-offload.expected.e2e.ts @@ -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: "", 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> }> @@ -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(resolve => server.close(() => { resolve() })) diff --git a/apps/cli/tests/profiles/fixtures/official-installer-observer.mjs b/apps/cli/tests/profiles/fixtures/official-installer-observer.mjs index 5ca8db1ac6..4acea7354d 100644 --- a/apps/cli/tests/profiles/fixtures/official-installer-observer.mjs +++ b/apps/cli/tests/profiles/fixtures/official-installer-observer.mjs @@ -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) diff --git a/apps/cli/tests/profiles/headless/team-snapshot.patch.yml b/apps/cli/tests/profiles/headless/team-snapshot.patch.yml index dec80a9fe9..7a1d4c8a24 100644 --- a/apps/cli/tests/profiles/headless/team-snapshot.patch.yml +++ b/apps/cli/tests/profiles/headless/team-snapshot.patch.yml @@ -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' diff --git a/apps/cli/tests/profiles/headless/tests/expected/goal-tools/stream-json.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/goal-tools/stream-json.expected.jsonl index c54f84df0b..7779eb2181 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/goal-tools/stream-json.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/goal-tools/stream-json.expected.jsonl @@ -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: \"\", 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: \"\", 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"}}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/headless-profile/session.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/headless-profile/session.expected.jsonl index d45d98dfd0..d4385b311d 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/headless-profile/session.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/headless-profile/session.expected.jsonl @@ -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: \"\", 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: \"\", 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"}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/invalid-credential/stream-json.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/invalid-credential/stream-json.expected.jsonl index e747f996ad..6cebc92a87 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/invalid-credential/stream-json.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/invalid-credential/stream-json.expected.jsonl @@ -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: \"\", 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: \"\", 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"}}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/missing-credential/stream-json.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/missing-credential/stream-json.expected.jsonl index bc7c728199..8cc16706c1 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/missing-credential/stream-json.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/missing-credential/stream-json.expected.jsonl @@ -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: \"\", 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: \"\", 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"}}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/provider-retry/stream-json.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/provider-retry/stream-json.expected.jsonl index 5792ae3774..8488837003 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/provider-retry/stream-json.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/provider-retry/stream-json.expected.jsonl @@ -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: \"\", 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: \"\", 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"}}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/semantic-checkpoint/session.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/semantic-checkpoint/session.expected.jsonl index 11ae2ec827..c14e345b7f 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/semantic-checkpoint/session.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/semantic-checkpoint/session.expected.jsonl @@ -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: \"\", 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: \"\", 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"}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-diagnostic/parent.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/subagent-diagnostic/parent.expected.jsonl index 9da1c6379a..57f93111e7 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-diagnostic/parent.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-diagnostic/parent.expected.jsonl @@ -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: \"\", 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: \"\", 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"} diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/child.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/child.expected.jsonl index b0372d96c6..27712bcd00 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/child.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/child.expected.jsonl @@ -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: \"\" }). 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: \"\" }). 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: \"\", 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: \"\", 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"}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/parent.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/parent.expected.jsonl index b9bee477bd..f8796478f5 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/parent.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/parent.expected.jsonl @@ -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: \"\", 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: \"\", 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"}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/child.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/child.expected.jsonl index c07532bd7d..2305a8a7c2 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/child.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/child.expected.jsonl @@ -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: \"\" }). 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: \"\" }). 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: \"\", 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: \"\", 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"}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl index 441cab1aa2..f05ae06cef 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl @@ -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: \"\", 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: \"\", 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"}}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/offline-edit/session.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/offline-edit/session.expected.jsonl index cbb82cb603..56b127865a 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/offline-edit/session.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/offline-edit/session.expected.jsonl @@ -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":"\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"}],"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: \"\", 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: \"\", 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"}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/precedence-change/session.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/precedence-change/session.expected.jsonl index 3bd9c55f95..dfe1d171a9 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/precedence-change/session.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/precedence-change/session.expected.jsonl @@ -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":"\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"}],"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: \"\", 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: \"\", 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"}}} diff --git a/apps/cli/tests/profiles/headless/tests/fixtures/team-llm.mjs b/apps/cli/tests/profiles/headless/tests/fixtures/team-llm.mjs index 9c1bedd00e..62751cd93f 100644 --- a/apps/cli/tests/profiles/headless/tests/fixtures/team-llm.mjs +++ b/apps/cli/tests/profiles/headless/tests/fixtures/team-llm.mjs @@ -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()) } diff --git a/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts b/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts index a2ae0c9f19..9f53fb980a 100644 --- a/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts +++ b/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts @@ -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, '').replaceAll(leadId, '') + } + 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: "", 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: "", message: "..." }).", + "You are a subagent delegated by agent "". + Your agent id is "". + The root agent of your collaboration tree is "". + + To message other agents, use send_message({ agent_id: "", message: "..." }). + Use list_agents({}) to find your collaborator agents and their ids.", + "You are a subagent delegated by agent "". + Your agent id is "". + The root agent of your collaboration tree is "". + + To message other agents, use send_message({ 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", } diff --git a/apps/cli/tests/profiles/headless/tests/subagent-diagnostic.expected.e2e.ts b/apps/cli/tests/profiles/headless/tests/subagent-diagnostic.expected.e2e.ts index 941d95c31d..3aff474038 100644 --- a/apps/cli/tests/profiles/headless/tests/subagent-diagnostic.expected.e2e.ts +++ b/apps/cli/tests/profiles/headless/tests/subagent-diagnostic.expected.e2e.ts @@ -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 { 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 } diff --git a/apps/cli/tests/profiles/web/tests/default-web-process.ts b/apps/cli/tests/profiles/web/tests/default-web-process.ts index 537ac77702..155f8d6670 100644 --- a/apps/cli/tests/profiles/web/tests/default-web-process.ts +++ b/apps/cli/tests/profiles/web/tests/default-web-process.ts @@ -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'), diff --git a/apps/cli/tests/profiles/web/tests/web-default-isolation.expected.e2e.ts b/apps/cli/tests/profiles/web/tests/web-default-isolation.expected.e2e.ts index 7563a323aa..687afcb6d8 100644 --- a/apps/cli/tests/profiles/web/tests/web-default-isolation.expected.e2e.ts +++ b/apps/cli/tests/profiles/web/tests/web-default-isolation.expected.e2e.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/'), ])) }) }) diff --git a/apps/cli/tests/web-agent-presets.e2e.ts b/apps/cli/tests/web-agent-presets.e2e.ts index 3907997d6a..792187acae 100644 --- a/apps/cli/tests/web-agent-presets.e2e.ts +++ b/apps/cli/tests/web-agent-presets.e2e.ts @@ -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() } diff --git a/apps/web/package.json b/apps/web/package.json index 51ecc68787..b011aa916d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -43,7 +43,7 @@ "@deepseek-ai/dsh-client-web": "workspace:*", "@deepseek-ai/dsh-cmdline": "workspace:*", "@deepseek-ai/dsh-pwsh-local": "workspace:*", - "@deepseek-ai/dsh-experimental-agent-team": "workspace:*", + "@deepseek-ai/dsh-agent-team": "workspace:*", "@deepseek-ai/dsh-experimental-webworker-packer": "workspace:*", "@deepseek-ai/dsh-experimental-webworker-runtime": "workspace:*", "@deepseek-ai/dsh-remote-mock": "workspace:*", diff --git a/apps/web/tests/access-confirmation.e2e.ts b/apps/web/tests/access-confirmation.e2e.ts index f97978d346..bb9b3d2065 100644 --- a/apps/web/tests/access-confirmation.e2e.ts +++ b/apps/web/tests/access-confirmation.e2e.ts @@ -14,7 +14,7 @@ import { launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold, } from './scaffold.ts' import { - ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, saveFailureShot, writeComposerDraft, + ZH_BROWSER_LOCALE, assertWarningDialogLayout, connectFreshWorkspaceZh, saveFailureShot, writeComposerDraft, } from './support.ts' import { AUTO_REVIEW_FIXTURE, captureAutoReviewState } from './auto-review-fixture.ts' @@ -154,6 +154,7 @@ describe('web e2e: experimental Auto and Full access confirmation', () => { await page.getByRole('menuitem', { name: '完全权限' }).click() const dialog = page.getByRole('dialog', { name: '确认启用完全权限?' }) await dialog.waitFor({ timeout: 10_000 }) + await assertWarningDialogLayout(dialog) const enable = dialog.getByRole('button', { name: '启用完全权限' }) expect(await enable.isDisabled()).toBe(true) @@ -163,7 +164,18 @@ describe('web e2e: experimental Auto and Full access confirmation', () => { const snapshot = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd) await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE) - await dialog.getByRole('checkbox', { name: '我已了解风险,并愿意继续' }).check() + const acknowledgement = dialog.getByRole('checkbox', { name: '我已了解风险,并愿意继续' }) + expect(await acknowledgement.evaluate((input) => { + const label = input.closest('label')! + const textStyle = getComputedStyle(label) + const inputStyle = getComputedStyle(input) + return { + focused: document.activeElement === input, + fontSize: textStyle.fontSize, lineHeight: textStyle.lineHeight, weight: textStyle.fontWeight, + gap: textStyle.gap, width: inputStyle.width, height: inputStyle.height, + } + })).toEqual({ focused: true, fontSize: '14px', lineHeight: '20px', weight: '400', gap: '6px', width: '14px', height: '14px' }) + await dialog.getByText('我已了解风险,并愿意继续', { exact: true }).click() expect(await enable.isEnabled()).toBe(true) await enable.click() await expect.poll(() => access.getAttribute('aria-label'), { timeout: 10_000 }) diff --git a/apps/web/tests/agent-team-panel.e2e.ts b/apps/web/tests/agent-team-panel.e2e.ts index 19d7924381..6da971cf03 100644 --- a/apps/web/tests/agent-team-panel.e2e.ts +++ b/apps/web/tests/agent-team-panel.e2e.ts @@ -1,79 +1,112 @@ -// Keyless assembled-browser coverage for the opt-in Agent Teams bundle +// Keyless assembled-browser coverage for per-Session agent teamwork // over the real Host projection frame flow. import { fileURLToPath } from 'node:url' import { join } from 'node:path' -import { readFileSync } from 'node:fs' import type { Browser, Page } from 'playwright' import { chromium } from 'playwright' import { afterAll, beforeAll, describe, expect, it, onTestFailed, onTestFinished, vi } from 'vitest' -import * as yaml from 'js-yaml' -import { entryListSchema } from '@deepseek-ai/cordis-plugin-include' -import type {} from '@deepseek-ai/dsh-experimental-agent-team' +import type {} from '@deepseek-ai/dsh-agent-team' +import { SessionId } from '@deepseek-ai/dsh-session' +import { CommandId } from '@deepseek-ai/dsh-commands/brand' import { createMessage, createUserMessage } from '@deepseek-ai/dsh-llm' import { - assertFixtureInventory, captureStableAria, compareOrRefreshGolden, + acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold, } from './scaffold.ts' -import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts' +import { assertWarningDialogLayout, connectFreshWorkspace, connectFreshWorkspaceZh, newEnglishPage, saveFailureShot, writeComposerDraft } from './support.ts' const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/agent-team-panel', import.meta.url)) const PANEL_EXPECTED = join(SNAPSHOT_DIR, 'task.expected.md') const RESIZE_EXPECTED = join(SNAPSHOT_DIR, 'resize.expected.md') -const OVERLAY = fileURLToPath(new URL('./agent-team-panel.overlay.yml', import.meta.url)) -const TEAM_PATCH = fileURLToPath(new URL('../../../packages/experimental/agent-team-profile/cordis.patch.yml', import.meta.url)) -const INSTALL_ANCHORS = [ - fileURLToPath(new URL('../../../packages/experimental/agent-team-profile/package.json', import.meta.url)), -] const MODE = webSnapshotMode() +const RESEARCHER_DESCRIPTION = 'Researcher: investigate the construction and verify every bound before reporting the result. '.repeat(8).trim() +const RECEIPT_LOCALES = [ + { + language: 'English', code: 'en', browserLocale: 'en-US', connect: connectFreshWorkspace, + modelButton: /^Select model/u, toggle: 'Enable agent teamwork', title: 'Agent teamwork', + enabled: 'Enabled', disabled: 'Disabled', + suggestions: 'Trigger suggestions', commandLabel: 'Team', + failure: 'Failed: Use /team on or /team off.', + }, + { + language: 'Chinese', code: 'zh', browserLocale: 'zh-CN', connect: connectFreshWorkspaceZh, + modelButton: /^选择模型/u, toggle: '启用智能体团队协作', title: '智能体团队协作', + enabled: '已开启', disabled: '已停用', + suggestions: '触发候选建议', commandLabel: '智能体团队协作', + failure: '执行失败:Use /team on or /team off.', + }, +] as const -function profileEntries(path: string): unknown[] { - const parsed = yaml.load(readFileSync(path, 'utf8'), { schema: entryListSchema }) - if (!Array.isArray(parsed)) throw new Error(`profile layer at ${path} must be a list`) - return parsed +type ReceiptLocale = typeof RECEIPT_LOCALES[number] + +async function seedTeamConversation(page: Page, scaffold: WebScaffold): Promise { + const agent = scaffold.ctx.agents.list()[0] + if (agent === undefined) throw new Error('connected Team workspace did not create an Agent') + agent.session.append('turn/start', { turn: 1 }) + agent.session.append('user/message', createUserMessage({ + content: [{ type: 'text', text: 'Open the task board.' }], + source: { kind: 'user' }, + }), { surfaceOp: 'append' }) + agent.session.append('step/start', { turn: 1, step: 1 }) + agent.session.append('assistant/message', { + stream: [], + turn: 1, + step: 1, + message: createMessage({ + role: 'assistant', + content: [{ type: 'text', text: 'Ready.' }], + source: { kind: 'model', provider: 'fixture', model: 'fixture' }, + }), + }, { surfaceOp: 'append' }) + agent.session.append('step/end', { turn: 1, step: 1 }) + agent.session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) + await scaffold.ctx.sessions.flush(agent.session) + await page.getByText('Ready.').waitFor({ timeout: 10_000 }) } -describe('Agent Teams panel overlay', () => { - it('matches the shipped Agent Teams bundle', () => { - expect(profileEntries(OVERLAY)).toEqual(profileEntries(TEAM_PATCH)) - }) -}) +async function withTeamReceiptPage( + locale: ReceiptLocale, + run: (page: Page, scaffold: WebScaffold, tripwire: ReturnType) => Promise, +): Promise { + const scaffold = await launchWebScaffold() + let browser: Browser | undefined + try { + browser = await chromium.launch() + const page = await browser.newPage({ + viewport: { width: 1680, height: 1000 }, locale: locale.browserLocale, timezoneId: 'Asia/Shanghai', + }) + const tripwire = watchConsole(page) + await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) + await locale.connect(page, scaffold.workspaceCwd) + try { + await seedTeamConversation(page, scaffold) + await run(page, scaffold, tripwire) + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + } catch (error) { + await saveFailureShot(page, `web-e2e-team-command-${locale.code}`) + throw error + } + } finally { + try { await browser?.close() } finally { await scaffold.close() } + } +} -describe('web e2e: Agent Teams panel', () => { +describe('web e2e: agent teamwork panel', () => { let scaffold: WebScaffold let browser: Browser let page: Page let tripwire: ReturnType beforeAll(async () => { - scaffold = await launchWebScaffold({ extraOverlayPath: OVERLAY, extraInstallAnchors: INSTALL_ANCHORS }) + scaffold = await launchWebScaffold() browser = await chromium.launch() page = await newEnglishPage(browser) tripwire = watchConsole(page) await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) await connectFreshWorkspace(page, scaffold.workspaceCwd) - const agent = scaffold.ctx.agents.list()[0] - if (agent === undefined) throw new Error('connected Team workspace did not create an Agent') - agent.session.append('turn/start', { turn: 1 }) - agent.session.append('user/message', createUserMessage({ - content: [{ type: 'text', text: 'Open the Agent Team controls.' }], - source: { kind: 'user' }, - }), { surfaceOp: 'append' }) - agent.session.append('step/start', { turn: 1, step: 1 }) - agent.session.append('assistant/message', { - stream: [], - turn: 1, - step: 1, - message: createMessage({ - role: 'assistant', - content: [{ type: 'text', text: 'Ready.' }], - source: { kind: 'model', provider: 'fixture', model: 'fixture' }, - }), - }, { surfaceOp: 'append' }) - agent.session.append('step/end', { turn: 1, step: 1 }) - agent.session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) - await scaffold.ctx.sessions.flush(agent.session) - await page.getByText('Ready.').waitFor({ timeout: 10_000 }) + await seedTeamConversation(page, scaffold) }, 120_000) afterAll(async () => { @@ -81,35 +114,318 @@ describe('web e2e: Agent Teams panel', () => { await scaffold?.close() }) - it('displays agent-owned task changes through a read-only board without a refresh action', async () => { + it('explains agent teamwork on hover and focus without changing the Session selection', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-team-info')) + await page.getByRole('button', { name: /^Select model/u }).click() + const info = page.getByRole('button', { name: 'About agent teamwork', exact: true }) + const toggle = page.getByRole('switch', { name: 'Enable agent teamwork', exact: true }) + const tooltip = page.getByRole('tooltip') + await info.hover() + await tooltip.waitFor({ state: 'visible' }) + await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'info.expected.md'), await captureStableAria(page, '[role="tooltip"]', scaffold.workspaceCwd), MODE) + expect(await toggle.getAttribute('aria-checked')).toBe('false') + await page.getByRole('button', { name: /^Select model/u }).hover() + await tooltip.waitFor({ state: 'hidden' }) + await page.keyboard.press('Shift') + await info.focus() + await tooltip.waitFor({ state: 'visible' }) + await page.keyboard.press('Escape') + await tooltip.waitFor({ state: 'hidden' }) + expect(await toggle.isVisible()).toBe(true) + await info.click() + await tooltip.waitFor({ state: 'visible' }) + expect(await info.getAttribute('aria-describedby')).toBe(await tooltip.getAttribute('id')) + expect(await toggle.getAttribute('aria-checked')).toBe('false') + await page.keyboard.press('Escape') + await tooltip.waitFor({ state: 'hidden' }) + await page.keyboard.press('Escape') + expect(tripwire.pageErrors).toEqual([]) + }) + + it('toggles from the row label and empty space while keeping the information button independent', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-team-row')) + await page.getByRole('button', { name: /^Select model/u }).click() + const toggle = page.getByRole('switch', { name: 'Enable agent teamwork', exact: true }) + const info = page.getByRole('button', { name: 'About agent teamwork', exact: true }) + const label = page.getByText('Enable agent teamwork', { exact: true }) + const labelBounds = await label.boundingBox() + if (labelBounds === null) throw new Error('Team label is not visible') + const labelPoint = { x: labelBounds.x + labelBounds.width / 2, y: labelBounds.y + labelBounds.height / 2 } + await page.mouse.move(labelPoint.x, labelPoint.y) + expect(await toggle.evaluate((element) => { + const row = element.parentElement! + return getComputedStyle(row).backgroundColor !== 'rgba(0, 0, 0, 0)' + })).toBe(true) + await page.mouse.click(labelPoint.x, labelPoint.y) + await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('true') + await info.click() + await page.getByRole('tooltip').waitFor({ state: 'visible' }) + expect(await toggle.getAttribute('aria-checked')).toBe('true') + await page.keyboard.press('Escape') + const gap = await toggle.evaluate((element) => { + const control = element.getBoundingClientRect() + const help = element.parentElement!.querySelector('button:not([role="switch"])')!.getBoundingClientRect() + return { x: (help.right + control.left) / 2, y: control.top + control.height / 2 } + }) + await page.mouse.click(gap.x, gap.y) + await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('false') + expect(await page.getByRole('tooltip').count()).toBe(0) + await page.keyboard.press('Escape') + expect(tripwire.pageErrors).toEqual([]) + }) + + it('defaults off and saves a Session selection in the model menu', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-team-mode')) + expect(await page.locator('[data-subagent-action]').count()).toBe(0) + await page.getByRole('button', { name: /^Select model/u }).click() + const toggle = page.getByRole('switch', { name: 'Enable agent teamwork', exact: true }) + await toggle.waitFor() + expect(await toggle.getAttribute('aria-checked')).toBe('false') + await toggle.focus() + await page.keyboard.press('ArrowUp') + expect(await toggle.evaluate(element => element === document.activeElement)).toBe(false) + await page.keyboard.press('ArrowDown') + expect(await toggle.evaluate(element => element === document.activeElement)).toBe(true) + await page.keyboard.press('Space') + await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('true') + await page.keyboard.press('Escape') + expect(await page.locator('[data-subagent-action]').count()).toBe(0) + const agent = scaffold.ctx.agents.list()[0]! + expect(scaffold.ctx.commands.list(agent).some(command => command.name === 'team')).toBe(false) + expect(agent.session.ownEvents().filter(event => event.type === 'command/run' || event.type === 'command/done')).toEqual([]) + const other = await scaffold.ctx.agents.create({ sessionId: SessionId('other-team-mode'), agentOptions: agent.options }) + expect(scaffold.ctx.agentTeams.isEnabled(other.agent)).toBe(false) + await other.dispose() + const beforeReload = tripwire.warnings.length + await page.reload({ waitUntil: 'load' }) + await page.getByRole('button', { name: /^Select model/u }).click() + expect(await page.locator('[data-subagent-action]').count()).toBe(0) + expect(await toggle.getAttribute('aria-checked')).toBe('true') + await page.keyboard.press('Escape') + acknowledgeReloadConnectionLoss(tripwire, beforeReload) + }, 60_000) + + it('toggles from the model menu and keeps text editing usable after refocusing', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-team-command-input')) + const input = page.locator('[data-composer-input]').first() + const agent = scaffold.ctx.agents.list()[0]! + for (const enabled of [false, true]) { + await page.getByRole('button', { name: /^Select model/u }).click() + const toggle = page.getByRole('switch', { name: 'Enable agent teamwork', exact: true }) + await toggle.click() + await expect.poll(() => toggle.getAttribute('aria-checked')).toBe(String(enabled)) + await page.keyboard.press('Escape') + await expect.poll(() => scaffold.ctx.agentTeams.isEnabled(agent)).toBe(enabled) + await expect.poll(() => input.textContent()).toBe('') + await page.getByRole('button', { name: 'Account menu', exact: true }).focus() + await input.click() + await page.keyboard.insertText('asdas') + await expect.poll(() => input.textContent()).toBe('asdas') + expect(await input.evaluate((element) => { + const selection = window.getSelection() + return document.activeElement === element && selection?.isCollapsed === true + && selection.anchorNode?.textContent === 'asdas' && selection.anchorOffset === 5 + })).toBe(true) + await input.press('Backspace') + await expect.poll(() => input.textContent()).toBe('asda') + await writeComposerDraft(page, input, '') + } + expect(tripwire.pageErrors).toEqual([]) + }) + + it('shows tasks without children or a divider and hides the entry after the last task is deleted', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-team-task-only')) + const agent = scaffold.ctx.agents.list()[0]! + expect(await page.locator('[data-subagent-action]').count()).toBe(0) + const task = await scaffold.ctx.agentTeams.createTask(agent, { + subject: 'Plan before delegation', description: 'Prepare shared work before creating subagents', + }) + await page.getByRole('button', { name: '0 subagents', exact: true }).click() + const board = page.locator('[data-team-panel]') + await board.getByText(task.subject, { exact: true }).waitFor() + const panel = page.locator('[data-subagent-panel]') + expect(await panel.getByRole('treeitem').count()).toBe(0) + await panel.getByText('No subagents', { exact: true }).waitFor() + expect(await panel.locator('[data-subagent-section-header]').textContent()).toBe('Subagents') + expect(await panel.locator('[data-subagent-section-header] button, h3 button').count()).toBe(0) + expect(await panel.evaluate(element => element.getBoundingClientRect().width)).toBe(360) + expect(await board.locator('[data-team-task-details]').count()).toBe(0) + expect(await board.getByRole('button', { expanded: false }).count()).toBe(1) + expect(await board.evaluate((element) => { + const style = getComputedStyle(element) + return { border: style.borderTopWidth, margin: style.marginTop } + })).toEqual({ border: '0px', margin: '0px' }) + await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'task-only.expected.md'), + await captureStableAria(page, '[data-subagent-panel]', scaffold.workspaceCwd), MODE) + await scaffold.ctx.agentTeams.updateTask(agent, { taskId: task.id, expectedRevision: task.revision, action: 'delete' }) + await page.locator('[data-subagent-action]').waitFor({ state: 'hidden' }) + await page.locator('[data-subagent-panel]').waitFor({ state: 'hidden' }) + expect(tripwire.pageErrors).toEqual([]) + }) + + it('opens the member tree and live read-only tasks in one panel', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-team-panel')) const projectionReads = vi.spyOn(scaffold.ctx.sessionController, 'projections') onTestFinished(() => { projectionReads.mockRestore() }) - await page.locator('[data-team-action]').getByRole('button', { name: 'Agent team', exact: true }).click() - const action = page.getByRole('dialog', { name: 'Agent team', exact: true }) - await action.getByText('No shared tasks yet. Create them through the conversation.').waitFor() - await action.getByText('lead').waitFor() + const agent = scaffold.ctx.agents.list()[0]! + const memberId = SessionId('team-panel-researcher') + agent.session.append('subagent/catalog', { + version: 2, childId: memberId, childCreatedAt: Date.now(), mode: 'external', + label: RESEARCHER_DESCRIPTION, + }) + const countAction = page.locator('[data-subagent-action]') + await countAction.waitFor() + await page.mouse.move(2, 2) + expect(await countAction.evaluate((element) => { + const header = element.closest('[data-slot="conversation.session.header.actions"]')!.parentElement! + const preset = header.querySelector('[data-icon="dsw-icon-agent-preset-outline-regular"]')! + const users = element.querySelector('[data-icon="dsw-icon-users-outline-regular"]')! + return { + display: getComputedStyle(header).display, gap: getComputedStyle(header).gap, + presetOpacity: getComputedStyle(preset).opacity, + presetParentOpacity: getComputedStyle(preset.parentElement!).opacity, + sameColor: getComputedStyle(preset).color === getComputedStyle(users).color, + } + })).toEqual({ display: 'flex', gap: '10px', presetOpacity: '1', presetParentOpacity: '1', sameColor: true }) + const countTrigger = countAction.getByRole('button', { name: /subagents?$/u }) + await countTrigger.click() + await countTrigger.click() + await page.locator('[data-subagent-panel]').waitFor({ state: 'detached' }) + expect(await countTrigger.evaluate(element => element === document.activeElement)).toBe(true) + await countTrigger.click() + const action = page.locator('[data-subagent-panel]') + await action.getByRole('treeitem', { name: /Researcher/u }).waitFor() + expect(await action.locator('[data-team-panel], [data-subagent-section-header]').count()).toBe(0) + expect(await action.getByText('No shared tasks yet. Create them through the conversation.').count()).toBe(0) + expect(await action.evaluate(element => element.getBoundingClientRect().width)).toBe(360) + expect(await action.locator('[data-subagent-section-header] button, h3 button').count()).toBe(0) + expect(await action.getByRole('heading', { name: 'Members', exact: true }).count()).toBe(0) expect(projectionReads).not.toHaveBeenCalled() expect(await action.getByRole('button', { name: 'New task' }).count()).toBe(0) expect(await action.getByRole('button', { name: /Refresh/u }).count()).toBe(0) - // Host-side Team mutations reach the open panel through projection frames only. - const agent = scaffold.ctx.agents.list()[0]! + await action.getByRole('treeitem', { name: /Researcher/u }).waitFor() + expect(await page.locator('[data-subagent-panel]').count()).toBe(1) + expect(await page.getByRole('button', { name: 'Task board', exact: true }).count()).toBe(0) const task = await scaffold.ctx.agentTeams.createTask(agent, { - subject: 'Agent task', description: 'Created by the Team Lead', writeScopes: ['src/web'], + subject: 'Agent task', description: 'Created by the root agent', writeScopes: ['src/web'], }) - await action.getByText('Agent task', { exact: true }).waitFor() - await action.getByText('Owner: Unowned', { exact: true }).waitFor() + const row = action.locator(`[data-team-task="${task.id}"]`) + await row.getByRole('button', { name: 'Pending Agent task', exact: true, expanded: false }).waitFor() + expect(await action.locator('[data-subagent-section-header]').textContent()).toBe('Subagents1 external') + expect(await action.locator('[data-subagent-section-header] button, h3 button').count()).toBe(0) + expect(await action.locator('[data-team-panel]').evaluate((element) => { + const style = getComputedStyle(element) + return { border: style.borderTopWidth, margin: style.marginTop } + })).toEqual({ border: '0px', margin: '4px' }) + expect(await action.evaluate((element) => { + const tree = element.querySelector('[role="tree"]')! + const board = element.querySelector('[data-team-panel]')! + return Boolean(tree.compareDocumentPosition(board) & Node.DOCUMENT_POSITION_FOLLOWING) + })).toBe(true) + expect(await row.evaluate(element => ({ tag: element.tagName, parent: element.parentElement?.tagName }))) + .toEqual({ tag: 'LI', parent: 'UL' }) + expect(await action.locator('[data-team-task-details], article').count()).toBe(0) + expect(await action.evaluate(element => element.getBoundingClientRect().width)).toBe(360) + await action.getByRole('heading', { name: 'Shared tasks 1 pending', exact: true }).waitFor() + expect(await action.evaluate(element => getComputedStyle(element).padding)).toBe('6px') + expect(await action.locator('[data-subagent-section-header]').evaluate((element) => { + const style = getComputedStyle(element) + return { padding: style.padding, gap: style.gap } + })).toEqual({ padding: '4px 7px 8px', gap: '7px' }) + expect(await action.locator('[data-team-panel]').getByRole('heading') + .evaluate(element => getComputedStyle(element).columnGap)).toBe('7px') + for (const [title, progress] of [ + [action.locator('[data-subagent-section-header]').getByRole('heading'), action.getByText('1 external', { exact: true })], + [action.locator('[data-team-panel]').getByText('Shared tasks', { exact: true }), action.getByText('1 pending', { exact: true })], + ] as const) { + expect(await progress.evaluate(element => getComputedStyle(element).marginInlineStart)).toBe('4px') + const titleRight = await title.evaluate(element => element.getBoundingClientRect().right) + const progressLeft = await progress.evaluate(element => element.getBoundingClientRect().left) + expect(Math.abs(progressLeft - titleRight - 11)).toBeLessThanOrEqual(1) + } + const member = action.getByRole('treeitem', { name: /Researcher/u }) + const memberTitle = member.getByText(RESEARCHER_DESCRIPTION, { exact: true }) + const taskTitle = row.getByText('Agent task', { exact: true }) + const titleLefts = await Promise.all([ + memberTitle, + action.locator('[data-subagent-section-header]').getByRole('heading'), + action.locator('[data-team-panel]').getByText('Shared tasks', { exact: true }), + taskTitle, + ].map(locator => locator.evaluate(element => element.getBoundingClientRect().left))) + expect(Math.max(...titleLefts) - Math.min(...titleLefts)).toBeLessThanOrEqual(1) + const primaryStyles = await Promise.all([memberTitle, taskTitle].map(locator => locator.evaluate((element) => { + const style = getComputedStyle(element) + return { fontSize: style.fontSize, color: style.color } + }))) + expect(primaryStyles.map(style => style.fontSize)).toEqual(['13px', '13px']) + expect(primaryStyles[1]).toEqual(primaryStyles[0]) + expect(await member.getByText('team-panel-researcher', { exact: true }) + .evaluate(element => getComputedStyle(element).fontSize)).toBe('12px') + const memberHover = member.locator(':scope > div') + for (const target of [member, memberHover]) { + expect(await target.evaluate((element) => { + const style = getComputedStyle(element) + return [style.paddingTop, style.paddingBottom] + })).toEqual(['4px', '4px']) + } + expect(await member.evaluate(element => getComputedStyle(element).minHeight)).toBe('40px') + expect(await member.evaluate((element) => { + const style = getComputedStyle(element) + return { left: style.paddingLeft, right: style.paddingRight, gap: style.gap } + })).toEqual({ left: '9px', right: '6px', gap: '7px' }) + expect(await memberHover.evaluate((element) => { + const style = getComputedStyle(element) + return { left: style.paddingLeft, right: style.paddingRight, gap: style.gap, margin: style.margin } + })).toEqual({ left: '6px', right: '6px', gap: '7px', margin: '-4px -6px' }) + const taskHover = row.getByRole('button') + expect(await taskHover.evaluate((element) => { + const style = getComputedStyle(element) + return [style.paddingTop, style.paddingBottom] + })).toEqual(['7px', '7px']) + expect(await taskHover.evaluate(element => element.getBoundingClientRect().height)).toBe(31) + expect(await taskHover.evaluate((element) => { + const style = getComputedStyle(element) + return { left: style.paddingLeft, right: style.paddingRight, gap: style.gap, margin: style.margin } + })).toEqual({ left: '6px', right: '6px', gap: '7px', margin: '0px -6px' }) + expect(await taskHover.evaluate(element => element.getBoundingClientRect().width + - element.parentElement!.getBoundingClientRect().width)).toBe(12) + const hoverStyles = [] + for (const target of [memberHover, taskHover]) { + await target.hover() + hoverStyles.push(await target.evaluate((element) => { + const bounds = element.getBoundingClientRect() + const style = getComputedStyle(element) + return { left: bounds.left, right: bounds.right, radius: style.borderRadius, background: style.backgroundColor } + })) + } + expect(Math.abs(hoverStyles[0]!.left - hoverStyles[1]!.left)).toBeLessThanOrEqual(1) + expect(Math.abs(hoverStyles[0]!.right - hoverStyles[1]!.right)).toBeLessThanOrEqual(1) + expect(hoverStyles[1]!.radius).toBe(hoverStyles[0]!.radius) + expect(hoverStyles[1]!.background).toBe(hoverStyles[0]!.background) + expect(hoverStyles[1]!.background).not.toBe('rgba(0, 0, 0, 0)') + await row.getByRole('button').click() + await row.locator('[data-team-task-owner]').filter({ hasText: 'Unowned' }).waitFor() + await row.getByText('Created by the root agent', { exact: true }).waitFor() + expect(await row.locator('[data-team-task-details]').evaluate((element) => { + const style = getComputedStyle(element) + return { fontSize: style.fontSize, indent: style.paddingLeft } + })).toEqual({ fontSize: '12px', indent: '21px' }) + expect(await row.locator('[data-team-task-details] p, [data-team-task-owner]') + .evaluateAll(elements => elements.every(element => getComputedStyle(element).fontSize === '12px'))).toBe(true) + expect(await row.getByRole('button').getAttribute('aria-expanded')).toBe('true') + expect(await action.evaluate(element => element.getBoundingClientRect().width)).toBe(360) await scaffold.ctx.agentTeams.updateTask(agent, { taskId: task.id, expectedRevision: task.revision, action: 'claim', }) - await action.getByText('In progress', { exact: true }).waitFor() - await action.getByText('Owner: lead', { exact: true }).waitFor() + await row.getByRole('img', { name: 'In progress', exact: true }).waitFor() + await row.locator('[data-team-task-owner]').filter({ hasText: agent.id }).waitFor() + await action.getByRole('heading', { name: 'Shared tasks 1 in progress', exact: true }).waitFor() expect(await action.getByRole('button', { name: /^(New task|Edit|Complete|Reopen|Delete)$/u }).count()).toBe(0) expect(await action.locator('input, select, textarea').count()).toBe(0) - const snapshot = await captureStableAria(page, '[data-team-panel]', scaffold.workspaceCwd) + const snapshot = await captureStableAria(page, '[data-subagent-panel]', scaffold.workspaceCwd) await compareOrRefreshGolden(PANEL_EXPECTED, snapshot, MODE) expect(projectionReads).not.toHaveBeenCalled() expect(tripwire.pageErrors).toEqual([]) @@ -117,15 +433,113 @@ describe('web e2e: Agent Teams panel', () => { await page.keyboard.press('Escape') }, 60_000) + it('hides the board while disabled and restores its tasks when re-enabled', async () => { + await page.getByRole('button', { name: /^Select model/u }).click() + const toggle = page.getByRole('switch', { name: 'Enable agent teamwork', exact: true }) + await toggle.click() + await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('false') + expect(await page.locator('[data-subagent-action]').count()).toBe(1) + await page.keyboard.press('Escape') + await page.locator('[data-subagent-action]').getByRole('button', { name: '1 subagent', exact: true }).click() + await page.getByRole('treeitem', { name: /Researcher/u }).waitFor() + expect(await page.locator('[data-team-panel]').count()).toBe(0) + expect(await page.locator('[data-subagent-panel]').evaluate(element => element.getBoundingClientRect().width)).toBe(360) + expect(await page.locator('[data-subagent-section-header]').count()).toBe(0) + await page.keyboard.press('Escape') + await page.getByRole('button', { name: /^Select model/u }).click() + await toggle.click() + await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('true') + await page.keyboard.press('Escape') + await page.locator('[data-subagent-action]').getByRole('button', { name: /subagents?$/u }).click() + await page.locator('[data-subagent-panel]').getByText('Agent task', { exact: true }).waitFor() + await page.keyboard.press('Escape') + }) + + it('keeps the narrow Users trigger focused and opens its catalog only on explicit activation', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-team-narrow-trigger')) + const trigger = page.locator('[data-subagent-action]').getByRole('button', { name: '1 subagent', exact: true }) + const panel = page.locator('[data-subagent-panel]') + const count = trigger.locator('[data-subagent-count]') + const viewport = page.viewportSize()! + expect(await count.isVisible()).toBe(true) + expect(await trigger.locator('[data-icon="dsw-icon-users-outline-regular"]').count()).toBe(1) + expect(await trigger.locator('[data-icon="dsw-icon-users-outline-regular"]').evaluate((element) => { + const bounds = element.getBoundingClientRect() + return { width: bounds.width, height: bounds.height } + })).toEqual({ width: 14, height: 14 }) + expect(await trigger.locator('[data-icon="dsw-icon-chevron-down-outline-regular"]').count()).toBe(0) + expect(await trigger.locator('svg').count()).toBe(1) + await trigger.focus() + const original = await trigger.elementHandle() + if (original === null) throw new Error('Subagent trigger is missing') + try { + await page.setViewportSize({ width: 390, height: 700 }) + await expect.poll(() => count.evaluate(element => getComputedStyle(element).display)).toBe('none') + expect(await original.evaluate(element => element.isConnected && element === document.activeElement)).toBe(true) + const expandSidebar = page.locator('[data-sidebar-right-expand]') + const compactStyles = await Promise.all([trigger, expandSidebar].map(control => control.evaluate((element) => { + const bounds = element.getBoundingClientRect() + const icon = element.querySelector('svg')!.getBoundingClientRect() + const style = getComputedStyle(element) + return { width: bounds.width, height: bounds.height, padding: style.padding, radius: style.borderRadius, + color: style.color, iconWidth: icon.width, iconHeight: icon.height } + }))) + expect(compactStyles[0]).toEqual(compactStyles[1]) + expect(compactStyles[0]).toMatchObject({ width: 28, height: 28, padding: '0px', iconWidth: 15, iconHeight: 15 }) + expect(await trigger.evaluate(element => getComputedStyle(element.closest('[data-slot="conversation.session.header.actions"]')!.parentElement!).gap)).toBe('8px') + await expandSidebar.hover() + const expandHover = await expandSidebar.evaluate((element) => { + const style = getComputedStyle(element) + return { background: style.backgroundColor, color: style.color } + }) + await page.mouse.move(1, 1) + await page.keyboard.press('Tab') + const countTooltip = page.getByRole('tooltip', { name: '1 subagent', exact: true }) + await countTooltip.waitFor({ state: 'hidden' }) + await trigger.hover() + expect(await trigger.evaluate((element) => { + const style = getComputedStyle(element) + return { background: style.backgroundColor, color: style.color } + })).toEqual(expandHover) + expect(expandHover.background).not.toBe('rgba(0, 0, 0, 0)') + // Observe beyond the wide trigger's 150ms hover-open deadline. + await page.waitForTimeout(200) + expect(await panel.count()).toBe(0) + await countTooltip.waitFor({ state: 'visible' }) + expect(await panel.count()).toBe(0) + await trigger.click() + await panel.waitFor() + expect(await trigger.getAttribute('aria-expanded')).toBe('true') + expect(await page.getByRole('tooltip', { name: '1 subagent', exact: true }).count()).toBe(0) + await panel.getByRole('treeitem', { name: /Researcher/u }).waitFor() + await panel.locator('[data-team-task]').getByRole('button', { expanded: false }).waitFor() + await trigger.click() + await panel.waitFor({ state: 'detached' }) + expect(await trigger.getAttribute('aria-expanded')).toBe('false') + await trigger.press('ArrowDown') + await panel.waitFor() + await page.mouse.move(1, 1) + // Keyboard activation stays pinned beyond the 120ms pointer-leave grace. + await page.waitForTimeout(180) + expect(await panel.count()).toBe(1) + await page.keyboard.press('Escape') + await panel.waitFor({ state: 'detached' }) + } finally { + await page.keyboard.press('Escape') + await page.setViewportSize(viewport) + await original.dispose() + } + }) + it('keeps the panel inside the viewport and closes on outside click or Escape', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-team-panel-keyboard')) - const panel = page.getByRole('dialog', { name: 'Agent team', exact: true }) - const trigger = page.locator('[data-team-action]').getByRole('button', { name: 'Agent team', exact: true }) + const panel = page.locator('[data-subagent-panel]') + const trigger = page.locator('[data-subagent-action]').getByRole('button', { name: /subagents?$/u }) const viewport = page.viewportSize()! await trigger.click() - await panel.getByText('lead', { exact: true }).waitFor() + await panel.getByRole('heading', { name: /Shared tasks/u }).waitFor() try { - for (const size of [{ width: 760, height: 540 }, { width: 600, height: 420 }]) { + for (const size of [{ width: 760, height: 540 }, { width: 600, height: 420 }, { width: 390, height: 700 }]) { await page.setViewportSize(size) await expect.poll(() => panel.evaluate((element) => { const rect = element.getBoundingClientRect() @@ -136,6 +550,30 @@ describe('web e2e: Agent Teams panel', () => { .every(([x, y]) => element.contains(document.elementFromPoint(x!, y!))) })).toBe(true) } + const row = panel.getByRole('treeitem', { name: /Researcher/u }) + const primary = row.getByText(RESEARCHER_DESCRIPTION, { exact: true }) + const secondary = row.getByText('team-panel-researcher', { exact: true }) + expect(await row.getByRole('button').count()).toBe(0) + expect(await primary.evaluate((element) => { + const style = getComputedStyle(element) + return { + tag: element.tagName, clipped: element.scrollWidth > element.clientWidth || element.scrollHeight > element.clientHeight, + whiteSpace: style.whiteSpace, overflowWrap: style.overflowWrap, + } + })).toEqual({ tag: 'SPAN', clipped: false, whiteSpace: 'normal', overflowWrap: 'anywhere' }) + const primaryBounds = await primary.boundingBox() + const secondaryBounds = await secondary.boundingBox() + expect(primaryBounds).not.toBeNull() + expect(secondaryBounds).not.toBeNull() + expect(primaryBounds!.y + primaryBounds!.height).toBeLessThanOrEqual(secondaryBounds!.y) + const beforeClick = page.url() + await primary.click() + expect(page.url()).toBe(beforeClick) + expect(await page.getByRole('tooltip', { name: RESEARCHER_DESCRIPTION, exact: true }).count()).toBe(0) + expect(await panel.evaluate(element => element.scrollWidth <= element.clientWidth)).toBe(true) + expect(await secondary.evaluate(element => element.scrollWidth <= element.clientWidth)).toBe(true) + await secondary.hover() + expect(await page.getByRole('tooltip').count()).toBe(0) await panel.getByRole('heading', { name: /Shared tasks/u }).click() expect(await panel.count()).toBe(1) await page.mouse.click(2, 2) @@ -166,8 +604,8 @@ describe('web e2e: Agent Teams panel', () => { await saveFailureShot(page, 'web-e2e-agent-team-hover') console.error('Agent Team hover diagnostics', JSON.stringify({ errors: tripwire.pageErrors, warnings: tripwire.warnings })) }) - const trigger = page.locator('[data-team-action]').getByRole('button', { name: 'Agent team', exact: true }) - const panel = page.getByRole('dialog', { name: 'Agent team', exact: true }) + const trigger = page.locator('[data-subagent-action]').getByRole('button', { name: /subagents?$/u }) + const panel = page.locator('[data-subagent-panel]') await trigger.hover() await panel.waitFor() await panel.hover() @@ -178,37 +616,412 @@ describe('web e2e: Agent Teams panel', () => { await page.keyboard.press('Escape') }) - it('offers expansion when resizing makes a task description exceed two lines', async () => { + it('keeps compact tasks ordered, opens one full detail at a time, and scrolls only the fixed-width menu', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-team-compact-details')) const agent = scaffold.ctx.agents.list()[0]! - await scaffold.ctx.agentTeams.createTask(agent, { - subject: 'Responsive task', - description: 'Check the task description before you start work. '.repeat(3), - writeScopes: [], + const completed = await scaffold.ctx.agentTeams.createTask(agent, { + subject: 'Completed preparation', description: 'Preparation is complete.', writeScopes: [], + }) + const claimed = await scaffold.ctx.agentTeams.updateTask(agent, { + taskId: completed.id, expectedRevision: completed.revision, action: 'claim', + }) + await scaffold.ctx.agentTeams.updateTask(agent, { + taskId: completed.id, expectedRevision: claimed.revision, action: 'complete', + }) + const blocker = await scaffold.ctx.agentTeams.createTask(agent, { + subject: 'Dependency task', description: 'Finish this prerequisite first.', writeScopes: [], + }) + const subject = 'Responsive task with a long title that remains fully readable when expanded at the minimum supported viewport' + const description = 'Check every dependency and write scope before you start work.\n'.repeat(16).trim() + const task = await scaffold.ctx.agentTeams.createTask(agent, { + subject, description, blockedBy: [blocker.id], writeScopes: ['src/web/components'], }) const viewport = page.viewportSize()! - const trigger = page.locator('[data-team-action]').getByRole('button', { name: 'Agent team', exact: true }) - const panel = page.getByRole('dialog', { name: 'Agent team', exact: true }) - const card = panel.locator('article').filter({ hasText: 'Responsive task' }) + const trigger = page.locator('[data-subagent-action]').getByRole('button', { name: /subagents?$/u }) + const panel = page.locator('[data-subagent-panel]') + const board = panel.locator('[data-team-panel]') + const row = board.locator(`[data-team-task="${task.id}"]`) + const emptyRow = board.locator(`[data-team-task="${completed.id}"]`) + onTestFinished(() => page.setViewportSize(viewport)) + // Constrain the menu so closed and expanded task rows both exercise its scrollbar. + await page.setViewportSize({ width: viewport.width, height: 540 }) await trigger.click() - await card.waitFor() - expect(await card.getByRole('button', { name: 'Show more' }).count()).toBe(0) + await row.waitFor() + expect(await panel.evaluate(element => element.getBoundingClientRect().width)).toBe(360) + expect(await board.locator('[data-team-task-details], article').count()).toBe(0) + expect(await board.getByRole('button', { expanded: false }).count()).toBe(4) + expect(await board.locator('[data-team-task]').evaluateAll(elements => elements.map(element => element.getAttribute('data-status')))) + .toEqual(['in_progress', 'completed', 'pending', 'pending']) + const counts = board.getByText('1 completed\u2002·\u20021 in progress\u2002·\u20022 pending', { exact: true }) + await counts.waitFor() + expect(await counts.textContent()).toBe('1 completed\u2002·\u20021 in progress\u2002·\u20022 pending') + expect(await panel.locator('[data-subagent-section-header] button, h3 button').count()).toBe(0) + expect(await panel.getByRole('button', { name: /^Show (more|less)$/u }).count()).toBe(0) + await row.getByRole('img', { name: 'Blocked by dependencies', exact: true }).waitFor() + const measureScroll = async () => panel.evaluate((element) => { + const style = getComputedStyle(element) + const bounds = element.getBoundingClientRect() + const scroller = element.querySelector('[role="tree"]')!.parentElement! + const scrollStyle = getComputedStyle(scroller) + const scrollBounds = scroller.getBoundingClientRect() + const taskBounds = element.querySelector('[data-team-task] button')!.getBoundingClientRect() + const scrollbarWidth = Number.parseFloat(getComputedStyle(scroller, '::-webkit-scrollbar').width) + return { + innerWidth: bounds.width - Number.parseFloat(style.paddingLeft) - Number.parseFloat(style.paddingRight) + - Number.parseFloat(style.borderLeftWidth) - Number.parseFloat(style.borderRightWidth), + clientWidth: scroller.clientWidth, clientHeight: scroller.clientHeight, scrollHeight: scroller.scrollHeight, + overflowing: scroller.scrollHeight > scroller.clientHeight, + gutter: scrollStyle.scrollbarGutter, marginRight: scrollStyle.marginRight, + trackMargin: scrollStyle.getPropertyValue('--dsh-scrollbar-track-margin').trim(), + taskLeft: taskBounds.left, taskRight: taskBounds.right, + trackLeft: scrollBounds.right - scrollbarWidth, trackRight: scrollBounds.right, panelRight: bounds.right, + arrowsBeforeTrack: [...element.querySelectorAll('[data-team-task] button svg')] + .every(arrow => arrow.getBoundingClientRect().right <= scrollBounds.right - scrollbarWidth), + } + }) + const closedScroll = await measureScroll() + expect(closedScroll.overflowing, JSON.stringify(closedScroll)).toBe(true) + expect(Math.abs(closedScroll.clientWidth - closedScroll.innerWidth), JSON.stringify(closedScroll)).toBeLessThanOrEqual(1) + expect(closedScroll.gutter).toBe('stable') + expect(closedScroll.marginRight).toBe('-5px') + expect(closedScroll.trackMargin).toBe('6px') + await row.getByRole('button').click() + const expandedScroll = await measureScroll() + expect(expandedScroll.overflowing).toBe(true) + expect(expandedScroll.clientWidth).toBe(closedScroll.clientWidth) + expect(expandedScroll.taskLeft).toBe(closedScroll.taskLeft) + expect(expandedScroll.taskRight).toBe(closedScroll.taskRight) + expect(expandedScroll.arrowsBeforeTrack).toBe(true) + expect(expandedScroll.trackLeft).toBeGreaterThanOrEqual(expandedScroll.panelRight - 6) + expect(expandedScroll.trackRight).toBeLessThanOrEqual(expandedScroll.panelRight - 1) + expect(await panel.evaluate(element => element.getBoundingClientRect().width)).toBe(360) + const details = row.locator('[data-team-task-details]') + expect(await details.locator('p:not([data-team-task-owner])').textContent()).toBe(description) + expect(await details.locator('[data-team-task-owner]').textContent()).toBe('Owner: Unowned') + expect(await details.locator(':scope > *').evaluateAll(elements => elements.map(element => + element.hasAttribute('data-team-task-owner') ? 'owner' : element.tagName))).toEqual(['P', 'owner']) + expect(await details.locator('dl, dt, dd').count()).toBe(0) + for (const omitted of [task.id, blocker.subject, 'Readiness', 'Write scopes', 'Write scope warnings', 'src/web/components']) { + expect(await details.textContent()).not.toContain(omitted) + } + expect(await row.getByRole('button').getAttribute('aria-controls')).toBe(await details.getAttribute('id')) try { - await page.setViewportSize({ width: 360, height: 700 }) - await card.getByRole('button', { name: 'Show more' }).waitFor() - expect(await card.locator('p').evaluate(p => p.scrollHeight > p.clientHeight)).toBe(true) - await compareOrRefreshGolden(RESIZE_EXPECTED, await captureStableAria(page, '[data-team-panel]', scaffold.workspaceCwd), MODE) - await card.getByRole('button', { name: 'Show more' }).click() - expect(await card.locator('p').evaluate(p => p.scrollHeight === p.clientHeight)).toBe(true) - await card.getByRole('button', { name: 'Show less' }).click() + await page.setViewportSize({ width: 390, height: 700 }) + await expect.poll(() => panel.evaluate((element) => { + const rect = element.getBoundingClientRect() + return rect.width === Math.min(360, innerWidth - 32) && rect.left >= 16 && rect.right <= innerWidth - 16 + && rect.top >= 16 && rect.bottom <= innerHeight - 16 + && element.scrollWidth <= element.clientWidth + })).toBe(true) + expect(await panel.locator('*').evaluateAll((elements) => { + const scrollers = elements.filter(element => + ['auto', 'scroll'].includes(getComputedStyle(element).overflowY) + && element.scrollHeight > element.clientHeight) + return scrollers.map(element => ({ + hasHeader: element.querySelector('[data-subagent-section-header]') !== null, + hasTree: element.querySelector('[role="tree"]') !== null, + hasBoard: element.querySelector('[data-team-panel]') !== null, + fitsWidth: element.scrollWidth <= element.clientWidth, + })) + })).toEqual([{ hasHeader: true, hasTree: true, hasBoard: true, fitsWidth: true }]) + const narrowScroll = await measureScroll() + expect(Math.abs(narrowScroll.clientWidth - narrowScroll.innerWidth), JSON.stringify(narrowScroll)).toBeLessThanOrEqual(1) + expect(narrowScroll.arrowsBeforeTrack).toBe(true) + const scroller = panel.getByRole('tree').locator('..') + await scroller.hover({ position: { x: 20, y: 20 } }) + await Promise.all([ + scroller.evaluate(element => new Promise((resolve) => { + element.addEventListener('scrollend', () => { resolve() }, { once: true }) + })), + page.mouse.wheel(0, 180), + ]) + await expect.poll(() => scroller.evaluate(element => element.scrollTop)).toBeGreaterThan(0) + await scroller.evaluate((element) => { element.scrollTo({ top: 0 }) }) + await expect.poll(() => scroller.evaluate(element => element.scrollTop)).toBe(0) + expect(await row.getByText(subject, { exact: true }).evaluate(element => + element.scrollWidth <= element.clientWidth && getComputedStyle(element).whiteSpace === 'pre-wrap')).toBe(true) + expect(await details.locator('p:not([data-team-task-owner])').evaluate(element => element.scrollHeight === element.clientHeight)).toBe(true) + const ownerBounds = await details.locator('[data-team-task-owner]').boundingBox() + const descriptionBounds = await details.locator('p:not([data-team-task-owner])').boundingBox() + expect(ownerBounds).not.toBeNull() + expect(descriptionBounds).not.toBeNull() + expect(ownerBounds!.height).toBeLessThanOrEqual(18) + expect(ownerBounds!.y).toBeGreaterThanOrEqual(descriptionBounds!.y + descriptionBounds!.height) + const detailColor = await details.evaluate(element => getComputedStyle(element).color) + expect(await details.locator('[data-team-task-owner], [data-team-task-owner] span') + .evaluateAll(elements => elements.map(element => getComputedStyle(element).color))) + .toEqual([detailColor, detailColor, detailColor]) + expect(await counts.evaluate(element => element.scrollWidth <= element.clientWidth)).toBe(true) + const titleBounds = await board.getByText('Shared tasks', { exact: true }).boundingBox() + const countBounds = await counts.boundingBox() + expect(titleBounds).not.toBeNull() + expect(countBounds).not.toBeNull() + expect(countBounds!.y).toBeGreaterThanOrEqual(titleBounds!.y + titleBounds!.height) + await compareOrRefreshGolden(RESIZE_EXPECTED, await captureStableAria(page, '[data-subagent-panel]', scaffold.workspaceCwd), MODE) + await emptyRow.getByRole('button').click() + expect(await row.getAttribute('data-expanded')).toBe('false') + expect(await row.locator('[data-team-task-details]').count()).toBe(0) + expect(await board.locator('[data-team-task-details]').count()).toBe(1) + expect(await emptyRow.locator('[data-team-task-owner]').textContent()).toBe(`Owner: ${agent.id}`) + expect(await emptyRow.locator('[data-team-task-details] p:not([data-team-task-owner])').textContent()).toBe(completed.description) + expect(await emptyRow.locator('dl, dt, dd').count()).toBe(0) + expect(await emptyRow.locator('[data-team-task-details]').textContent()).not.toContain(completed.id) + await emptyRow.getByRole('button').click() + expect(await board.locator('[data-team-task-details]').count()).toBe(0) await page.setViewportSize(viewport) - await expect.poll(() => card.getByRole('button', { name: 'Show more' }).count()).toBe(0) + expect(await panel.evaluate(element => element.getBoundingClientRect().width)).toBe(360) } finally { await page.keyboard.press('Escape') await page.setViewportSize(viewport) } }) + it('confirms disabling after a recorded model request and keeps cancellation side-effect free', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-team-disable-confirmation')) + const agent = scaffold.ctx.agents.list()[0]! + const selection = agent.model.current() + if (selection === undefined) throw new Error('Team fixture has no selected model') + agent.session.append('request/header', { header: { config: selection }, reason: 'initial' }) + await scaffold.ctx.sessions.flush(agent.session) + const openConfirmation = async (): Promise => { + await page.getByRole('button', { name: /^Select model/u }).click() + await page.getByRole('switch', { name: 'Enable agent teamwork', exact: true }).click() + await page.getByRole('dialog', { name: 'Disable agent teamwork?', exact: true }).waitFor() + } + await openConfirmation() + const dialog = page.getByRole('dialog', { name: 'Disable agent teamwork?', exact: true }) + await assertWarningDialogLayout(dialog) + expect(await page.getByRole('menu', { name: 'Model and reasoning effort', exact: true }).count()).toBe(0) + expect(await dialog.getByRole('checkbox').count()).toBe(0) + expect(scaffold.ctx.agentTeams.isEnabled(agent)).toBe(true) + await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'disable-confirmation.expected.md'), + await captureStableAria(page, '[aria-modal="true"]', scaffold.workspaceCwd), MODE) + await dialog.getByRole('button', { name: 'Cancel', exact: true }).click() + await dialog.waitFor({ state: 'hidden' }) + await expect.poll(() => page.getByRole('button', { name: /^Select model/u }).evaluate(element => element === document.activeElement)).toBe(true) + expect(scaffold.ctx.agentTeams.isEnabled(agent)).toBe(true) + await openConfirmation() + await page.keyboard.press('Escape') + await dialog.waitFor({ state: 'hidden' }) + await expect.poll(() => page.getByRole('button', { name: /^Select model/u }).evaluate(element => element === document.activeElement)).toBe(true) + expect(scaffold.ctx.agentTeams.isEnabled(agent)).toBe(true) + await openConfirmation() + await dialog.getByRole('button', { name: 'Disable', exact: true }).click() + await dialog.waitFor({ state: 'hidden' }) + await expect.poll(() => page.getByRole('button', { name: /^Select model/u }).evaluate(element => element === document.activeElement)).toBe(true) + await expect.poll(() => scaffold.ctx.agentTeams.isEnabled(agent)).toBe(false) + await page.getByRole('button', { name: /^Select model/u }).click() + await page.getByRole('switch', { name: 'Enable agent teamwork', exact: true }).click() + await expect.poll(() => scaffold.ctx.agentTeams.isEnabled(agent)).toBe(true) + expect(await dialog.count()).toBe(0) + await page.keyboard.press('Escape') + expect(agent.session.ownEvents().filter(event => event.type === 'command/run' || event.type === 'command/done')).toEqual([]) + expect(tripwire.pageErrors).toEqual([]) + }) + + it('removes both section headings and the board divider when the last shared task is deleted', async () => { + const agent = scaffold.ctx.agents.list()[0]! + await page.getByRole('button', { name: '1 subagent', exact: true }).click() + const panel = page.locator('[data-subagent-panel]') + await panel.locator('[data-team-panel]').waitFor() + for (const task of scaffold.ctx.agentTeams.listTasks(agent).toReversed()) { + if (task.status === 'deleted') continue + await scaffold.ctx.agentTeams.updateTask(agent, { + taskId: task.id, expectedRevision: task.revision, action: 'delete', + }) + } + await panel.locator('[data-team-panel]').waitFor({ state: 'detached' }) + expect(await panel.locator('[data-subagent-section-header]').count()).toBe(0) + expect(await panel.getByRole('heading').count()).toBe(0) + expect(await panel.getByText('No shared tasks yet. Create them through the conversation.').count()).toBe(0) + expect(await panel.getByRole('treeitem', { name: /Researcher/u }).count()).toBe(1) + expect(await panel.evaluate(element => element.getBoundingClientRect().width)).toBe(360) + await page.keyboard.press('Escape') + }) + it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => { - await assertFixtureInventory(SNAPSHOT_DIR, ['task.expected.md', 'resize.expected.md']) + await assertFixtureInventory(SNAPSHOT_DIR, [ + 'task.expected.md', 'resize.expected.md', 'info.expected.md', 'task-only.expected.md', + 'disable-confirmation.expected.md', 'collaboration.zh.expected.md', + 'command-error.en.expected.md', 'command-error.zh.expected.md', + ]) }) }) + +it.each(RECEIPT_LOCALES)('toggles teamwork in $language without command records or Chat changes after reload', async (locale) => { + await withTeamReceiptPage(locale, async (page, scaffold, tripwire) => { + const agent = scaffold.ctx.agents.list()[0] + if (agent === undefined) throw new Error('Team receipt fixture has no Agent') + const chat = page.locator('[data-chat-flow]').first() + const recordedReady = await captureStableAria(page, '[data-chat-flow]', scaffold.workspaceCwd) + expect(recordedReady).toContain('Ready.') + expect(recordedReady).toContain('Open the task board.') + const captureLayout = () => chat.evaluate((element) => { + const flow = element.getBoundingClientRect() + return { + height: flow.height, + rows: [...element.querySelectorAll('[data-chat-flow-key]')].map((row) => { + const bounds = row.getBoundingClientRect() + return { kind: row.getAttribute('data-chat-flow-kind'), top: bounds.top - flow.top, height: bounds.height } + }), + } + }) + const recordedLayout = await captureLayout() + const commandEvents = () => agent.session.snapshotEvents() + .filter(event => event.type === 'command/run' || event.type === 'command/done') + expect(commandEvents()).toEqual([]) + const input = page.locator('[data-composer-input]').first() + await writeComposerDraft(page, input, '/') + const suggestions = page.getByRole('listbox', { name: locale.suggestions, exact: true }) + await suggestions.getByRole('option').first().waitFor() + const candidate = suggestions.getByRole('option').filter({ hasText: locale.commandLabel }) + expect(await candidate.count()).toBe(0) + await writeComposerDraft(page, input, '/tea') + await expect.poll(() => suggestions.getByRole('option').count()).toBe(0) + expect(await candidate.count()).toBe(0) + await input.press('Escape') + await suggestions.waitFor({ state: 'hidden' }) + await writeComposerDraft(page, input, '') + expect(commandEvents()).toEqual([]) + expect(scaffold.ctx.agentTeams.isEnabled(agent)).toBe(false) + const assertChatUnchanged = async (): Promise => { + await chat.getByText('Ready.', { exact: true }).waitFor() + expect(await page.locator('[data-team-command], [data-chat-flow-kind="command"], [data-command-input]').count()).toBe(0) + expect(await captureStableAria(page, '[data-chat-flow]', scaffold.workspaceCwd)).toBe(recordedReady) + await expect.poll(captureLayout).toEqual(recordedLayout) + } + for (const enabled of [true, false]) { + await page.getByRole('button', { name: locale.modelButton }).click() + const toggle = page.getByRole('switch', { name: locale.toggle, exact: true }) + await toggle.click() + await expect.poll(() => toggle.getAttribute('aria-checked')).toBe(String(enabled)) + await expect.poll(() => scaffold.ctx.agentTeams.isEnabled(agent)).toBe(enabled) + expect(scaffold.ctx.commands.list(agent).some(command => command.name === 'team')).toBe(false) + expect(commandEvents()).toEqual([]) + await page.keyboard.press('Escape') + await assertChatUnchanged() + } + await scaffold.ctx.sessions.flush(agent.session) + const recordedCommands = commandEvents() + expect(recordedCommands).toEqual([]) + const warningStart = tripwire.warnings.length + await page.reload({ waitUntil: 'load' }) + await assertChatUnchanged() + expect(commandEvents()).toEqual(recordedCommands) + expect(scaffold.ctx.agentTeams.isEnabled(agent)).toBe(false) + acknowledgeReloadConnectionLoss(tripwire, warningStart) + + // Historical receipts are read-only fixtures, not invocations of a registered command. + for (const args of [' on', ' off']) { + const commandId = CommandId(`historical-team-${args.trim()}`) + agent.session.append('command/run', { commandId, name: 'team', args, source: { kind: 'user' } }) + agent.session.append('command/done', { commandId, kind: 'success', text: 'Team mode saved.' }) + } + agent.session.append('command/run', { + commandId: CommandId('historical-team-pending'), name: 'team', args: ' on', source: { kind: 'user' }, + }) + await scaffold.ctx.sessions.flush(agent.session) + const historicalCommands = commandEvents() + expect(historicalCommands.map(event => event.type)).toEqual([ + 'command/run', 'command/done', 'command/run', 'command/done', 'command/run', + ]) + const historicalWarningStart = tripwire.warnings.length + await page.reload({ waitUntil: 'load' }) + await assertChatUnchanged() + expect(commandEvents()).toEqual(historicalCommands) + expect(scaffold.ctx.agentTeams.isEnabled(agent)).toBe(false) + acknowledgeReloadConnectionLoss(tripwire, historicalWarningStart) + }) +}) + +it.each(RECEIPT_LOCALES)('renders historical Team failures in $language without a success receipt after reload', async (locale) => { + await withTeamReceiptPage(locale, async (page, scaffold, tripwire) => { + const agent = scaffold.ctx.agents.list()[0] + if (agent === undefined) throw new Error('Team failure fixture has no Agent') + const commandId = CommandId('historical-team-error') + agent.session.append('command/run', { commandId, name: 'team', args: ' invalid', source: { kind: 'user' } }) + agent.session.append('command/done', { commandId, kind: 'error', text: 'Use /team on or /team off.' }) + await scaffold.ctx.sessions.flush(agent.session) + const row = page.locator('[data-team-command]') + const captureFailure = async (): Promise => { + await row.getByText(locale.failure, { exact: true }).waitFor() + expect(await row.count()).toBe(1) + expect(await row.getAttribute('data-state')).toBe('error') + expect(await row.getByText(locale.title, { exact: true }).count()).toBe(1) + expect(await row.getByText(locale.enabled, { exact: true }).count()).toBe(0) + expect(await row.getByText(locale.disabled, { exact: true }).count()).toBe(0) + expect(scaffold.ctx.agentTeams.isEnabled(agent)).toBe(false) + const collapsed = await captureStableAria(page, '[data-team-command]', scaffold.workspaceCwd) + const disclosure = row.getByRole('button', { expanded: false }) + await disclosure.click() + await row.getByRole('button', { expanded: true }).waitFor() + await row.getByText('Use /team on or /team off.', { exact: true }).waitFor() + const expanded = await captureStableAria(page, '[data-team-command]', scaffold.workspaceCwd) + return `## Failed receipt\n\n${collapsed}\n\n## Raw error\n\n${expanded}` + } + const failure = await captureFailure() + await compareOrRefreshGolden(join(SNAPSHOT_DIR, `command-error.${locale.code}.expected.md`), failure, MODE) + await scaffold.ctx.sessions.flush(agent.session) + const warningStart = tripwire.warnings.length + await page.reload({ waitUntil: 'load' }) + expect(await captureFailure()).toBe(failure) + acknowledgeReloadConnectionLoss(tripwire, warningStart) + }) +}) + +it('shows the Chinese empty subagent section above compact shared tasks', async () => { + await withTeamReceiptPage(RECEIPT_LOCALES[1], async (page, scaffold) => { + const agent = scaffold.ctx.agents.list()[0]! + await scaffold.ctx.agentTeams.setEnabled(agent, true) + await scaffold.ctx.agentTeams.createTask(agent, { + subject: '准备协作', description: '先准备共享任务,再创建子智能体。', + }) + await page.getByRole('button', { name: '0 个子智能体', exact: true }).click() + const panel = page.locator('[data-subagent-panel]') + await panel.getByText('暂无子智能体', { exact: true }).waitFor() + expect(await panel.locator('[data-subagent-section-header]').textContent()).toBe('子智能体') + expect(await panel.locator('[data-subagent-section-header] button, h3 button').count()).toBe(0) + expect(await panel.evaluate(element => element.getBoundingClientRect().width)).toBe(360) + await panel.getByRole('heading', { name: '共享任务 1 待处理', exact: true }).waitFor() + expect(await panel.locator('[data-team-task-details]').count()).toBe(0) + }) +}) + +it('uses the Chinese collaboration name in the switch and disable confirmation', async () => { + const scaffold = await launchWebScaffold() + let browser: Browser | undefined + try { + browser = await chromium.launch() + const page = await browser.newPage({ locale: 'zh-CN', timezoneId: 'Asia/Shanghai' }) + const tripwire = watchConsole(page) + await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) + await connectFreshWorkspaceZh(page, scaffold.workspaceCwd) + const agent = scaffold.ctx.agents.list()[0] + if (agent === undefined) throw new Error('Chinese Team fixture has no Agent') + await scaffold.ctx.agentTeams.setEnabled(agent, true) + const selection = agent.model.current() + if (selection === undefined) throw new Error('Chinese Team fixture has no model') + agent.session.append('request/header', { header: { config: selection }, reason: 'initial' }) + await scaffold.ctx.sessions.flush(agent.session) + await page.getByRole('button', { name: /^选择模型/u }).click() + const toggle = page.getByRole('switch', { name: '启用智能体团队协作', exact: true }) + await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('true') + await page.getByRole('button', { name: '关于智能体团队协作', exact: true }).waitFor() + expect(await toggle.evaluate(element => element.parentElement!.scrollWidth <= element.parentElement!.clientWidth)).toBe(true) + const menu = await captureStableAria(page, '[role="menu"]', scaffold.workspaceCwd) + await toggle.click() + const dialog = page.getByRole('dialog', { name: '确认停用智能体团队协作?', exact: true }) + await dialog.waitFor() + await dialog.getByText('对话过程中停用智能体团队协作,将停止共享任务工具和团队协调指令。', { exact: true }).waitFor() + await assertWarningDialogLayout(dialog) + const confirmation = await captureStableAria(page, '[aria-modal="true"]', scaffold.workspaceCwd) + await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'collaboration.zh.expected.md'), + `## Menu\n\n${menu}\n\n## Confirmation\n\n${confirmation}`, MODE) + await dialog.getByRole('button', { name: '取消', exact: true }).click() + expect(scaffold.ctx.agentTeams.isEnabled(agent)).toBe(true) + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + } finally { + try { await browser?.close() } finally { await scaffold.close() } + } +}) diff --git a/apps/web/tests/agent-team-panel.overlay.yml b/apps/web/tests/agent-team-panel.overlay.yml deleted file mode 100644 index bdcf49e256..0000000000 --- a/apps/web/tests/agent-team-panel.overlay.yml +++ /dev/null @@ -1,27 +0,0 @@ -# Equivalent to the Agent Teams bundle over the stable base and Web bundles. -- id: tool-subagent-control - 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: ui-agent-team - name: '@deepseek-ai/dsh-experimental-client-ui-agent-team' diff --git a/apps/web/tests/assembled-remote.ts b/apps/web/tests/assembled-remote.ts index 4d60b8749e..08a226a835 100644 --- a/apps/web/tests/assembled-remote.ts +++ b/apps/web/tests/assembled-remote.ts @@ -158,7 +158,6 @@ export function createAssembledRemote(options: AssembledRemoteOptions = {}): Ass 'agentPresets/list': structuredClone(fixture.agentPresets), 'commands/list': structuredClone(fixture.commands), 'settings/openSettingsDocument': ok({ opened: true }), - 'subagents/list': ok({ entries: [], parentAvailable: true }), 'terminal/list': ok([]), 'skills/list': ok({ skills: [] }), 'session/canOpenWorkspacePath': ok(true), diff --git a/apps/web/tests/background-job-list.e2e.ts b/apps/web/tests/background-job-list.e2e.ts index fab978232b..899cda61c4 100644 --- a/apps/web/tests/background-job-list.e2e.ts +++ b/apps/web/tests/background-job-list.e2e.ts @@ -85,7 +85,7 @@ describe.skipIf(MODE === 'record')('web e2e: background job list', () => { let phase = 'running' onTestFailed(() => saveFailureShot(page, `web-e2e-background-job-${phase}`)) // Polling for zero would pass at t=0 before delivery and prove nothing. - const trigger = page.getByRole('button', { name: '1 background job running' }) + const trigger = page.getByRole('button', { name: '1 background job', exact: true }) expect(await trigger.count()).toBe(0) const started = await executeToolFixture(scaffold.ctx, { @@ -101,10 +101,28 @@ describe.skipIf(MODE === 'record')('web e2e: background job list', () => { const jobId = JobId(matched[0]) await trigger.waitFor({ timeout: 15_000 }) - await trigger.click() - const row = page.getByRole('list', { name: 'Background jobs' }).getByRole('listitem').first() + expect(await trigger.locator('[data-state="ongoing"]').count()).toBe(1) + expect(await trigger.locator('[data-icon="dsw-icon-chevron-down-outline-regular"]').count()).toBe(0) + await trigger.hover() + const list = page.getByRole('list', { name: 'Background jobs' }) + const row = list.getByRole('listitem').first() await row.waitFor({ timeout: 10_000 }) await expect.poll(() => row.textContent()).toContain(COMMAND) + expect(await trigger.getAttribute('aria-expanded')).toBe('true') + await row.hover() + await row.getByRole('button', { name: /Show live output/ }).focus() + await page.mouse.move(2, 2) + // Focusing a menu control pins it beyond the 120ms hover-leave deadline. + await page.waitForTimeout(180) + expect(await list.count()).toBe(1) + await trigger.click() + await list.waitFor({ state: 'detached' }) + await trigger.click() + await list.waitFor() + await page.mouse.move(2, 2) + await page.waitForTimeout(180) + expect(await list.count()).toBe(1) + expect(await trigger.getAttribute('aria-label')).toBe('1 background job') const running = await captureStableAria(page, '[class*="menu"]', scaffold.workspaceCwd, { runningJobs: 'keep' }) await compareOrRefreshGolden(RUNNING_EXPECTED, running, MODE) @@ -120,9 +138,7 @@ describe.skipIf(MODE === 'record')('web e2e: background job list', () => { await expect.poll(() => stop.getAttribute('data-kill-state')).toBe('armed') await stop.click() - // Exact: the running trigger's name ('1 background job running') contains this label. - const idle = page.getByRole('button', { name: '1 background job', exact: true }) - await idle.waitFor({ timeout: 20_000 }) + expect(await trigger.getAttribute('aria-label')).toBe('1 background job') // The unclaimed report's reason lands in the settled row's detail. await expect.poll( () => page.getByRole('list', { name: 'Background jobs' }).textContent(), @@ -185,6 +201,82 @@ describe.skipIf(MODE === 'record')('web e2e: background job list', () => { expect(await trigger.evaluate(node => node === document.activeElement)).toBe(true) }, 60_000) + it('keeps the compact Layers trigger focused and opens jobs only on click or keyboard', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-background-job-compact-trigger')) + const trigger = page.getByRole('button', { name: '1 background job', exact: true }) + const list = page.getByRole('list', { name: 'Background jobs' }) + const count = trigger.locator('[data-job-count]') + const viewport = page.viewportSize()! + await page.getByRole('button', { name: 'Collapse right sidebar', exact: true }).click() + await expect.poll(() => page.locator('[data-rightbar-col]') + .evaluate(column => column.parentElement?.hasAttribute('data-animating'))).toBe(false) + await page.getByRole('button', { name: 'Open right sidebar', exact: true }).waitFor() + await page.mouse.move(2, 2) + expect(await count.isVisible()).toBe(true) + expect(await trigger.locator('[data-icon="dsw-icon-layers-outline-regular"]').count()).toBe(1) + expect(await trigger.locator('svg').count()).toBe(1) + expect(await trigger.locator('svg').evaluate((element) => { + const bounds = element.getBoundingClientRect() + return [bounds.width, bounds.height] + })).toEqual([14, 14]) + await trigger.focus() + const original = await trigger.elementHandle() + if (original === null) throw new Error('Background jobs trigger is missing') + try { + await page.setViewportSize({ width: 390, height: 700 }) + await expect.poll(() => count.evaluate(element => getComputedStyle(element).display)).toBe('none') + expect(await original.evaluate(element => element.isConnected && element === document.activeElement)).toBe(true) + const expandSidebar = page.locator('[data-sidebar-right-expand]') + const compactStyles = await Promise.all([trigger, expandSidebar].map(control => control.evaluate((element) => { + const style = getComputedStyle(element) + const bounds = element.getBoundingClientRect() + const icon = element.querySelector('svg')!.getBoundingClientRect() + return { width: bounds.width, height: bounds.height, padding: style.padding, radius: style.borderRadius, + color: style.color, iconWidth: icon.width, iconHeight: icon.height } + }))) + expect(compactStyles[0]).toEqual(compactStyles[1]) + expect(compactStyles[0]).toMatchObject({ width: 28, height: 28, padding: '0px', iconWidth: 15, iconHeight: 15 }) + expect(await trigger.evaluate(element => getComputedStyle(element.closest('[data-slot="conversation.session.header.actions"]')!.parentElement!).gap)).toBe('8px') + await expandSidebar.hover() + const expandHover = await expandSidebar.evaluate((element) => { + const style = getComputedStyle(element) + return { background: style.backgroundColor, color: style.color } + }) + await expect.poll(() => trigger.evaluate((element) => { + const bounds = element.getBoundingClientRect() + return element.contains(document.elementFromPoint(bounds.left + bounds.width / 2, bounds.top + bounds.height / 2)) + })).toBe(true) + await trigger.hover() + expect(await trigger.evaluate((element) => { + const style = getComputedStyle(element) + return { background: style.backgroundColor, color: style.color } + })).toEqual(expandHover) + expect(expandHover.background).not.toBe('rgba(0, 0, 0, 0)') + // Compact hover stays closed beyond the wide trigger's 150ms open deadline. + await page.waitForTimeout(200) + expect(await list.count()).toBe(0) + await trigger.click() + await list.waitFor() + await list.getByRole('button', { name: 'Finished 1', exact: true }).waitFor() + await trigger.click() + await list.waitFor({ state: 'detached' }) + await trigger.press('ArrowDown') + await list.waitFor() + expect(await list.getByRole('button').first().evaluate(element => element === document.activeElement)).toBe(true) + await page.mouse.move(2, 2) + // Keyboard activation remains pinned after leaving both trigger and menu. + await page.waitForTimeout(180) + expect(await list.count()).toBe(1) + await page.mouse.click(2, 2) + await list.waitFor({ state: 'detached' }) + expect(await trigger.getAttribute('aria-expanded')).toBe('false') + } finally { + await page.keyboard.press('Escape') + await page.setViewportSize(viewport) + await original.dispose() + } + }) + it('keeps its snapshot inventory closed', async () => { await assertFixtureInventory(SNAPSHOT_DIR, ['running.expected.md', 'settled.expected.md']) }) diff --git a/apps/web/tests/cua-optin.e2e.ts b/apps/web/tests/cua-optin.e2e.ts index 767af7ff10..77ec920da5 100644 --- a/apps/web/tests/cua-optin.e2e.ts +++ b/apps/web/tests/cua-optin.e2e.ts @@ -133,7 +133,7 @@ it.skipIf(process.platform !== 'darwin').each([ expect(scaffold.ctx.tools.schemas(originalAgent).some(tool => tool.name === 'cua')).toBe(preset === 'ptc') } const child = await scaffold.ctx.agents.create({ - sessionId: SessionId('cua-child'), parentAgent: ptcAgent, + sessionId: SessionId('cua-child'), setup: (ctx) => { scaffold.ctx.agentPresets.composeFrom(ctx, ptcAgent.ctx) }, }) expect(scaffold.ctx.tools.schemas(child.agent).some(tool => tool.name === 'cua')).toBe(true) diff --git a/apps/web/tests/default-model.e2e.ts b/apps/web/tests/default-model.e2e.ts index 03a22e3c41..afd89b3c77 100644 --- a/apps/web/tests/default-model.e2e.ts +++ b/apps/web/tests/default-model.e2e.ts @@ -170,7 +170,8 @@ describe('web e2e: the composer model switch is the default for later sessions', expect(popupBounds!.width).toBeCloseTo(commandMenuBounds!.width) expect(popupBounds!.x).toBeCloseTo(commandMenuBounds!.x) expect(await search.getAttribute('placeholder')).toBe('搜索模型…') - await page.getByRole('option').first().waitFor() + // Command menu height changes where the pointer lands in the model popup. + await page.getByRole('option', { name: 'Acme Large', exact: true }).hover() await compareOrRefreshGolden( fileURLToPath(new URL('./expected/default-model/command-picker.expected.md', import.meta.url)), await captureStableAria(page, '[aria-label="/model 选项"]', scaffold.workspaceCwd), diff --git a/apps/web/tests/default-product-isolation.e2e.ts b/apps/web/tests/default-product-isolation.e2e.ts index 4b535448e1..603a35753b 100644 --- a/apps/web/tests/default-product-isolation.e2e.ts +++ b/apps/web/tests/default-product-isolation.e2e.ts @@ -94,7 +94,7 @@ it('activates the actual default Client registry without experimental packages', expect(roster.plugins.some(plugin => plugin.modules.includes('@deepseek-ai/dsh-client-ui-layout'))).toBe(true) expect(experimentalClientReferences(roster)).toEqual([]) const contaminatedHost = await request('mount-experimental-entry') - const experimentalName = '@deepseek-ai/dsh-experimental-client-ui-agent-team' + const experimentalName = '@deepseek-ai/dsh-experimental-client-ui-voice-input' expect(contaminatedHost.client.entries.map(entry => entry.id)).toContain(experimentalName) await page.reload() await expect.poll(async () => { diff --git a/apps/web/tests/expected/math-rendering/ui.expected.md b/apps/web/tests/expected/math-rendering/ui.expected.md index f80e94775d..82c51f246e 100644 --- a/apps/web/tests/expected/math-rendering/ui.expected.md +++ b/apps/web/tests/expected/math-rendering/ui.expected.md @@ -19,7 +19,6 @@ - text: . - math: π 4 < θ < π 2 - math: θ ∈ ( π 4 , π 2 ) . (1) -- math: α + β γ - table: - rowgroup: - row "Symbol Value": diff --git a/apps/web/tests/expected/plugin-config/official.expected.md b/apps/web/tests/expected/plugin-config/official.expected.md index 1060826d8a..bdf3d7033a 100644 --- a/apps/web/tests/expected/plugin-config/official.expected.md +++ b/apps/web/tests/expected/plugin-config/official.expected.md @@ -22,11 +22,6 @@ - heading "实验性插件" [level=3] - button "更多" - list: - - listitem: - - button "查看 智能体团队": 智能体团队 - - text: 实验性 让与你对话的智能体把工作分给有名字的队友,在共享任务看板上跟踪进度,并汇总队友的结果向你汇报。 - - status - - switch "启用 智能体团队" - listitem: - button "查看 语音输入": 语音输入 - text: 实验性 在本机使用 SenseVoice 转写录音,首次使用需安装依赖。 diff --git a/apps/web/tests/expected/plugin-manager/icons.expected.md b/apps/web/tests/expected/plugin-manager/icons.expected.md index 4510accc6c..f42be11885 100644 --- a/apps/web/tests/expected/plugin-manager/icons.expected.md +++ b/apps/web/tests/expected/plugin-manager/icons.expected.md @@ -1,6 +1,6 @@ Third-party bundle card: image, 36×36, decoded -Disabled Agent Teams card: image, 36×36, decoded -Agent Teams detail: image, 36×36, decoded +Disabled Voice Input card: image, 36×36, decoded +Voice Input detail: image, 36×36, decoded Third-party bundle detail: image, 36×36, decoded Independent search row: image, 30×30, decoded Independent review row: generic artwork diff --git a/apps/web/tests/expected/plugin-manager/live-enabled.expected.md b/apps/web/tests/expected/plugin-manager/live-enabled.expected.md index 561ee7ae23..8a7e68999a 100644 --- a/apps/web/tests/expected/plugin-manager/live-enabled.expected.md +++ b/apps/web/tests/expected/plugin-manager/live-enabled.expected.md @@ -22,11 +22,6 @@ - heading "实验性插件" [level=3] - button "更多" - list: - - listitem: - - button "查看 智能体团队": 智能体团队 - - text: 实验性 让与你对话的智能体把工作分给有名字的队友,在共享任务看板上跟踪进度,并汇总队友的结果向你汇报。 - - status - - switch "启用 智能体团队" - listitem: - button "查看 语音输入": 语音输入 - text: 实验性 在本机使用 SenseVoice 转写录音,首次使用需安装依赖。 diff --git a/apps/web/tests/expected/plugin-manager/manager.expected.md b/apps/web/tests/expected/plugin-manager/manager.expected.md index 88dd772e51..1053a94651 100644 --- a/apps/web/tests/expected/plugin-manager/manager.expected.md +++ b/apps/web/tests/expected/plugin-manager/manager.expected.md @@ -22,11 +22,6 @@ - heading "实验性插件" [level=3] - button "更多" - list: - - listitem: - - button "查看 智能体团队": 智能体团队 - - text: 实验性 让与你对话的智能体把工作分给有名字的队友,在共享任务看板上跟踪进度,并汇总队友的结果向你汇报。 - - status - - switch "启用 智能体团队" - listitem: - button "查看 语音输入": 语音输入 - text: 实验性 在本机使用 SenseVoice 转写录音,首次使用需安装依赖。 diff --git a/apps/web/tests/expected/plugin-manager/refresh.expected.md b/apps/web/tests/expected/plugin-manager/refresh.expected.md index 85d666fd3e..75dda78845 100644 --- a/apps/web/tests/expected/plugin-manager/refresh.expected.md +++ b/apps/web/tests/expected/plugin-manager/refresh.expected.md @@ -6,7 +6,7 @@ Focus at 500ms: - tooltip "刷新" failure: pending - button "刷新" [disabled] -{"busy":"true","spinners":1,"cards":12,"skeletons":0,"alerts":[],"retry":0,"toasts":[]} +{"busy":"true","spinners":1,"cards":11,"skeletons":0,"alerts":[],"retry":0,"toasts":[]} failure: settled - button "刷新" -{"busy":"false","spinners":0,"cards":12,"skeletons":0,"alerts":[],"retry":0,"toasts":["刷新失败,请重试"]} +{"busy":"false","spinners":0,"cards":11,"skeletons":0,"alerts":[],"retry":0,"toasts":["刷新失败,请重试"]} diff --git a/apps/web/tests/expected/sidebar-subagent-activity/compact-running.expected.md b/apps/web/tests/expected/sidebar-subagent-activity/compact-running.expected.md new file mode 100644 index 0000000000..7a26f1bae7 --- /dev/null +++ b/apps/web/tests/expected/sidebar-subagent-activity/compact-running.expected.md @@ -0,0 +1,10 @@ +- banner: + - navigation "Session hierarchy": Delegate a background job. + - button "1 subagent" + - button "1 background job" + - button "More actions" + - button "Open right sidebar" + - tablist: + - tab "Chat" [selected] + - tab "Trajectory" + - button "More conversation views" diff --git a/apps/web/tests/expected/subagent-conversation/migration-required-child.expected.md b/apps/web/tests/expected/subagent-conversation/migration-required-child.expected.md index 0e4f8f018b..b60114ef0f 100644 --- a/apps/web/tests/expected/subagent-conversation/migration-required-child.expected.md +++ b/apps/web/tests/expected/subagent-conversation/migration-required-child.expected.md @@ -1,13 +1,13 @@ # Before opening the child - tree "Subagent sessions": - - treeitem "historical researcher Migration required · not running" [level=1]: - - text: historical researcher Migration required · not running + - treeitem "historical researcher migration-catalog-child · Migration required · not running" [level=1]: + - text: historical researcher migration-catalog-child Migration required - button "Open historical researcher in sidebar" # After opening the child - tree "Subagent sessions": - - treeitem "historical editor Migration required · not running" [level=1]: - - text: historical editor Migration required · not running + - treeitem "historical editor migration-catalog-grandchild · Migration required · not running" [level=1]: + - text: historical editor migration-catalog-grandchild Migration required - button "Open historical editor in sidebar" diff --git a/apps/web/tests/expected/subagent-conversation/navigation-only.expected.md b/apps/web/tests/expected/subagent-conversation/navigation-only.expected.md new file mode 100644 index 0000000000..76df6f6c57 --- /dev/null +++ b/apps/web/tests/expected/subagent-conversation/navigation-only.expected.md @@ -0,0 +1,8 @@ +- dialog "Subagent sessions": + - tree "Subagent sessions": + - treeitem "event-sourcing researcher {{uuid}} · continuable · not running 15.7K tok · {{duration}}" [level=1]: + - button "Expand event-sourcing researcher descendants" + - text: event-sourcing researcher {{uuid}} 15.7K tok {{duration}} + - treeitem "event-sourcing reviewer recorded-one-shot · one-shot · completed 0 tok · {{duration}}" [level=1]: + - text: event-sourcing reviewer recorded-one-shot 0 tok ~6mo 12d + - button "Open event-sourcing reviewer in sidebar" diff --git a/apps/web/tests/expected/subagent-result-notice/en.expected.md b/apps/web/tests/expected/subagent-result-notice/en.expected.md new file mode 100644 index 0000000000..a44d176b78 --- /dev/null +++ b/apps/web/tests/expected/subagent-result-notice/en.expected.md @@ -0,0 +1,41 @@ +- button "Subagent run completed {{clock}}" [expanded]: + - text: Subagent run completed + - time: {{clock}} +- paragraph: This notification triggered this response. +- text: Background subagent notice-result-child finished and will do no further work unless you send it more.Its closing message:## Research result The original child reply stays available. + +- button "Subagent run stopped {{clock}}" [expanded]: + - text: Subagent run stopped + - time: {{clock}} +- paragraph: This notification triggered this response. +- text: Background subagent notice-result-child was stopped before it finished.Its closing message:## Research result The original child reply stays available. + +- button "Subagent run failed {{clock}}" [expanded]: + - text: Subagent run failed + - time: {{clock}} +- paragraph: This notification triggered this response. +- text: Background subagent notice-result-child failed before it finished.Its closing message:## Research result The original child reply stays available. + +- button "Subagent reached the token limit {{clock}}" [expanded]: + - text: Subagent reached the token limit + - time: {{clock}} +- paragraph: This notification triggered this response. +- text: Background subagent notice-result-child ran out of room before it finished.Its closing message:## Research result The original child reply stays available. + +- button "Subagent declined the task {{clock}}" [expanded]: + - text: Subagent declined the task + - time: {{clock}} +- paragraph: This notification triggered this response. +- text: Background subagent notice-result-child declined the task.Its closing message:## Research result The original child reply stays available. + +- button "Subagent run completed {{clock}}" [expanded]: + - text: Subagent run completed + - time: {{clock}} +- paragraph: This notification triggered this response. +- text: Background subagent notice-result-child finished and will do no further work unless you send it more.It left no closing message. + +- button "Subtask status updated {{clock}}" [expanded]: + - text: Subtask status updated + - time: {{clock}} +- paragraph: This notification triggered this response. +- text: Background subagent finished. diff --git a/apps/web/tests/expected/subagent-result-notice/zh.expected.md b/apps/web/tests/expected/subagent-result-notice/zh.expected.md new file mode 100644 index 0000000000..d26df1f590 --- /dev/null +++ b/apps/web/tests/expected/subagent-result-notice/zh.expected.md @@ -0,0 +1,41 @@ +- button "子智能体已完成任务 {{clock}}" [expanded]: + - text: 子智能体已完成任务 + - time: {{clock}} +- paragraph: 这条通知触发了本轮回复。 +- text: Background subagent notice-result-child finished and will do no further work unless you send it more.Its closing message:## Research result The original child reply stays available. + +- button "子智能体任务已停止 {{clock}}" [expanded]: + - text: 子智能体任务已停止 + - time: {{clock}} +- paragraph: 这条通知触发了本轮回复。 +- text: Background subagent notice-result-child was stopped before it finished.Its closing message:## Research result The original child reply stays available. + +- button "子智能体执行失败 {{clock}}" [expanded]: + - text: 子智能体执行失败 + - time: {{clock}} +- paragraph: 这条通知触发了本轮回复。 +- text: Background subagent notice-result-child failed before it finished.Its closing message:## Research result The original child reply stays available. + +- button "子智能体已达 token 上限 {{clock}}" [expanded]: + - text: 子智能体已达 token 上限 + - time: {{clock}} +- paragraph: 这条通知触发了本轮回复。 +- text: Background subagent notice-result-child ran out of room before it finished.Its closing message:## Research result The original child reply stays available. + +- button "子智能体拒绝执行 {{clock}}" [expanded]: + - text: 子智能体拒绝执行 + - time: {{clock}} +- paragraph: 这条通知触发了本轮回复。 +- text: Background subagent notice-result-child declined the task.Its closing message:## Research result The original child reply stays available. + +- button "子智能体已完成任务 {{clock}}" [expanded]: + - text: 子智能体已完成任务 + - time: {{clock}} +- paragraph: 这条通知触发了本轮回复。 +- text: Background subagent notice-result-child finished and will do no further work unless you send it more.It left no closing message. + +- button "子任务状态更新 {{clock}}" [expanded]: + - text: 子任务状态更新 + - time: {{clock}} +- paragraph: 这条通知触发了本轮回复。 +- text: Background subagent finished. diff --git a/apps/web/tests/fold-blank-matrix.e2e.ts b/apps/web/tests/fold-blank-matrix.e2e.ts index c46a62c711..920534b6ca 100644 --- a/apps/web/tests/fold-blank-matrix.e2e.ts +++ b/apps/web/tests/fold-blank-matrix.e2e.ts @@ -623,8 +623,10 @@ async function prepareWorld(browser: Browser, scenario: Case, scaffold: WebScaff const trigger = page.getByRole('button', { name: '1 subagent', exact: true }) await trigger.waitFor({ timeout: 15_000 }) await trigger.hover() - await page.getByRole('treeitem', { name: new RegExp(CHILD_LABEL) }) - .getByRole('button', { name: `Open ${CHILD_LABEL} in sidebar`, exact: true }).click() + const openAside = page.getByRole('treeitem', { name: new RegExp(CHILD_LABEL) }) + .getByRole('button', { name: /^Open .+ in sidebar$/u }) + await expect.poll(() => openAside.count()).toBe(1) + await openAside.click() await page.locator(SIDE_ROOT).getByText('FOLDMX_BODY', { exact: false }).last().waitFor({ timeout: 30_000 }) await page.mouse.move(0, 0) return { diff --git a/apps/web/tests/jobs-menu-layering.e2e.ts b/apps/web/tests/jobs-menu-layering.e2e.ts index 7388c9f4b3..ca2f53e227 100644 --- a/apps/web/tests/jobs-menu-layering.e2e.ts +++ b/apps/web/tests/jobs-menu-layering.e2e.ts @@ -28,7 +28,7 @@ const COMMAND = `printf 'jobs-menu-%s\\n' output; for _ in $(seq 1 3000); do [ - function placement(page: Page) { return page.evaluate(() => { const list = document.querySelector('ul[aria-label="Background jobs"]') - const anchor = document.querySelector('button[aria-label="1 background job running"]') + const anchor = document.querySelector('button[aria-label="1 background job"]') const frame = document.querySelector('[data-sidebar-browser-frame]') const dock = document.querySelector('[data-sidebar-right-panel][data-sidebar-right-open]') if (list === null || anchor === null || frame === null || dock === null) { @@ -105,6 +105,7 @@ describe.skipIf(MODE === 'record')('web e2e: jobs menu layering', () => { it('keeps the list and live output above the Browser dock while anchored and dismissable', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-jobs-menu-layering')) + const initialViewport = page.viewportSize()! const session = page.locator(`[data-row-key="session:${SEED_ID}"]`) await expect.poll(async () => { if (await session.isVisible()) return true @@ -148,7 +149,7 @@ describe.skipIf(MODE === 'record')('web e2e: jobs menu layering', () => { await page.frameLocator('[data-sidebar-browser-frame]') .getByRole('heading', { name: 'Jobs menu browser fixture' }).waitFor() - const trigger = page.getByRole('button', { name: '1 background job running', exact: true }) + const trigger = page.getByRole('button', { name: '1 background job', exact: true }) await trigger.waitFor({ timeout: 15_000 }) await trigger.focus() await page.keyboard.press('Enter') @@ -184,6 +185,13 @@ describe.skipIf(MODE === 'record')('web e2e: jobs menu layering', () => { await list.waitFor({ state: 'detached' }) const outsideDismisses = await trigger.getAttribute('aria-expanded') === 'false' expect(outsideDismisses).toBe(true) + await page.setViewportSize(initialViewport) + await trigger.hover() + await list.waitFor() + expect(await address.evaluate(element => element === document.activeElement)).toBe(true) + await page.keyboard.press('Escape') + await list.waitFor({ state: 'detached' }) + expect(await address.evaluate(element => element === document.activeElement)).toBe(true) expect(scaffold.ctx.jobs.get(jobId, agent.id).status).toBe('running') expect(tripwire.pageErrors).toEqual([]) expect(tripwire.warnings).toEqual([]) diff --git a/apps/web/tests/live-job-stream.e2e.ts b/apps/web/tests/live-job-stream.e2e.ts index 4bffdb8a62..b51d55ba99 100644 --- a/apps/web/tests/live-job-stream.e2e.ts +++ b/apps/web/tests/live-job-stream.e2e.ts @@ -85,7 +85,7 @@ describe.skipIf(MODE === 'record')('web e2e: live job stream', () => { it('streams a running job\'s real output into the expanded panel, then settles it in place on kill', async () => { let phase = 'streaming' onTestFailed(() => saveFailureShot(page, `web-e2e-live-job-${phase}`)) - const trigger = page.getByRole('button', { name: '1 background job running' }) + const trigger = page.getByRole('button', { name: '1 background job', exact: true }) expect(await trigger.count()).toBe(0) const started = await executeToolFixture(scaffold.ctx, { @@ -99,6 +99,14 @@ describe.skipIf(MODE === 'record')('web e2e: live job stream', () => { const matched = /\bbash-[0-9a-f]{16}\b/.exec(reported) if (matched === null) throw new Error(`background bash reported no job id: ${reported}`) const jobId = JobId(matched[0]) + await expect.poll(() => { + const job = scaffold.ctx.jobs.get(jobId, agent.id) + const output = scaffold.ctx.jobs.readAt(jobId, 0, agent.id).chunks.map(chunk => chunk.text).join('') + return job.status !== 'running' || output.includes('streamed-marker-line') + }, { timeout: 15_000 }).toBe(true) + const readyJob = scaffold.ctx.jobs.get(jobId, agent.id) + const readyOutput = scaffold.ctx.jobs.readAt(jobId, 0, agent.id).chunks.map(chunk => chunk.text).join('') + expect(readyJob.status, `${readyJob.detail ?? ''}\n${readyOutput}`).toBe('running') // The job row reaches the header over the job roster stream. await trigger.waitFor({ timeout: 15_000 }) @@ -116,13 +124,24 @@ describe.skipIf(MODE === 'record')('web e2e: live job stream', () => { expect(tripwire.pageErrors).toEqual([]) expect(tripwire.warnings).toEqual([]) + await trigger.click() + await page.getByRole('list', { name: 'Background jobs' }).waitFor({ state: 'detached' }) + expect(await page.getByText('streamed-marker-line', { exact: true }).count()).toBe(0) + await page.mouse.move(2, 2) + await trigger.hover() + await page.getByRole('list', { name: 'Background jobs' }).waitFor() + await page.getByRole('button', { name: /Hide live output/ }).waitFor() + await page.getByText('streamed-marker-line', { exact: true }).waitFor() + expect(scaffold.ctx.jobs.get(jobId, agent.id).status).toBe('running') + phase = 'settled' expect(scaffold.ctx.jobs.kill(jobId, agent.id, 'web e2e cancellation')).toBe('requested') - // Settlement arrives on the observation stream itself, so the open panel - // flips without any further interaction; the roster trigger follows. - const idle = page.getByRole('button', { name: '1 background job', exact: true }) - await idle.waitFor({ timeout: 20_000 }) + // The unchanged count label is not a settlement signal; the output row owns that state. + await page.getByRole('button', { name: 'Finished 1', exact: true }).waitFor({ timeout: 20_000 }) + await expect.poll(() => page.getByRole('list', { name: 'Background jobs' }).textContent()).toContain('web e2e cancellation') + expect(await trigger.getAttribute('aria-label')).toBe('1 background job') + expect(scaffold.ctx.jobs.get(jobId, agent.id).status).toBe('killed') await page.getByText('streamed-marker-line').waitFor({ timeout: 10_000 }) const settled = await captureStableAria(page, '[class*="menu"]', scaffold.workspaceCwd) diff --git a/apps/web/tests/math-rendering.e2e.ts b/apps/web/tests/math-rendering.e2e.ts index 8612498522..7f31d0a3fe 100644 --- a/apps/web/tests/math-rendering.e2e.ts +++ b/apps/web/tests/math-rendering.e2e.ts @@ -56,9 +56,6 @@ function mathFixture(): string { '', '$$\\theta \\in \\left(\\frac{\\pi}{4}, \\frac{\\pi}{2}\\right). \\tag{1}$$', '', - '$$\\alpha+\\beta', - '\\gamma$$', - '', '| Symbol | Value |', '| --- | --- |', '| $\\theta$ | \\(\\frac{1}{5}\\) |', @@ -105,6 +102,13 @@ describe('web e2e: settled Markdown math rendering', () => { tripwire = watchConsole(page) await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) + const groupRow = page.locator('[role="treeitem"]').first() + await groupRow.waitFor({ timeout: 15_000 }) + await groupRow.click() + const sessionRow = page.locator('[role="treeitem"]').nth(1) + await sessionRow.waitFor({ timeout: 10_000 }) + await sessionRow.click() + await page.getByText(DONE, { exact: true }).waitFor({ timeout: 15_000 }) }, 120_000) afterAll(async () => { @@ -114,16 +118,8 @@ describe('web e2e: settled Markdown math rendering', () => { it.skipIf(MODE === 'record')('renders the settled reply without KaTeX errors', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-math-rendering')) - const groupRow = page.locator('[role="treeitem"]').first() - await groupRow.waitFor({ timeout: 15_000 }) - await groupRow.click() - const sessionRow = page.locator('[role="treeitem"]').nth(1) - await sessionRow.waitFor({ timeout: 10_000 }) - await sessionRow.click() - await expect.poll(() => page.getByText(DONE, { exact: true }).count(), { timeout: 15_000 }).toBe(1) - - await expect.poll(() => page.locator('.katex').count(), { timeout: 10_000 }).toBe(7) - await expect.poll(() => page.locator('.katex-display').count(), { timeout: 10_000 }).toBe(3) + await expect.poll(() => page.locator('.katex').count(), { timeout: 10_000 }).toBe(6) + await expect.poll(() => page.locator('.katex-display').count(), { timeout: 10_000 }).toBe(2) expect(await page.locator('.katex-error').count()).toBe(0) const fonts = await page.evaluate(async () => { const faces = [...document.fonts].filter(face => face.family.includes('KaTeX_')) @@ -144,4 +140,27 @@ describe('web e2e: settled Markdown math rendering', () => { expect(tripwire.warnings).toEqual([]) await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md']) }, 60_000) + + it.skipIf(MODE === 'record')('copies a selection with formulas as TeX source', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-math-copy')) + await page.context().grantPermissions(['clipboard-read', 'clipboard-write'], { origin: new URL(page.url()).origin }) + const sentinel = 'clipboard-before-copy' + await page.evaluate(value => navigator.clipboard.writeText(value), sentinel) + await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toBe(sentinel) + await page.evaluate(() => { + const paragraph = [...document.querySelectorAll('p')].find(item => item.textContent.startsWith('Inline dollar'))! + const visual = paragraph.parentElement!.querySelector('.katex-display .katex-html')! + const range = document.createRange() + range.setStart(paragraph.firstChild!, 0) + range.setEnd(visual.firstChild!, 1) + document.getSelection()!.removeAllRanges() + document.getSelection()!.addRange(range) + }) + await page.keyboard.press('ControlOrMeta+C') + await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toBe( + 'Inline dollar $\\theta$ and backslash $\\frac{1}{5}$.\n\n$$\\frac{\\pi}{4} < \\theta < \\frac{\\pi}{2}$$', + ) + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + }, 30_000) }) diff --git a/apps/web/tests/minimal-preset.snapshot.ts b/apps/web/tests/minimal-preset.snapshot.ts index ee65497459..8a131ed29b 100644 --- a/apps/web/tests/minimal-preset.snapshot.ts +++ b/apps/web/tests/minimal-preset.snapshot.ts @@ -142,8 +142,11 @@ describe('minimal agent preset', () => { ], } `) + const requestedTools = new Set(requestHeader.tools?.map(tool => tool.name)) expect(requestHeader.tools?.toSorted((left, right) => left.name.localeCompare(right.name))) - .toEqual(scaffold.ctx.tools.schemas(agentHandle.agent).toSorted((left, right) => left.name.localeCompare(right.name))) + .toEqual(scaffold.ctx.tools.schemas(agentHandle.agent) + .filter(tool => requestedTools.has(tool.name)) + .toSorted((left, right) => left.name.localeCompare(right.name))) }) it.skipIf(MODE === 'record')('expands the completed persistent Bash call in the Web conversation', async () => { diff --git a/apps/web/tests/notice-wake-tail.e2e.ts b/apps/web/tests/notice-wake-tail.e2e.ts index 884adcb9d3..a45a5b85d7 100644 --- a/apps/web/tests/notice-wake-tail.e2e.ts +++ b/apps/web/tests/notice-wake-tail.e2e.ts @@ -170,6 +170,7 @@ describe('web e2e: notice Turn after a long Detailed Turn', () => { kind: 'subagent-settled', form: 'notice', summary: 'Background subagent finished.', + stopReason: 'completed', senderSessionId: SessionId('notice-wake-child'), } const notice = createUserMessage({ diff --git a/apps/web/tests/official-subagent-bundle.e2e.ts b/apps/web/tests/official-subagent-bundle.e2e.ts index 8a21671b29..ef6268cd42 100644 --- a/apps/web/tests/official-subagent-bundle.e2e.ts +++ b/apps/web/tests/official-subagent-bundle.e2e.ts @@ -7,12 +7,13 @@ import { fileURLToPath } from 'node:url' import { chromium, type Browser, type Page } from 'playwright' import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' import { startMockLlmServer, type MockLlmServer } from '@deepseek-ai/dsh-llm-mock-server' +import { parseSessionHeader } from '@deepseek-ai/dsh-llm-replay' import type {} from '@deepseek-ai/dsh-plugin-manager' import type {} from '@deepseek-ai/dsh-tools' import type {} from '@deepseek-ai/dsh-subagent' import { assertFinalWorkspaceSnapshot, assertFixtureInventory, captureExpandedTurnProcessAria, compareOrRefreshGolden, - fixtureUserPrompts, launchWebScaffold, selectedSessionFixture, watchConsole, webSnapshotMode, type WebScaffold, + fixtureUserPrompts, launchWebScaffold, seedSession, selectedSessionFixture, watchConsole, webSnapshotMode, type WebScaffold, } from './scaffold.ts' import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts' @@ -137,6 +138,14 @@ describe.skipIf(MODE === 'record')('web snapshot: Official native subagent bundl const sessionId = await settled const agent = scaffold.ctx.agents.get(sessionId) if (agent === undefined) throw new Error('Native delegation Session has no live Agent') + const firstTurnEvents = agent.session.snapshotEvents() + const toolResults = firstTurnEvents.filter(event => event.type === 'tool/result').map(event => event.data) + expect(toolResults, `PTC execution must start the native child before waiting for its result: ${JSON.stringify(toolResults)}`) + .toMatchObject([{ message: { isError: false } }]) + const dispatches = firstTurnEvents.filter(event => event.type === 'tool/ptc-dispatch').map(event => event.data) + expect(dispatches).toMatchObject([{ name: 'subagent_claude_code', isError: false }]) + const catalog = firstTurnEvents.filter(event => event.type === 'subagent/catalog').map(event => event.data) + expect(catalog).toMatchObject([{ mode: 'external', label: 'Read the fixture response' }]) expect(server.requests).toHaveLength(0) const completed = scaffold.whenTurnSettled(120_000) nativeEndpoint.release() @@ -148,10 +157,6 @@ describe.skipIf(MODE === 'record')('web snapshot: Official native subagent bundl expect(system).toContain('subagent_claude_code') const events = agent.session.snapshotEvents() expect(events.filter(event => event.type === 'tool/call').map(event => event.data.name)).toEqual(['run_code']) - const dispatches = events.filter(event => event.type === 'tool/ptc-dispatch').map(event => event.data) - expect(dispatches).toMatchObject([{ name: 'subagent_claude_code', isError: false }]) - const catalog = events.filter(event => event.type === 'subagent/catalog').map(event => event.data) - expect(catalog).toMatchObject([{ mode: 'external', label: 'Read the fixture response' }]) const notice = events.filter(event => event.type === 'user/message').find(event => event.data.source.kind === 'subagent-settled') if (notice?.data.source.kind !== 'subagent-settled') throw new Error('Native completion was not delivered to the parent') expect(notice.data.source.senderSessionId).toBe(catalog[0]?.childId) @@ -178,3 +183,31 @@ describe.skipIf(MODE === 'record')('web snapshot: Official native subagent bundl ]) }) }) + +it.skipIf(MODE === 'record')('renders recorded official notice titles after reopening the Session', async () => { + const scaffold = await launchWebScaffold({ developerTools: true }) + let browser: Browser | undefined + try { + const fixture = await readFile(await selectedSessionFixture(FIXTURE), 'utf8') + await seedSession(scaffold, fixture, 'official-subagent-recorded-ui') + browser = await chromium.launch() + const page = await newEnglishPage(browser) + await page.clock.setFixedTime(parseSessionHeader(fixture).createdAt) + const tripwire = watchConsole(page) + await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) + const workspace = page.locator('[role="treeitem"]').first() + await workspace.waitFor() + await workspace.click() + const session = page.locator('[role="treeitem"]').nth(1) + await session.waitFor() + await session.click() + const heading = page.locator('[data-turn-trigger] > button > span').nth(1) + await expect.poll(() => heading.textContent()).toBe('Subagent run completed') + await compareOrRefreshGolden(join(DIR, 'ui.expected.md'), + await captureExpandedTurnProcessAria(page, '[data-chat-flow]', scaffold.workspaceCwd), MODE) + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + } finally { + try { await browser?.close() } finally { await scaffold.close() } + } +}) diff --git a/apps/web/tests/plugin-config.e2e.ts b/apps/web/tests/plugin-config.e2e.ts index 8e78f856eb..8bba474e11 100644 --- a/apps/web/tests/plugin-config.e2e.ts +++ b/apps/web/tests/plugin-config.e2e.ts @@ -23,6 +23,7 @@ const ROW_EXPECTED = join(SNAPSHOT_DIR, 'row.expected.md') const BUNDLE_EXPECTED = join(SNAPSHOT_DIR, 'bundle.expected.md') const FIXTURE_PLUGINS = fileURLToPath(new URL('./fixtures/plugins', import.meta.url)) const MODE = webSnapshotMode() +const UNUSED_KEY_ENV = 'DSH_PLUGIN_CONFIG_E2E_UNUSED_KEY' describe('web e2e: plugin configuration pages', () => { let scaffold: WebScaffold @@ -35,7 +36,8 @@ describe('web e2e: plugin configuration pages', () => { // below, that half registers its row's configuration into the page. scaffold = await launchWebScaffold({ extraOverlayPath: fileURLToPath(new URL('./pin-browse-picker.overlay.yml', import.meta.url)), - deepSeekSearch: { apiKeyEnv: 'DSH_PLUGIN_CONFIG_E2E_UNUSED_KEY' }, + deepSeekSearch: { apiKeyEnv: UNUSED_KEY_ENV }, + deepSeekWebApi: { apiKeyEnv: UNUSED_KEY_ENV }, profile: { packages: [{ dir: join(FIXTURE_PLUGINS, 'fixture-live-client') }] }, }) browser = await chromium.launch() diff --git a/apps/web/tests/plugin-manager.e2e.ts b/apps/web/tests/plugin-manager.e2e.ts index e4c27d71bc..9bb7f1cfdd 100644 --- a/apps/web/tests/plugin-manager.e2e.ts +++ b/apps/web/tests/plugin-manager.e2e.ts @@ -9,7 +9,6 @@ import type { Browser, Locator, Page } from 'playwright' import { chromium } from 'playwright' import { FiberState } from '@deepseek-ai/cordis' import { readProfileManifest } from '@deepseek-ai/dsh-app-boot' -import { createUserMessage } from '@deepseek-ai/dsh-llm' import { afterAll, beforeAll, describe, expect, it, onTestFailed, onTestFinished, vi } from 'vitest' import { join } from 'node:path' import { @@ -872,7 +871,6 @@ describe('web e2e: plugin manager', () => { await card.getAttribute('data-plugin-package'), await card.getByText('实验性', { exact: true }).count(), ]))).toEqual([ - ['@deepseek-ai/dsh-experimental-agent-team-profile', 1], ['@deepseek-ai/dsh-experimental-voice-input-bundle', 1], ['@deepseek-ai/dsh-experimental-plugin-market-bundle', 1], ['@deepseek-ai/dsh-experimental-auto-review', 1], @@ -977,7 +975,7 @@ describe('web e2e: plugin manager', () => { onTestFinished(closeSettings) const fixtureIcon = `data:image/svg+xml;base64,${(await readFile(join(FIXTURE_PLUGINS, 'fixture-bundle/icon.svg'))).toString('base64')}` const fallbackIcon = `data:image/svg+xml;base64,${(await readFile(join(FIXTURE_PLUGINS, 'fixture-bundle/fallback-icon.svg'))).toString('base64')}` - const teamIcon = `data:image/svg+xml;base64,${(await readFile(fileURLToPath(new URL('../../../packages/experimental/agent-team-profile/icon.svg', import.meta.url)))).toString('base64')}` + const voiceIcon = `data:image/svg+xml;base64,${(await readFile(fileURLToPath(new URL('../../../packages/experimental/voice-input-bundle/icon.svg', import.meta.url)))).toString('base64')}` const images: string[] = [] const checkImage = async (selector: string, source: string, label: string) => { const image = panel.locator(`${selector} img`).first() @@ -991,9 +989,9 @@ describe('web e2e: plugin manager', () => { images.push(`${label}: image, ${size.width}×${size.height}, decoded`) } await checkImage('[data-plugin-package="@fixture/bundle"]', fixtureIcon, 'Third-party bundle card') - const team = panel.locator('[data-plugin-package="@deepseek-ai/dsh-experimental-agent-team-profile"]') + const team = panel.locator('[data-plugin-package="@deepseek-ai/dsh-experimental-voice-input-bundle"]') expect(await team.getByRole('switch').getAttribute('aria-checked')).toBe('false') - await checkImage('[data-plugin-package="@deepseek-ai/dsh-experimental-agent-team-profile"]', teamIcon, 'Disabled Agent Teams card') + await checkImage('[data-plugin-package="@deepseek-ai/dsh-experimental-voice-input-bundle"]', voiceIcon, 'Disabled Voice Input card') try { for (const colorScheme of ['light', 'dark'] as const) { await page.emulateMedia({ colorScheme }) @@ -1007,8 +1005,8 @@ describe('web e2e: plugin manager', () => { } finally { await page.emulateMedia({ colorScheme: null }) } - await panel.getByRole('button', { name: '查看 智能体团队', exact: true }).click() - await checkImage('[data-plugin-detail]', teamIcon, 'Agent Teams detail') + await panel.getByRole('button', { name: '查看 语音输入', exact: true }).click() + await checkImage('[data-plugin-detail]', voiceIcon, 'Voice Input detail') await panel.getByRole('button', { name: '返回插件' }).click() await panel.getByRole('button', { name: '查看 @fixture/bundle', exact: true }).click() await checkImage('[data-plugin-detail]', fixtureIcon, 'Third-party bundle detail') @@ -1108,23 +1106,18 @@ describe('web e2e: plugin manager', () => { onTestFinished(closeSettings) onTestFailed(() => saveFailureShot(page, 'web-e2e-plugin-manager-locale')) const panel = await openPluginsPanel() - await panel.getByRole('button', { name: '查看 智能体团队', exact: true }).click() + await panel.getByRole('button', { name: '查看 语音输入', exact: true }).click() const packageName = panel.locator('[data-plugin-name]') - expect(await packageName.textContent()).toBe('@deepseek-ai/dsh-experimental-agent-team-profile') - expect(await panel.getByText(experimentalBundleDescription('agent-team-profile', 'zh')).count()).toBe(1) - const child = panel.locator('[data-plugin-row]', { hasText: 'tool-agent-team' }) - await child.getByText('团队工具', { exact: true }).waitFor() - expect(await child.getByText('为智能体提供成员协调、消息通信和共享任务管理工具。', { exact: true }).count()).toBe(1) + expect(await packageName.textContent()).toBe('@deepseek-ai/dsh-experimental-voice-input-bundle') + expect(await panel.getByText('在本机使用 SenseVoice 转写录音,首次使用需安装依赖。').count()).toBe(1) try { await setLanguage('en') - await panel.getByRole('heading', { name: 'Agent teams', exact: true }).waitFor() - expect(await packageName.textContent()).toBe('@deepseek-ai/dsh-experimental-agent-team-profile') - expect(await panel.getByText(experimentalBundleDescription('agent-team-profile', 'en')).count()).toBe(1) - await child.getByText('Team tools', { exact: true }).waitFor() - expect(await child.getByText('Give agents tools to coordinate members, exchange messages, and manage shared tasks.', { exact: true }).count()).toBe(1) + await panel.getByRole('heading', { name: 'Voice input', exact: true }).waitFor() + expect(await packageName.textContent()).toBe('@deepseek-ai/dsh-experimental-voice-input-bundle') + expect(await panel.getByText('Transcribe recordings locally with SenseVoice; first use requires installing dependencies').count()).toBe(1) await panel.getByRole('button', { name: 'Back to plugins' }).click() - await panel.getByRole('button', { name: 'View Agent teams', exact: true }).waitFor() - expect(await panel.getByRole('switch', { name: 'Enable Agent teams', exact: true }).count()).toBe(1) + await panel.getByRole('button', { name: 'View Voice input', exact: true }).waitFor() + expect(await panel.getByRole('switch', { name: 'Enable Voice input', exact: true }).count()).toBe(1) // Schedule ships in the delivered composition, so no bundle card carries it in either language. expect(await panel.getByRole('button', { name: 'View Automation tasks', exact: true }).count()).toBe(0) // The official configuration pages follow the language too, from their own dictionary. @@ -1134,69 +1127,35 @@ describe('web e2e: plugin manager', () => { } finally { await setLanguage('zh') } - await panel.getByRole('button', { name: '查看 智能体团队', exact: true }).waitFor() + await panel.getByRole('button', { name: '查看 语音输入', exact: true }).waitFor() expect(tripwire.pageErrors).toEqual([]) }, 60_000) - it('enables the Team tools and browser plugin with one bundle switch', async () => { + it('provides Team mode per Session without an experimental plugin switch', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-plugin-manager-team')) + onTestFinished(closeSettings) const panel = await openPluginsPanel() - const toggle = panel.getByRole('switch', { name: '启用 智能体团队', exact: true }) - const teamRows = () => [...scaffold.ctx.loader.entries()] - .filter(entry => ['agent-team', 'tool-agent-team', 'ui-agent-team'].includes(entry.options.id)) + expect(await panel.getByRole('switch', { name: '启用 智能体团队', exact: true }).count()).toBe(0) + expect(await panel.locator('[data-plugin-package="@deepseek-ai/dsh-experimental-agent-team-profile"]').count()).toBe(0) + expect([...scaffold.ctx.loader.entries()].filter(entry => + ['agent-team', 'tool-agent-team', 'ui-agent-team'].includes(entry.options.id) + && entry.fiber?.state === FiberState.ACTIVE)).toHaveLength(3) const teamPage = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE }) const teamTripwire = watchConsole(teamPage) try { await teamPage.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) await connectFreshWorkspaceZh(teamPage, scaffold.workspaceCwd) - const agent = scaffold.ctx.agents.list()[0] - if (agent === undefined) throw new Error('connected Team workspace did not create an Agent') - // Session actions render only after the conversation leaves its blank state. - agent.session.append('turn/start', { turn: 1 }) - agent.session.append('user/message', createUserMessage({ - content: [{ type: 'text', text: 'Team UI lifecycle' }], source: { kind: 'user' }, - }), { surfaceOp: 'append' }) - agent.session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) - await scaffold.ctx.sessions.flush(agent.session) - // The current crumb renders as plain text inside the zh-labeled hierarchy nav. - await teamPage.getByRole('navigation', { name: '会话层级' }) - .getByText('Team UI lifecycle', { exact: true }).waitFor() - const action = teamPage.locator('[data-team-action]') - expect(await action.count()).toBe(0) - await toggle.click() - try { - await expect.poll(() => teamRows().filter(entry => entry.fiber?.state === FiberState.ACTIVE).length, { timeout: 20_000 }).toBe(3) - await expect.poll(() => toggle.getAttribute('aria-checked'), { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe('true') - await action.waitFor({ timeout: 20_000 }) - await action.getByRole('button', { name: '智能体团队', exact: true }).click() - const teamPanel = teamPage.getByRole('dialog', { name: '智能体团队', exact: true }) - await teamPanel.getByText('Team 暂不可用', { exact: true }).waitFor() - await teamPage.reload({ waitUntil: 'load' }) - await action.getByRole('button', { name: '智能体团队', exact: true }).click() - await teamPanel.getByText('暂无共享任务,可以通过对话创建').waitFor() - await teamPanel.getByText('lead', { exact: true }).waitFor() - const manifest = JSON.parse(await homeFile('profiles', 'scaffold', 'package.json')) as { - dsh: { profile: { bundles: string[] } } - } - expect(manifest.dsh.profile.bundles).toEqual([...SCAFFOLD_BUNDLES, '@deepseek-ai/dsh-experimental-agent-team-profile']) - await panel.getByRole('button', { name: '查看 智能体团队', exact: true }).click() - for (const id of ['agent-team', 'tool-agent-team', 'ui-agent-team']) { - await panel.locator('[data-plugin-row]', { hasText: id }).first().waitFor() - } - await panel.getByRole('button', { name: '返回插件' }).click() - } finally { - const back = panel.getByRole('button', { name: '返回插件' }) - if (await back.count() > 0) await back.click() - if (await toggle.getAttribute('aria-checked') === 'true') await toggle.click() - await expect.poll(() => teamRows().filter(entry => entry.fiber?.state === FiberState.ACTIVE).length, { timeout: 20_000 }).toBe(0) - await expect.poll(() => action.count(), { timeout: 20_000 }).toBe(0) - } + await teamPage.getByRole('button', { name: /^选择模型/u }).click() + const mode = teamPage.getByRole('switch', { name: '启用智能体团队协作', exact: true }) + await mode.waitFor() + expect(await mode.getAttribute('aria-checked')).toBe('false') + await mode.click() + await expect.poll(() => mode.getAttribute('aria-checked')).toBe('true') expect(teamTripwire.pageErrors).toEqual([]) expect(teamTripwire.warnings).toEqual([]) } finally { await teamPage.close() } - expect(tripwire.pageErrors).toEqual([]) }, 60_000) it('mounts the delivered Automation tasks rows without a bundle switch', async () => { diff --git a/apps/web/tests/preset-migration.snapshot.ts b/apps/web/tests/preset-migration.snapshot.ts index 302aea3fdc..cf2aac31b8 100644 --- a/apps/web/tests/preset-migration.snapshot.ts +++ b/apps/web/tests/preset-migration.snapshot.ts @@ -76,10 +76,13 @@ describe.skipIf(webSnapshotMode() === 'record')('historical preset restoration t expect(resolved.agent.session.snapshotEvents() .filter(event => event.type === 'agent-preset/selected') .map(event => event.data.agentPreset)).toEqual(withSelections ? ['ptc', 'standard', 'ptc'] : []) - expect(await scaffold.ctx.subagents.listChildren(id)).toEqual([ - { id: brokenId, createdAt: childFixture[0]?.['createdAt'], mode: 'unknown' }, - { id: childId, createdAt: childFixture[0]?.['createdAt'], mode: 'one-shot', label: 'historical child' }, - ]) + { + using observed = await scaffold.ctx.sessionQuery.observeSession(id) + expect(observed.projections?.values.subagentCatalog).toEqual([ + { id: brokenId, createdAt: childFixture[0]?.['createdAt'], mode: 'unknown' }, + { id: childId, createdAt: childFixture[0]?.['createdAt'], mode: 'one-shot', label: 'historical child' }, + ]) + } const childSuccessor = generationLogPath(scaffold.persistenceRoot, scaffold.workspaceCwd, childId, SESSION_FORMAT_VERSION, 'zstd') await expect(readFile(childSuccessor)).rejects.toMatchObject({ code: 'ENOENT' }) diff --git a/apps/web/tests/scaffold.ts b/apps/web/tests/scaffold.ts index 6a0c089a9a..92f307c095 100644 --- a/apps/web/tests/scaffold.ts +++ b/apps/web/tests/scaffold.ts @@ -412,8 +412,8 @@ export interface LaunchOptions { * the settings switch does. */ deepSeekWebApi?: { - /** Search and Fetch API base URL; the providers append `/search` or `/fetch`. */ - baseURL: string + /** Search and Fetch API base URL; omitted uses the provider default. The providers append `/search` or `/fetch`. */ + baseURL?: string /** Credential reference resolved by both providers. */ apiKeyEnv: string /** Select `deepseek-search-api` and `deepseek-fetch-api` in place of the shipped selection. */ @@ -699,7 +699,13 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise { } expect(await detail.getByRole('tab', { name: 'Records', exact: true }).getAttribute('aria-selected')).toBe('true') await detail.getByRole('tab', { name: 'Rules', exact: true }).click() - await detail.getByRole('button', { name: 'Edit', exact: true }).click() await detail.getByRole('button', { name: /^Repeat / }).click() await page.getByRole('menuitem', { name: 'Every N hours', exact: true }).click() const interval = detail.getByLabel('Interval', { exact: true }) @@ -1251,9 +1250,8 @@ describe.skipIf(MODE === 'record')('web e2e: active Schedule catalog', () => { expect(await row.textContent()).toContain(frequency) await row.click() const detail = manager.getByRole('complementary', { name: 'Task details' }) - expect(await detail.getByText(frequency, { exact: true }).count()).toBe(1) + expect(await detail.getByRole('button', { name: /^Repeat / }).textContent()).toContain('Every day') expect(await detail.getByText('Once', { exact: true }).count()).toBe(0) - await detail.getByRole('button', { name: 'Edit', exact: true }).click() const runTime = detail.getByRole('region', { name: 'Run time', exact: true }) const timeField = runTime.getByRole('button', { name: 'Time', exact: true }) // The Time zone row is a menu trigger button; the zone is chosen, never typed. @@ -1281,7 +1279,7 @@ describe.skipIf(MODE === 'record')('web e2e: active Schedule catalog', () => { }) expect(rruleOf(saved)).toContain('BYHOUR=12;BYMINUTE=34') // A stored zone equal to the browser zone is omitted from the frequency line. - await expect.poll(() => detail.getByText('Daily at 12:34', { exact: true }).count(), { timeout: 15_000 }).toBe(1) + await expect.poll(() => row.textContent(), { timeout: 15_000 }).toContain('Daily at 12:34') expect(await scaffold.ctx.schedule.history({ id: created.id, limit: 20 })) .toEqual({ id: created.id, records: [], earlierRecordsUnavailable: false, earlierRecordsPruned: false, retention: { days: 30, records: 200 } }) @@ -1296,6 +1294,39 @@ describe.skipIf(MODE === 'record')('web e2e: active Schedule catalog', () => { } }, 60_000) + it('restores the latest linked Session after reviewing a concurrent destination change', async () => { + const before = SessionId('schedule-destination-before') + const after = SessionId('schedule-destination-after') + const fixture = await readFile(CATALOG_FIXTURE, 'utf8') + for (const id of [before, after]) await seedSession(scaffold, fixture, id, 'standard') + const created = await scaffold.ctx.schedule.create({ title: 'Destination recovery', prompt: 'Review the destination', + timing: { kind: 'at', at: '2099-09-01T12:00:00.000Z' }, target: { kind: 'session', sessionId: before } }) + try { + await page.getByRole('button', { name: 'Automation tasks', exact: true }).click() + const manager = page.getByTestId('task-manager-page') + await manager.getByRole('searchbox', { name: 'Search tasks' }).fill('') + await manager.getByRole('button', { name: 'All', exact: true }).click() + await manager.getByRole('button', { name: 'Destination recovery', exact: true }).click() + const detail = manager.getByRole('complementary') + await detail.getByRole('textbox', { name: 'Automation task name', exact: true }).fill('Recovered destination') + await detail.getByRole('button', { name: /^Run in / }).click() + await page.getByRole('menuitem', { name: /^Create a session and reuse it/ }).click() + const remote = await scaffold.ctx.schedule.edit({ id: created.id, action: 'update', expectedUpdatedAt: created.updatedAt ?? null, + patch: { target: { kind: 'session', sessionId: after } } }) + expect(remote).toMatchObject({ target: { kind: 'session', sessionId: after } }) + await detail.getByRole('button', { name: 'Review with my edits', exact: true }).click() + await detail.getByRole('button', { name: /^Run in / }).click() + await page.getByRole('menuitem', { name: 'Current linked session', exact: true }).click() + await detail.getByRole('button', { name: 'Save changes', exact: true }).click() + await detail.getByRole('heading', { name: 'Recovered destination', exact: true }).waitFor() + expect(await scaffold.ctx.schedule.view({ id: created.id })) + .toMatchObject({ title: 'Recovered destination', target: { kind: 'session', sessionId: after } }) + expect(tripwire.pageErrors).toEqual([]) + } finally { + await scaffold.ctx.schedule.edit({ id: created.id, action: 'delete' }) + } + }) + it('removes every ended task from the page and keeps the active ones', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-schedule-clear-ended')) const endedSession = SessionId('schedule-manager-ended-clear') diff --git a/apps/web/tests/schedule-manual.e2e.ts b/apps/web/tests/schedule-manual.e2e.ts index c4525cbebe..60ca701a4d 100644 --- a/apps/web/tests/schedule-manual.e2e.ts +++ b/apps/web/tests/schedule-manual.e2e.ts @@ -30,8 +30,10 @@ it('creates from a paused empty state, selects the task, and saves an inline edi await page.getByRole('menuitem', { name: '手动创建', exact: true }).waitFor() await page.keyboard.press('Escape') await manager.getByRole('button', { name: '新建自动化任务', exact: true }).click() - const dialog = page.getByRole('dialog', { name: '手动创建', exact: true }) + const dialog = manager.getByRole('complementary', { name: '手动创建', exact: true }) await dialog.getByRole('textbox', { name: '自动化任务名称', exact: true }).fill('AI 新闻简报') + expect(await dialog.getByRole('status').count()).toBe(0) + expect(await dialog.getByRole('textbox', { name: '自动化任务名称', exact: true }).getAttribute('aria-required')).toBe('true') await dialog.getByRole('textbox', { name: '自动化任务内容', exact: true }).fill('整理过去 24 小时的 AI 新闻,附原文链接。') await dialog.getByRole('button', { name: /^重复 / }).click() await page.getByRole('menuitem', { name: '周一至周五', exact: true }).click() @@ -62,14 +64,60 @@ it('creates from a paused empty state, selects the task, and saves an inline edi const task = (await scaffold.ctx.schedule.catalog()).find(task => task.title === 'AI 新闻简报') if (task === undefined || task.target.kind !== 'new_once') throw new Error('manual task was not created with the reusable target') expect(task.target.create.cwd).toBe(defaultPath) - await detail.getByRole('button', { name: '编辑', exact: true }).click() + expect(await detail.getByRole('button', { name: '保存修改', exact: true }).isDisabled()).toBe(true) await detail.getByRole('textbox', { name: '自动化任务内容', exact: true }).fill('整理过去 24 小时的 AI 新闻,附原文链接,按重要性排序。') + expect(await detail.getByRole('button', { name: '暂停任务', exact: true }).isDisabled()).toBe(true) + await manager.getByRole('button', { name: '关闭详情', exact: true }).click() + await manager.getByRole('list', { name: '任务列表', exact: true }).getByRole('button', { name: task.title, exact: true }).click() + expect(await detail.getByRole('textbox', { name: '自动化任务内容', exact: true }).inputValue()) + .toBe('整理过去 24 小时的 AI 新闻,附原文链接,按重要性排序。') await detail.getByRole('button', { name: '保存修改', exact: true }).click() - await detail.getByRole('textbox', { name: '自动化任务内容', exact: true }).waitFor({ state: 'detached' }) - await detail.getByText('整理过去 24 小时的 AI 新闻,附原文链接,按重要性排序。', { exact: true }).waitFor() + await expect.poll(() => detail.getByRole('button', { name: '保存修改', exact: true }).isDisabled()).toBe(true) + expect(await detail.getByRole('textbox', { name: '自动化任务内容', exact: true }).inputValue()) + .toBe('整理过去 24 小时的 AI 新闻,附原文链接,按重要性排序。') const saved = await scaffold.ctx.schedule.view({ id: task.id }) if ('code' in saved) throw new Error('edited task was not retained') expect(saved.prompt).toBe('整理过去 24 小时的 AI 新闻,附原文链接,按重要性排序。') + await detail.getByRole('textbox', { name: '自动化任务名称', exact: true }).fill('本地简报草稿') + const remote = await scaffold.ctx.schedule.edit({ id: task.id, action: 'update', + expectedUpdatedAt: saved.updatedAt ?? null, patch: { prompt: '来自其他窗口的新指令' } }) + expect(remote).toMatchObject({ prompt: '来自其他窗口的新指令' }) + const recovery = detail.getByRole('button', { name: '保留修改并复核', exact: true }) + await recovery.waitFor() + const notice = detail.getByRole('alert').locator('..') + const palettes = [] + for (const dark of [false, true]) { + await page.evaluate(dark => document.body.toggleAttribute('data-ds-dark-theme', dark), dark) + const [box, field, message, action] = await Promise.all([ + notice.boundingBox(), detail.getByRole('textbox', { name: '自动化任务名称', exact: true }).locator('..').boundingBox(), + detail.getByRole('alert').boundingBox(), recovery.boundingBox(), + ]) + expect(box).not.toBeNull() + expect(field).not.toBeNull() + expect(message).not.toBeNull() + expect(action).not.toBeNull() + expect(Math.abs(box!.x - field!.x)).toBeLessThanOrEqual(1) + expect(Math.abs(box!.x + box!.width - field!.x - field!.width)).toBeLessThanOrEqual(1) + expect(action!.y).toBeLessThan(message!.y + message!.height) + expect(action!.y + action!.height).toBeGreaterThan(message!.y) + expect(box!.y + box!.height).toBeLessThan(field!.y) + palettes.push(await notice.evaluate((element) => { + const style = getComputedStyle(element) + return { color: style.color, background: style.backgroundColor } + })) + } + expect(palettes[0]).not.toEqual(palettes[1]) + await page.evaluate(() => { document.body.removeAttribute('data-ds-dark-theme') }) + expect(await detail.getByRole('button', { name: '保存修改', exact: true }).isDisabled()).toBe(true) + expect(await detail.getByRole('textbox', { name: '自动化任务内容', exact: true }).inputValue()).toBe(saved.prompt) + await detail.getByRole('button', { name: '保留修改并复核', exact: true }).click() + expect(await detail.getByRole('textbox', { name: '自动化任务名称', exact: true }).inputValue()).toBe('本地简报草稿') + expect(await detail.getByRole('textbox', { name: '自动化任务内容', exact: true }).inputValue()).toBe('来自其他窗口的新指令') + expect(await scaffold.ctx.schedule.view({ id: task.id })).toMatchObject({ title: task.title }) + await detail.getByRole('button', { name: '保存修改', exact: true }).click() + await detail.getByRole('heading', { name: '本地简报草稿', exact: true }).waitFor() + expect(await scaffold.ctx.schedule.view({ id: task.id })).toMatchObject({ title: '本地简报草稿', prompt: '来自其他窗口的新指令' }) + expect(await detail.evaluate(element => element === document.activeElement)).toBe(true) await manager.getByRole('button', { name: '选择创建方式', exact: true }).click() await page.getByRole('menuitem', { name: '手动创建', exact: true }).click() await dialog.getByRole('textbox', { name: '自动化任务名称', exact: true }).fill('检查最新进展') @@ -92,7 +140,7 @@ it('creates from a paused empty state, selects the task, and saves an inline edi await detail.getByRole('button', { name: '恢复任务', exact: true }).waitFor() expect(await detail.getByText('待创建', { exact: true }).count()).toBe(0) expect(await detail.getByText('最近运行会话', { exact: true }).count()).toBe(0) - expect(await detail.getByText('运行间隔至少', { exact: false }).count()).toBe(0) + expect(await detail.getByText('运行间隔至少', { exact: false }).count()).toBe(1) expect(tripwire.pageErrors).toEqual([]) expect(scaffold.ctx.agents.get(task.target.sessionId)).toBeUndefined() } finally { @@ -113,7 +161,7 @@ it('creates and edits Custom Cron through the built UI while retaining its expli const manager = page.getByTestId('task-manager-page') await manager.getByRole('button', { name: '选择创建方式', exact: true }).click() await page.getByRole('menuitem', { name: '手动创建', exact: true }).click() - const dialog = page.getByRole('dialog', { name: '手动创建', exact: true }) + const dialog = manager.getByRole('complementary', { name: '手动创建', exact: true }) await dialog.getByRole('textbox', { name: '自动化任务名称', exact: true }).fill('Cron deployment check') await dialog.getByRole('textbox', { name: '自动化任务内容', exact: true }).fill('Check the deployment status.') await dialog.getByRole('button', { name: /^重复 / }).click() @@ -128,15 +176,14 @@ it('creates and edits Custom Cron through the built UI while retaining its expli expect(created).toMatchObject({ kind: 'cron', expression: '0 9 * * 1-5', timeZone: 'Asia/Shanghai' }) expect(created).not.toHaveProperty('rrule') - await detail.getByRole('button', { name: '编辑', exact: true }).click() await detail.getByRole('textbox', { name: '自定义', exact: true }).fill(' ') - await detail.getByRole('button', { name: '保存修改', exact: true }).click() - await detail.getByRole('alert').getByText('请检查规则内容和时区', { exact: true }).waitFor() + expect(await detail.getByRole('button', { name: '保存修改', exact: true }).isDisabled()).toBe(true) + await detail.getByRole('status').getByText('请检查必填项和运行规则', { exact: true }).waitFor() expect(await scaffold.ctx.schedule.view({ id: created.id })).toEqual(created) await detail.getByRole('textbox', { name: '自定义', exact: true }).fill('0 10 * * 1-5') await detail.getByRole('button', { name: /^时区 / }).waitFor() await detail.getByRole('button', { name: '保存修改', exact: true }).click() - await detail.getByRole('textbox', { name: '自定义', exact: true }).waitFor({ state: 'detached' }) + await expect.poll(() => detail.getByRole('button', { name: '保存修改', exact: true }).isDisabled()).toBe(true) const edited = await scaffold.ctx.schedule.view({ id: created.id }) expect(edited).toMatchObject({ id: created.id, kind: 'cron', expression: '0 10 * * 1-5', timeZone: 'Asia/Shanghai' }) expect(edited).not.toHaveProperty('rrule') diff --git a/apps/web/tests/schedule-phone.e2e.ts b/apps/web/tests/schedule-phone.e2e.ts index 46e1bb1bfd..6fdd275553 100644 --- a/apps/web/tests/schedule-phone.e2e.ts +++ b/apps/web/tests/schedule-phone.e2e.ts @@ -53,6 +53,26 @@ async function viewFacts(page: Page): Promise { }) } +/** Header, form, notices, and footer share the pane's visible content edges. */ +async function expectEditorAlignment(title: Locator): Promise { + await expect.poll(() => title.evaluate((field) => { + const pane = field.closest('aside')! + const bounds = pane.getBoundingClientRect() + const name = field.parentElement!.getBoundingClientRect() + const action = pane.querySelector('footer:last-of-type > button:last-of-type')!.getBoundingClientRect() + const content = Array.from(pane.querySelectorAll('h2, textarea, [class*="_ruleRows"], [class*="_saveNotice"], [class*="_detailNotice"], [class*="_saveFailure"]')) + .filter(node => node.getClientRects().length > 0) + const offsets = { + gutters: Math.abs(name.left - bounds.left - bounds.right + name.right), + action: Math.abs(action.right - name.right), + left: Math.max(...content.map(node => Math.abs(node.getBoundingClientRect().left - name.left))), + right: Math.max(...Array.from(pane.querySelectorAll('textarea, [class*="_ruleRows"]')) + .map(node => Math.abs(node.getBoundingClientRect().right - name.right))), + } + return Object.fromEntries(Object.entries(offsets).filter(([, offset]) => offset > 1)) + })).toEqual({}) +} + it('keeps the automation page on screen with its text fields at the touch step', async () => { const scaffold = await launchWebScaffold() const url = scaffold.authenticatedUrl @@ -82,7 +102,6 @@ it('keeps the automation page on screen with its text fields at the touch step', // The editor keeps the touch floor for its name and instruction fields. await phonePanel.getByRole('button', { name: PHONE_TASK_TITLE, exact: true }).click() await phonePanel.locator('[class*="_detailScroll"]').waitFor({ timeout: 10_000 }) - await phonePanel.getByRole('button', { name: '编辑', exact: true }).click() await expect.poll(() => viewFacts(phonePage)).toEqual({ width: PHONE_PAGE_WIDTH, documentWidth: PHONE_PAGE_WIDTH, @@ -91,6 +110,66 @@ it('keeps the automation page on screen with its text fields at the touch step', search: '16px', instruction: '16px', }) + const detail = phonePanel.getByRole('complementary', { name: '任务详情', exact: true }) + await expectEditorAlignment(detail.getByRole('textbox', { name: '自动化任务名称', exact: true })) + await detail.getByRole('textbox', { name: '自动化任务名称', exact: true }).fill('未保存的手机草稿') + const task = (await scaffold.ctx.schedule.catalog()).find(item => item.title === PHONE_TASK_TITLE) + if (task === undefined) throw new Error('phone task was not retained') + await scaffold.ctx.schedule.edit({ id: task.id, action: 'update', expectedUpdatedAt: task.updatedAt ?? null, + patch: { prompt: '其他窗口的最新内容' } }) + const recovery = detail.getByRole('button', { name: '保留修改并复核', exact: true }) + await recovery.waitFor() + await recovery.scrollIntoViewIfNeeded() + const recoveryBox = await recovery.boundingBox() + expect(recoveryBox).not.toBeNull() + expect(recoveryBox!.x).toBeGreaterThanOrEqual(0) + expect(recoveryBox!.x + recoveryBox!.width).toBeLessThanOrEqual(PHONE_PAGE_WIDTH) + expect(recoveryBox!.y).toBeGreaterThanOrEqual(0) + expect(recoveryBox!.y + recoveryBox!.height).toBeLessThanOrEqual(844) + const [noticeBox, messageBox, nameBox] = await Promise.all([ + detail.getByRole('alert').locator('..').boundingBox(), detail.getByRole('alert').boundingBox(), + detail.getByRole('textbox', { name: '自动化任务名称', exact: true }).locator('..').boundingBox(), + ]) + expect(noticeBox).not.toBeNull() + expect(messageBox).not.toBeNull() + expect(nameBox).not.toBeNull() + expect(Math.abs(noticeBox!.x - nameBox!.x)).toBeLessThanOrEqual(1) + expect(Math.abs(noticeBox!.x + noticeBox!.width - nameBox!.x - nameBox!.width)).toBeLessThanOrEqual(1) + expect(recoveryBox!.y).toBeGreaterThanOrEqual(messageBox!.y + messageBox!.height) + expect(recoveryBox!.x + recoveryBox!.width).toBeLessThanOrEqual(noticeBox!.x + noticeBox!.width) + expect(await detail.getByRole('button', { name: '保存修改', exact: true }).isDisabled()).toBe(true) + await recovery.click() + expect(await detail.getByRole('textbox', { name: '自动化任务名称', exact: true }).inputValue()).toBe('未保存的手机草稿') + expect(await detail.getByRole('textbox', { name: '自动化任务内容', exact: true }).inputValue()).toBe('其他窗口的最新内容') + expect((await viewFacts(phonePage)).documentWidth).toBe(PHONE_PAGE_WIDTH) + expect(await scaffold.ctx.schedule.view({ id: task.id })).toMatchObject({ title: PHONE_TASK_TITLE }) + await detail.getByRole('button', { name: '放弃', exact: true }).click() + await phonePanel.getByRole('button', { name: '关闭详情', exact: true }).click() + await phonePanel.getByRole('button', { name: '选择创建方式', exact: true }).click() + await phonePage.getByRole('menuitem', { name: '手动创建', exact: true }).click() + const creation = phonePanel.getByRole('complementary', { name: '手动创建', exact: true }) + await creation.waitFor() + await expectEditorAlignment(creation.getByRole('textbox', { name: '自动化任务名称', exact: true })) + await expect.poll(() => viewFacts(phonePage)).toEqual({ + width: PHONE_PAGE_WIDTH, + documentWidth: PHONE_PAGE_WIDTH, + smallestField: 16, + name: '16px', + search: '16px', + instruction: '16px', + }) + for (const name of ['放弃', '创建']) { + const button = creation.getByRole('button', { name, exact: true }) + await button.scrollIntoViewIfNeeded() + const box = await button.boundingBox() + expect(box).not.toBeNull() + expect(box!.x).toBeGreaterThanOrEqual(0) + expect(box!.x + box!.width).toBeLessThanOrEqual(PHONE_PAGE_WIDTH) + expect(box!.y).toBeGreaterThanOrEqual(0) + expect(box!.y + box!.height).toBeLessThanOrEqual(844) + } + await creation.getByRole('button', { name: '放弃', exact: true }).click() + expect(await phonePanel.getByRole('complementary').count()).toBe(0) await phone.close() // A fine pointer keeps the compact steps: the floor is pointer-scoped. @@ -108,7 +187,6 @@ it('keeps the automation page on screen with its text fields at the touch step', }) await desktopPanel.getByRole('button', { name: PHONE_TASK_TITLE, exact: true }).click() await desktopPanel.locator('[class*="_detailScroll"]').waitFor({ timeout: 10_000 }) - await desktopPanel.getByRole('button', { name: '编辑', exact: true }).click() await expect.poll(() => viewFacts(desktopPage)).toEqual({ width: 1024, documentWidth: 1024, @@ -130,3 +208,83 @@ it('keeps the automation page on screen with its text fields at the touch step', await scaffold.close() } }, 180_000) + +it.each([ + { locale: 'en-US', tasks: 'Automation tasks', sidebar: 'Collapse sidebar', openSidebar: 'Open sidebar', close: 'Close details', + methods: 'Choose a creation method', manual: 'Create manually', title: 'Automation task name', discard: 'Discard', + instruction: 'Automation task instruction', create: 'Create', + repeat: /^Repeat /, frequencies: ['Every month', 'Weekly', 'Once'] }, + { locale: 'zh-CN', tasks: '自动化任务', sidebar: '收起侧边栏', openSidebar: '打开侧边栏', close: '关闭详情', + methods: '选择创建方式', manual: '手动创建', title: '自动化任务名称', discard: '放弃', + instruction: '自动化任务内容', create: '创建', + repeat: /^重复 /, frequencies: ['每月', '每周', '仅一次'] }, + { locale: 'fr-FR', tasks: "Tâches d'automatisation", sidebar: 'Réduire la barre latérale', openSidebar: 'Ouvrir la barre latérale', close: 'Fermer les détails', + methods: 'Choisir le mode de création', manual: 'Créer manuellement', title: "Nom de la tâche d'automatisation", discard: 'Abandonner', + instruction: "Instruction de tâche d'automatisation", create: 'Créer', + repeat: /^Répéter /, frequencies: ['Tous les mois', 'Hebdomadaire', 'Une fois'] }, +] as const)('keeps both editor modes inside a narrow pane with the sidebar open ($locale)', async (copy) => { + const scaffold = await launchWebScaffold() + let browser: Browser | undefined + try { + browser = await chromium.launch() + await scaffold.ctx.schedule.create({ + title: PHONE_TASK_TITLE, prompt: 'Check the deployment log', + timing: { kind: 'at', at: '2099-08-25T12:08:00.000Z' }, target: { kind: 'new_once' }, + }) + const context = await browser.newContext({ viewport: { width: 1100, height: 844 }, locale: copy.locale, timezoneId: 'Asia/Shanghai' }) + const page = await context.newPage() + const consoleWatch = watchConsole(page) + await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) + await page.getByRole('button', { name: copy.sidebar, exact: true }).waitFor() + await page.getByRole('button', { name: copy.tasks, exact: true }).click() + const panel = page.getByTestId('task-manager-page') + await panel.getByRole('button', { name: PHONE_TASK_TITLE, exact: true }).click() + await page.evaluate(() => document.fonts.ready) + for (const mode of ['edit', 'create']) { + if (mode === 'create') { + await panel.getByRole('button', { name: copy.close, exact: true }).click() + await panel.getByRole('button', { name: copy.methods, exact: true }).click() + await page.getByRole('menuitem', { name: copy.manual, exact: true }).click() + } + const editor = panel.getByRole('complementary') + const title = editor.getByRole('textbox', { name: copy.title, exact: true }) + if (mode === 'create') { + for (const name of [copy.title, copy.instruction]) { + const field = editor.getByRole('textbox', { name, exact: true }) + expect(await field.getAttribute('required')).toBeNull() + expect(await field.getAttribute('aria-required')).toBe('true') + expect(await field.evaluate(node => (node as HTMLInputElement | HTMLTextAreaElement).validationMessage)).toBe('') + } + expect(await editor.getByRole('button', { name: copy.create, exact: true }).isDisabled()).toBe(true) + } + await title.fill('Retained draft through resizing') + await editor.getByRole('button', { name: copy.discard, exact: true }).waitFor() + for (const width of [1280, 1100, 1024, 900, 800]) { + await title.focus() + await page.setViewportSize({ width, height: 844 }) + expect(await title.evaluate(node => document.activeElement === node)).toBe(true) + // Entering the shell's narrow range publishes the reopen action after its resize observer runs. + if (width === 900) { await page.getByRole('button', { name: copy.openSidebar, exact: true }).click(); await title.focus() } + await page.getByRole('button', { name: copy.sidebar, exact: true }).waitFor() + expect(await panel.getByRole('button', { name: PHONE_TASK_TITLE, exact: true }).isVisible()).toBe(true) + for (const frequency of copy.frequencies) { + await editor.getByRole('button', { name: copy.repeat }).click() + await page.getByRole('menuitem', { name: frequency, exact: true }).click() + await expectEditorAlignment(title) + await expect.poll(() => editor.locator('[class*="_detailScroll"]').evaluate(node => node.scrollWidth - node.clientWidth)).toBe(0) + expect(await editor.locator('input, textarea, button').evaluateAll(nodes => nodes.every((node) => { + const box = node.getBoundingClientRect() + const pane = node.closest('aside')!.getBoundingClientRect() + return box.left >= pane.left && box.right <= pane.right + }))).toBe(true) + } + expect(await title.inputValue()).toBe('Retained draft through resizing') + } + await page.setViewportSize({ width: 1100, height: 844 }) + } + expect(consoleWatch.pageErrors).toEqual([]) + expect(consoleWatch.warnings).toEqual([]) + } finally { + try { await browser?.close() } finally { await scaffold.close() } + } +}, 180_000) diff --git a/apps/web/tests/schedule-pickers.e2e.ts b/apps/web/tests/schedule-pickers.e2e.ts index 057ec0879c..2548f71777 100644 --- a/apps/web/tests/schedule-pickers.e2e.ts +++ b/apps/web/tests/schedule-pickers.e2e.ts @@ -50,8 +50,8 @@ async function expectEllipsizedText(labels: Locator): Promise { } describe.each([ - { locale: 'en-US', tasks: 'Automation tasks', edit: 'Edit', time: 'Time', second: 'Second', zone: 'Time zone' }, - { locale: 'zh-CN', tasks: '自动化任务', edit: '编辑', time: '时间', second: '秒', zone: '时区' }, + { locale: 'en-US', tasks: 'Automation tasks', time: 'Time', second: 'Second', zone: 'Time zone' }, + { locale: 'zh-CN', tasks: '自动化任务', time: '时间', second: '秒', zone: '时区' }, ])('automation pickers ($locale)', (copy) => { it.each(['clock', 'zone'] as const)('ellipsizes the %s labels and keeps vertical choices reachable at 500% zoom', async (kind) => { const context = await browser.newContext({ @@ -66,7 +66,6 @@ describe.each([ await page.getByRole('button', { name: copy.tasks, exact: true }).click() const detail = page.getByTestId('task-manager-page') await detail.getByRole('button', { name: TITLE, exact: true }).click() - await detail.getByRole('button', { name: copy.edit, exact: true }).click() await page.evaluate(() => document.fonts.ready) const trigger = kind === 'clock' ? detail.getByRole('button', { name: copy.time, exact: true }) diff --git a/apps/web/tests/shipped-composition.e2e.ts b/apps/web/tests/shipped-composition.e2e.ts index 39d995e8c9..48ca2546c6 100644 --- a/apps/web/tests/shipped-composition.e2e.ts +++ b/apps/web/tests/shipped-composition.e2e.ts @@ -498,8 +498,13 @@ const EXPECTED_TOOLS = [ 'skill', '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', @@ -593,8 +598,10 @@ it('assembles the shipped Web transport, catalog, guidance, and defaults', async "mode": "always", } `) - // The directory tool is global; the deployment's default preset supplies the agent's remaining tools. - expect(ctx.tools.schemas().map(schema => schema.name)).toEqual(['working_directory']) + // Directory and conditional Team tools are global; presets supply the remaining registrations. + expect(ctx.tools.schemas().map(schema => schema.name)).toEqual([ + 'working_directory', 'team_task_create', 'team_task_list', 'team_task_get', 'team_task_update', + ]) const handle = await ctx.agents.create({ sessionId: SessionId('shipped-composition'), setup: agentCtx => ctx.agentPresets.mount(agentCtx).then(() => undefined), diff --git a/apps/web/tests/sidebar-subagent-activity.e2e.ts b/apps/web/tests/sidebar-subagent-activity.e2e.ts index f2c8eb3434..7ae28dd478 100644 --- a/apps/web/tests/sidebar-subagent-activity.e2e.ts +++ b/apps/web/tests/sidebar-subagent-activity.e2e.ts @@ -101,10 +101,8 @@ describe('web e2e: sidebar subagent activity', () => { provider: 'spawn', label: 'sidebar activity child', signal: new AbortController().signal, - request: { - prompt: [{ type: 'text', text: 'Hold this delegated task open.' }], - parent: parentHandle.agent, - }, + prompt: [{ type: 'text', text: 'Hold this delegated task open.' }], + parent: parentHandle.agent, }) childId = started.childId await waitForRunningChild(scaffold, adapter, childId) @@ -147,10 +145,72 @@ describe('web e2e: sidebar subagent activity', () => { ) expect(await ownerRow.locator('[data-state="ongoing"]').count()).toBe(1) await ownerRow.click() - const runningTrigger = page.getByRole('button', { name: '1 subagent running' }) + const runningTrigger = page.getByRole('button', { name: '1 subagent', exact: true }) await runningTrigger.waitFor({ timeout: 10_000 }) expect(await runningTrigger.locator('[data-state="ongoing"]').count()).toBe(1) - await assertFixtureInventory(SNAPSHOT_DIR, ['owner-running.expected.md']) + // This directly created owner has no tool preset; the fixture collects its own job. + scaffold.ctx.effect(() => scaffold.ctx.jobs.attachController('compact header fixture'), 'compact header job controller') + const completion = Promise.withResolvers<{ status: 'completed' }>() + const ownerId = parentHandle.agent.id + const jobId = scaffold.ctx.jobs.start({ + kind: 'bash', label: 'Concurrent background work', owner: ownerId, + run(handle) { + handle.append('Background work is running.\n') + return { done: completion.promise, cancel: () => { completion.resolve({ status: 'completed' }) } } + }, + }) + const viewport = page.viewportSize()! + try { + const jobTrigger = page.getByRole('button', { name: '1 background job', exact: true }) + await jobTrigger.waitFor() + expect(await jobTrigger.evaluate(element => document.getElementById(element.getAttribute('aria-describedby')!)?.textContent)).toBe('Running') + const controls = [ + { trigger: runningTrigger, icon: '[data-icon="dsw-icon-users-outline-regular"]', count: '[data-subagent-count]' }, + { trigger: jobTrigger, icon: '[data-icon="dsw-icon-layers-outline-regular"]', count: '[data-job-count]' }, + ] + for (const { trigger, icon, count } of controls) { + expect(await trigger.locator(count).isVisible()).toBe(true) + expect(await trigger.locator('[data-state="ongoing"]').isVisible()).toBe(true) + expect(await trigger.locator(icon).isVisible()).toBe(false) + } + await runningTrigger.focus() + await page.setViewportSize({ width: 390, height: 700 }) + for (const { trigger, icon, count } of controls) { + await expect.poll(() => trigger.locator(count).isVisible()).toBe(false) + expect(await trigger.locator(icon).isVisible()).toBe(true) + expect(await trigger.locator('[data-state="ongoing"]').isVisible()).toBe(false) + } + expect(await runningTrigger.evaluate(element => element === document.activeElement)).toBe(true) + expect(await jobTrigger.evaluate(element => document.getElementById(element.getAttribute('aria-describedby')!)?.textContent)).toBe('Running') + await compareOrRefreshGolden( + join(SNAPSHOT_DIR, 'compact-running.expected.md'), + await captureStableAria(page, 'header:has([data-subagent-action])', scaffold.workspaceCwd), + MODE, + ) + await saveFailureShot(page, 'web-e2e-compact-running-icons') + await runningTrigger.click() + const members = page.locator('[data-subagent-panel]') + await members.waitFor() + expect(await members.getByRole('treeitem').locator('[data-state="ongoing"]').isVisible()).toBe(true) + await page.keyboard.press('Escape') + await jobTrigger.click() + const jobs = page.getByRole('list', { name: 'Background jobs', exact: true }) + await jobs.waitFor() + expect(await jobs.locator('[data-state="ongoing"]').isVisible()).toBe(true) + await page.keyboard.press('Escape') + await page.setViewportSize(viewport) + for (const { trigger, icon, count } of controls) { + await expect.poll(() => trigger.locator(count).isVisible()).toBe(true) + expect(await trigger.locator('[data-state="ongoing"]').isVisible()).toBe(true) + expect(await trigger.locator(icon).isVisible()).toBe(false) + } + } finally { + completion.resolve({ status: 'completed' }) + await scaffold.ctx.jobs.wait(jobId, 5_000, ownerId) + await page.keyboard.press('Escape') + await page.setViewportSize(viewport) + } + await assertFixtureInventory(SNAPSHOT_DIR, ['owner-running.expected.md', 'compact-running.expected.md']) expect(tripwire.pageErrors).toEqual([]) expect(tripwire.warnings).toEqual([]) }) diff --git a/apps/web/tests/snapshots/agent-team-panel/collaboration.zh.expected.md b/apps/web/tests/snapshots/agent-team-panel/collaboration.zh.expected.md new file mode 100644 index 0000000000..07a6ac96dc --- /dev/null +++ b/apps/web/tests/snapshots/agent-team-panel/collaboration.zh.expected.md @@ -0,0 +1,17 @@ +## Menu + +- menu "模型与推理等级": + - menuitem "模型 DeepSeek-V4-Flash" + - text: 启用智能体团队协作 + - button "关于智能体团队协作" + - switch "启用智能体团队协作" [checked] + +## Confirmation + +- dialog "确认停用智能体团队协作?": + - heading "确认停用智能体团队协作?" [level=2] + - button "关闭" + - paragraph: 对话过程中停用智能体团队协作,将停止共享任务工具和团队协调指令。 + - paragraph: 停用后可能影响缓存复用、增加后续 token 用量与开销,并影响当前任务的协作效果。 + - button "取消" + - button "确认停用" diff --git a/apps/web/tests/snapshots/agent-team-panel/command-error.en.expected.md b/apps/web/tests/snapshots/agent-team-panel/command-error.en.expected.md new file mode 100644 index 0000000000..f636b75b13 --- /dev/null +++ b/apps/web/tests/snapshots/agent-team-panel/command-error.en.expected.md @@ -0,0 +1,8 @@ +## Failed receipt + +- 'button "Agent teamwork Failed: Use /team on or /team off."' + +## Raw error + +- 'button "Agent teamwork Failed: Use /team on or /team off." [expanded]' +- text: Use /team on or /team off. diff --git a/apps/web/tests/snapshots/agent-team-panel/command-error.zh.expected.md b/apps/web/tests/snapshots/agent-team-panel/command-error.zh.expected.md new file mode 100644 index 0000000000..198f3edb33 --- /dev/null +++ b/apps/web/tests/snapshots/agent-team-panel/command-error.zh.expected.md @@ -0,0 +1,8 @@ +## Failed receipt + +- button "智能体团队协作 执行失败:Use /team on or /team off." + +## Raw error + +- button "智能体团队协作 执行失败:Use /team on or /team off." [expanded] +- text: Use /team on or /team off. diff --git a/apps/web/tests/snapshots/agent-team-panel/disable-confirmation.expected.md b/apps/web/tests/snapshots/agent-team-panel/disable-confirmation.expected.md new file mode 100644 index 0000000000..ae29ea02b2 --- /dev/null +++ b/apps/web/tests/snapshots/agent-team-panel/disable-confirmation.expected.md @@ -0,0 +1,7 @@ +- dialog "Disable agent teamwork?": + - heading "Disable agent teamwork?" [level=2] + - button "Close" + - paragraph: Disabling agent teamwork during a conversation stops the use of shared task tools and team coordination instructions. + - paragraph: Disabling it may affect cache reuse, increase subsequent token usage and costs, and affect collaboration on the current task. + - button "Cancel" + - button "Disable" diff --git a/apps/web/tests/snapshots/agent-team-panel/info.expected.md b/apps/web/tests/snapshots/agent-team-panel/info.expected.md new file mode 100644 index 0000000000..5e675607a5 --- /dev/null +++ b/apps/web/tests/snapshots/agent-team-panel/info.expected.md @@ -0,0 +1 @@ +- tooltip "Coordinate work and progress across agents with a shared task board for better results on complex tasks, with correspondingly higher token costs." diff --git a/apps/web/tests/snapshots/agent-team-panel/resize.expected.md b/apps/web/tests/snapshots/agent-team-panel/resize.expected.md index 57fbab1f84..3e8f7521b2 100644 --- a/apps/web/tests/snapshots/agent-team-panel/resize.expected.md +++ b/apps/web/tests/snapshots/agent-team-panel/resize.expected.md @@ -1,15 +1,26 @@ -- dialog "Agent team": - - heading "Members" [level=3] - - button "lead Current chat Inactive" [disabled] - - heading "Shared tasks 2" [level=3] - - article: - - strong: Agent task - - text: In progress - - paragraph: Created by the Team Lead - - text: "task-1Owner: leadWrite scopes: src/web" - - article: - - strong: Responsive task - - text: Pending - - paragraph: Check the task description before you start work. Check the task description before you start work. Check the task description before you start work. - - button "Show more" - - text: "task-2Owner: UnownedReady" +- dialog "Subagent sessions": + - heading "Subagents" [level=3] + - text: 1 external + - tree "Subagent sessions": + - 'treeitem "Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. team-panel-researcher · External task" [disabled] [level=1]': "Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. team-panel-researcher" + - region "Task board": + - heading "Shared tasks 1 completed · 1 in progress · 2 pending" [level=3] + - list: + - listitem: + - button "In progress Agent task": + - img "In progress" + - text: Agent task + - listitem: + - button "Completed Completed preparation": + - img "Completed" + - text: Completed preparation + - listitem: + - button "Pending Dependency task": + - img "Pending" + - text: Dependency task + - listitem: + - button "Blocked by dependencies Responsive task with a long title that remains fully readable when expanded at the minimum supported viewport" [expanded]: + - img "Blocked by dependencies" + - text: Responsive task with a long title that remains fully readable when expanded at the minimum supported viewport + - paragraph: Check every dependency and write scope before you start work. Check every dependency and write scope before you start work. Check every dependency and write scope before you start work. Check every dependency and write scope before you start work. Check every dependency and write scope before you start work. Check every dependency and write scope before you start work. Check every dependency and write scope before you start work. Check every dependency and write scope before you start work. Check every dependency and write scope before you start work. Check every dependency and write scope before you start work. Check every dependency and write scope before you start work. Check every dependency and write scope before you start work. Check every dependency and write scope before you start work. Check every dependency and write scope before you start work. Check every dependency and write scope before you start work. Check every dependency and write scope before you start work. + - paragraph: "Owner: Unowned" diff --git a/apps/web/tests/snapshots/agent-team-panel/task-only.expected.md b/apps/web/tests/snapshots/agent-team-panel/task-only.expected.md new file mode 100644 index 0000000000..6a663176b6 --- /dev/null +++ b/apps/web/tests/snapshots/agent-team-panel/task-only.expected.md @@ -0,0 +1,11 @@ +- dialog "Subagent sessions": + - heading "Subagents" [level=3] + - paragraph: No subagents + - tree "Subagent sessions" + - region "Task board": + - heading "Shared tasks 1 pending" [level=3] + - list: + - listitem: + - button "Pending Plan before delegation": + - img "Pending" + - text: Plan before delegation diff --git a/apps/web/tests/snapshots/agent-team-panel/task.expected.md b/apps/web/tests/snapshots/agent-team-panel/task.expected.md index d5593a069b..9032c3413b 100644 --- a/apps/web/tests/snapshots/agent-team-panel/task.expected.md +++ b/apps/web/tests/snapshots/agent-team-panel/task.expected.md @@ -1,9 +1,14 @@ -- dialog "Agent team": - - heading "Members" [level=3] - - button "lead Current chat Inactive" [disabled] - - heading "Shared tasks 1" [level=3] - - article: - - strong: Agent task - - text: In progress - - paragraph: Created by the Team Lead - - text: "task-1Owner: leadWrite scopes: src/web" +- dialog "Subagent sessions": + - heading "Subagents" [level=3] + - text: 1 external + - tree "Subagent sessions": + - 'treeitem "Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. team-panel-researcher · External task" [disabled] [level=1]': "Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. Researcher: investigate the construction and verify every bound before reporting the result. team-panel-researcher" + - region "Task board": + - heading "Shared tasks 1 in progress" [level=3] + - list: + - listitem: + - button "In progress Agent task" [expanded]: + - img "In progress" + - text: Agent task + - paragraph: Created by the root agent + - paragraph: "Owner: session-{{uuid}}" diff --git a/apps/web/tests/snapshots/subagent-summary/fork.expected.md b/apps/web/tests/snapshots/subagent-summary/fork.expected.md new file mode 100644 index 0000000000..c94fd252dd --- /dev/null +++ b/apps/web/tests/snapshots/subagent-summary/fork.expected.md @@ -0,0 +1,40 @@ +## Running + +- text: Running +- button "Fork subagent Review inherited conversation context" + +## Creation receipt and complete task + +- button "Fork subagent Review inherited conversation context" [expanded] +- text: Recorded result +- list: + - listitem: + - text: Started + - term: Agent task description + - definition: Review inherited conversation context + - term: ID + - definition: fork-worker + - group: + - text: Task + - list: + - listitem: + - paragraph: Summarize the earlier agreed requirements. Do not modify files. +- button "Inspect" + +## Restored creation receipt + +- button "Fork subagent Review inherited conversation context" [expanded] +- text: Recorded result +- list: + - listitem: + - text: Started + - term: Agent task description + - definition: Review inherited conversation context + - term: ID + - definition: fork-worker + - group: + - text: Task + - list: + - listitem: + - paragraph: Summarize the earlier agreed requirements. Do not modify files. +- button "Inspect" diff --git a/apps/web/tests/snapshots/subagent-summary/lifecycle.expected.md b/apps/web/tests/snapshots/subagent-summary/lifecycle.expected.md new file mode 100644 index 0000000000..105e201da9 --- /dev/null +++ b/apps/web/tests/snapshots/subagent-summary/lifecycle.expected.md @@ -0,0 +1,52 @@ +## Preparing without description + +- text: Preparing tool call Create subagent + +## Preparing with partial description + +- text: Preparing tool call Create subagent Review subagent task summaries + +## Running + +- text: Running +- button "Create subagent Review subagent task summaries" + +## Created + +- button "Create subagent Review subagent task summaries" + +## Creation receipt and complete task + +- button "Create subagent Review subagent task summaries" [expanded] +- text: Recorded result +- list: + - listitem: + - text: Started + - term: Agent task description + - definition: Review subagent task summaries Additional description context + - term: ID + - definition: child-foo + - group: + - text: Task + - list: + - listitem: + - paragraph: Inspect the subagent tool card across its lifecycle. Check streaming arguments, creation receipts, and restored history. Return findings without changing unrelated files. +- button "Inspect" + +## Restored creation receipt + +- button "Create subagent Review subagent task summaries" [expanded] +- text: Recorded result +- list: + - listitem: + - text: Started + - term: Agent task description + - definition: Review subagent task summaries Additional description context + - term: ID + - definition: child-foo + - group: + - text: Task + - list: + - listitem: + - paragraph: Inspect the subagent tool card across its lifecycle. Check streaming arguments, creation receipts, and restored history. Return findings without changing unrelated files. +- button "Inspect" diff --git a/apps/web/tests/snapshots/subagent-summary/variants.expected.md b/apps/web/tests/snapshots/subagent-summary/variants.expected.md new file mode 100644 index 0000000000..b79b9010c4 --- /dev/null +++ b/apps/web/tests/snapshots/subagent-summary/variants.expected.md @@ -0,0 +1,39 @@ +## fallback + +- button "Create subagent Review the fallback summary." [expanded] +- text: Recorded result +- list: + - listitem: + - text: Started + - term: ID + - definition: fallback-worker + - group: + - text: Task + - list: + - listitem: + - paragraph: Review the fallback summary. Keep the complete task in details. +- button "Inspect" + +## reply + +- button "Create subagent Check synchronous agent reply" [expanded] +- text: Recorded result +- list: + - listitem: + - text: Agent response + - paragraph: The synchronous agent found two issues. Both are documented in the report. + - group: + - text: Task + - list: + - listitem: + - paragraph: Review the synchronous report. Keep every instruction available. +- button "Inspect" + +## title-only + +- button "Create subagent" + +## error + +- text: Failed +- button "Create subagent Subagent capacity exceeded" diff --git a/apps/web/tests/subagent-conversation.e2e.ts b/apps/web/tests/subagent-conversation.e2e.ts index 56d7f0e66b..2b9cae7b84 100644 --- a/apps/web/tests/subagent-conversation.e2e.ts +++ b/apps/web/tests/subagent-conversation.e2e.ts @@ -5,7 +5,7 @@ import { dirname, join } from 'node:path' import { zstdCompressSync } from 'node:zlib' import type { Browser, Page } from 'playwright' import { chromium } from 'playwright' -import { afterAll, beforeAll, describe, expect, it, onTestFailed, vi } from 'vitest' +import { afterAll, beforeAll, describe, expect, it, onTestFailed, onTestFinished, vi } from 'vitest' import { prepareSessionSnapshotFixtureForComparison } from '@deepseek-ai/dsh-llm-replay' import { SESSION_FORMAT_VERSION, SessionId as sessionId, SessionLogOffset, SessionSeq, @@ -32,7 +32,7 @@ const TREE_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/subagent-con const BRANCHLESS_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/subagent-conversation/branchless.expected.md', import.meta.url)) const SIDEBAR_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/subagent-conversation/sidebar.expected.md', import.meta.url)) const SIDEBAR_CHAT_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/subagent-conversation/sidebar-chat.expected.md', import.meta.url)) -const UNAVAILABLE_GRANDCHILD_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/subagent-conversation/nested.expected.md', import.meta.url)) +const GRANDCHILD_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/subagent-conversation/nested.expected.md', import.meta.url)) const FORK_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/subagent-conversation/fork.expected.md', import.meta.url)) const MIGRATING_CHILD_EXPECTED = fileURLToPath(new URL( './expected/subagent-conversation/migration-required-child.expected.md', import.meta.url, @@ -102,6 +102,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () = let browser: Browser let page: Page let sidecarRoot: string + let parentId: SessionId let childId: SessionId let oneShotId: SessionId let grandchildId: SessionId @@ -139,8 +140,9 @@ describe('web e2e: persisted subagent conversation and human continuation', () = await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) await connectFreshWorkspace(page, scaffold.workspaceCwd) - const parent = scaffold.ctx.agents.roots()[0] + const parent = scaffold.ctx.agents.list().filter(agent => agent.session.header.origin !== 'subagent')[0] if (parent === undefined) throw new Error('fresh workspace did not publish its parent Agent') + parentId = parent.id const parentSettled = scaffold.whenTurnSettled() const parentInput = page.locator('[data-composer-input][contenteditable="true"]').first() await parentInput.fill(PARENT_PROMPT) @@ -152,10 +154,8 @@ describe('web e2e: persisted subagent conversation and human continuation', () = provider: 'spawn', label: LABEL, signal: new AbortController().signal, - request: { - prompt: [{ type: 'text', text: INITIAL_PROMPT }], - parent, - }, + prompt: [{ type: 'text', text: INITIAL_PROMPT }], + parent, }) childId = started.childId await waitForAgentToSettle(scaffold, childId) @@ -309,25 +309,23 @@ describe('web e2e: persisted subagent conversation and human continuation', () = expect(scaffold.ctx.agents.get(childId)).toBeUndefined() expect(scaffold.ctx.agents.get(oneShotId)).toBeUndefined() expect(scaffold.ctx.agents.get(grandchildId)).toBeUndefined() - expect(await scaffold.ctx.subagents.listChildren(parent.id)).toMatchObject([ - { - id: childId, mode: 'continuable', label: LABEL, - }, - { - id: oneShotId, mode: 'one-shot', label: ONE_SHOT_LABEL, - }, - ]) + { + using observed = await scaffold.ctx.sessionQuery.observeSession(parent.id) + expect(observed.projections?.values.subagentCatalog).toMatchObject([ + { + id: childId, mode: 'continuable', label: LABEL, + }, + { + id: oneShotId, mode: 'one-shot', label: ONE_SHOT_LABEL, + }, + ]) + } { using observed = await scaffold.ctx.sessionQuery.observeSession(childId) expect(observed.source).toBe('prepared') expect(observed.projections?.values.subagentCatalog).toEqual([ { id: grandchildId, createdAt: authoredAt, mode: 'continuable', label: NESTED_LABEL }, ]) - expect(await scaffold.ctx.subagents.listChildren(childId)).toMatchObject([ - { - id: grandchildId, mode: 'continuable', label: NESTED_LABEL, - }, - ]) } await waitForCacheRow(scaffold, childHeader) const cache = scaffold.ctx.storageDomain.get(projectionCacheDomainSpec.name) @@ -379,6 +377,65 @@ describe('web e2e: persisted subagent conversation and human continuation', () = await writeComposerDraft(page, input, '') }) + it('enters the member tree after pointer or keyboard activation of its trigger', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-subagent-trigger-focus')) + const trigger = page.getByRole('button', { name: '2 subagents', exact: true }) + const panel = page.locator('[data-subagent-panel]') + try { + for (const activation of ['click', 'Enter', 'Space']) { + if (activation === 'click') await trigger.click() + else await trigger.press(activation) + await expect.poll(() => panel.evaluate(element => element === document.activeElement)).toBe(true) + await page.keyboard.press('ArrowDown') + expect(await panel.locator('[role="treeitem"]:not([aria-disabled="true"])').first() + .evaluate(element => element === document.activeElement)).toBe(true) + await page.keyboard.press('Escape') + await panel.waitFor({ state: 'detached' }) + expect(await trigger.evaluate(element => element === document.activeElement)).toBe(true) + } + } finally { + await page.locator('[data-composer-input]').first().hover() + } + }) + + it('shows complete descriptions and IDs without hover at desktop and touch widths', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-subagent-catalog-labels')) + const originalViewport = page.viewportSize()! + const originalUrl = page.url() + try { + for (const width of [originalViewport.width, 390]) { + await page.setViewportSize({ ...originalViewport, width }) + const trigger = page.getByRole('button', { name: '2 subagents', exact: true }) + await trigger.click() + const panel = page.locator('[data-subagent-panel]') + const row = panel.getByRole('treeitem', { name: new RegExp(LABEL) }) + const primary = row.getByText(LABEL, { exact: true }) + const secondary = row.getByText(childId, { exact: true }) + const primaryBounds = await primary.boundingBox() + const secondaryBounds = await secondary.boundingBox() + expect(primaryBounds).not.toBeNull() + expect(secondaryBounds).not.toBeNull() + expect(primaryBounds!.y + primaryBounds!.height).toBeLessThanOrEqual(secondaryBounds!.y) + for (const label of [primary, secondary]) { + expect(await label.evaluate(element => element.scrollWidth <= element.clientWidth + && element.scrollHeight <= element.clientHeight)).toBe(true) + } + expect(await row.getAttribute('aria-label')).toContain(`${LABEL} ${childId} `) + expect(await row.getByRole('button', { name: /description/u }).count()).toBe(0) + expect(await row.getByRole('button', { name: `Expand ${LABEL} descendants`, exact: true }).count()).toBe(1) + expect(await row.getByRole('button', { name: `Open ${LABEL} in sidebar`, exact: true }).count()).toBe(1) + expect(page.url()).toBe(originalUrl) + expect(await page.getByRole('tooltip').count()).toBe(0) + expect(scaffold.ctx.agents.get(childId)).toBeUndefined() + await page.keyboard.press('Escape') + await panel.waitFor({ state: 'detached' }) + } + } finally { + await page.setViewportSize(originalViewport) + await page.locator('[data-composer-input]').first().hover() + } + }) + it('recovers a current-format cold child catalog cache miss without activating either level', async () => { onTestFailed(() => saveFailureShot(page, 'screenshots/non-migrating-session-reads/web-e2e-subagent-tree')) await page.getByRole('button', { name: '2 subagents' }).hover() @@ -388,6 +445,8 @@ describe('web e2e: persisted subagent conversation and human continuation', () = const hit = document.elementFromPoint(rect.left + 8, rect.top + 8) return hit !== null && element.contains(hit) })).toBe(true) + const sectionHeader = page.locator('[data-subagent-section-header]') + expect(await sectionHeader.count()).toBe(0) const oneShotDisclosure = page.getByRole('button', { name: `Expand ${ONE_SHOT_LABEL} descendants`, }) @@ -396,22 +455,112 @@ describe('web e2e: persisted subagent conversation and human continuation', () = expect(await oneShotRow.locator('[data-state="done"]').count()).toBe(1) expect(await oneShotRow.getByText('~6mo 12d', { exact: true }).count()).toBe(1) expect(await oneShotRow.getAttribute('aria-label')).toContain('192d 00h 00m 00s') + const childRow = page.getByRole('treeitem', { name: new RegExp(LABEL) }) + const disclosure = childRow.getByRole('button', { name: new RegExp(`^(Expand|Collapse) ${LABEL} descendants$`, 'u') }) + const beforeDisclosureUrl = page.url() + const clickDisclosureGutter = async (offset: number): Promise => { + const geometry = await disclosure.evaluate((button, xOffset) => { + const row = button.closest('[role="treeitem"]')! + const rowBounds = row.getBoundingClientRect() + const buttonBounds = button.getBoundingClientRect() + const glyph = getComputedStyle(button.querySelector('svg')!) + const target = { x: rowBounds.left + xOffset, y: rowBounds.bottom - 3 } + const pseudo = getComputedStyle(button, '::after') + const status = row.querySelector('[data-state]')!.getBoundingClientRect() + return { + target, + outsideButton: target.x < buttonBounds.left || target.x > buttonBounds.right + || target.y < buttonBounds.top || target.y > buttonBounds.bottom, + pseudo: { width: pseudo.width, height: pseudo.height, top: pseudo.top, bottom: pseudo.bottom, left: pseudo.left }, + rowHeight: rowBounds.height, + glyph: { width: glyph.width, height: glyph.height }, + transform: getComputedStyle(button).transform, + hitsDisclosure: document.elementFromPoint(target.x, target.y) === button, + missesNextRow: document.elementFromPoint(target.x, rowBounds.bottom + 1) !== button, + endsBeforeStatus: rowBounds.left + 28 <= status.left, + missesStatus: document.elementFromPoint(status.left + status.width / 2, status.top + status.height / 2) !== button, + } + }, offset) + expect(geometry.outsideButton, JSON.stringify(geometry)).toBe(true) + expect(geometry.pseudo).toEqual({ width: '28px', height: `${geometry.rowHeight}px`, top: '0px', bottom: '0px', left: '0px' }) + expect(geometry.glyph).toEqual({ width: '12px', height: '12px' }) + expect(geometry.transform).toBe('none') + expect(geometry.hitsDisclosure).toBe(true) + expect(geometry.missesNextRow).toBe(true) + expect(geometry.endsBeforeStatus).toBe(true) + expect(geometry.missesStatus).toBe(true) + const body = childRow.locator(':scope > div') + await childRow.getByText(LABEL, { exact: true }).hover() + expect(await body.evaluate(element => getComputedStyle(element).backgroundColor)).not.toBe('rgba(0, 0, 0, 0)') + await page.mouse.move(geometry.target.x, geometry.target.y) + expect(await body.evaluate(element => getComputedStyle(element).backgroundColor)).toBe('rgba(0, 0, 0, 0)') + await page.mouse.click(geometry.target.x, geometry.target.y) + expect(page.url()).toBe(beforeDisclosureUrl) + expect(await page.getByText(PARENT_PROMPT, { exact: true }).isVisible()).toBe(true) + expect(await page.locator('[data-sidebar-chat]').count()).toBe(0) + } const observe = vi.spyOn(scaffold.ctx.sessionQuery, 'observeSession') try { const projections = page.waitForResponse(response => new URL(response.url()).pathname === '/api/session/projections') - await page.getByRole('button', { name: `Expand ${LABEL} descendants` }).click() + await clickDisclosureGutter(3) await projections await page.getByRole('treeitem', { name: new RegExp(NESTED_LABEL) }).waitFor({ timeout: 15_000 }) + expect(await sectionHeader.count()).toBe(0) expect(observe.mock.calls.some(([id]) => id === childId)).toBe(true) expect(observe.mock.calls.some(([id]) => id === grandchildId)).toBe(false) } finally { observe.mockRestore() } - const childRow = page.getByRole('treeitem', { name: new RegExp(LABEL) }) + await clickDisclosureGutter(26) + await page.getByRole('treeitem', { name: new RegExp(NESTED_LABEL) }).waitFor({ state: 'detached' }) + expect(await childRow.getAttribute('aria-expanded')).toBe('false') + await clickDisclosureGutter(3) + await page.getByRole('treeitem', { name: new RegExp(NESTED_LABEL) }).waitFor() + expect(await childRow.getAttribute('aria-expanded')).toBe('true') + const connector = await page.getByRole('treeitem', { name: new RegExp(NESTED_LABEL) }).evaluate((row) => { + const rowBounds = row.getBoundingClientRect() + const dot = row.querySelector('[data-state]')! + const dotBounds = dot.getBoundingClientRect() + const slot = dot.parentElement!.getBoundingClientRect() + const horizontal = getComputedStyle(row, '::before') + const node = row.parentElement! + const spine = getComputedStyle(node, '::before') + const stroke = Number.parseFloat(horizontal.borderTopWidth) + const colorProbe = document.createElement('span') + colorProbe.style.cssText = 'display:none;color:var(--dsw-alias-border-l3)' + row.append(colorProbe) + let expectedColor: string + try { + expectedColor = getComputedStyle(colorProbe).color + } finally { + colorProbe.remove() + } + return { + rowTop: rowBounds.top, + dotCenter: dotBounds.top + dotBounds.height / 2, + lineCenter: rowBounds.top + Number.parseFloat(horizontal.top) + stroke / 2, + lineEnd: rowBounds.left + Number.parseFloat(horizontal.left) + Number.parseFloat(horizontal.width), + slotLeft: slot.left, + spineEnd: node.getBoundingClientRect().top + Number.parseFloat(spine.top) + Number.parseFloat(spine.height), + lastSibling: node.nextElementSibling === null, + stroke, + color: horizontal.borderTopColor, + spineColor: spine.borderLeftColor, + expectedColor, + } + }) + expect(connector.lastSibling).toBe(true) + expect(Math.abs(connector.lineCenter - connector.dotCenter), JSON.stringify(connector)).toBeLessThanOrEqual(0.5) + expect(Math.abs(connector.spineEnd - connector.dotCenter), JSON.stringify(connector)).toBeLessThanOrEqual(0.5) + expect(Math.abs(connector.lineEnd - connector.slotLeft), JSON.stringify(connector)).toBeLessThanOrEqual(0.5) + expect(connector.stroke).toBeGreaterThanOrEqual(0.5) + expect(connector.stroke).toBeLessThanOrEqual(1) + expect(connector.color).toBe(connector.expectedColor) + expect(connector.spineColor).toBe(connector.expectedColor) const childLabel = await childRow.getAttribute('aria-label') await page.waitForTimeout(1_100) expect(await childRow.getAttribute('aria-label')).toBe(childLabel) - expect(await page.getByRole('treeitem', { name: new RegExp(NESTED_LABEL) }).textContent()).toContain('continuable') + expect(await page.getByRole('treeitem', { name: new RegExp(NESTED_LABEL) }).getAttribute('aria-label')).toContain('continuable') expect(scaffold.ctx.agents.get(childId)).toBeUndefined() expect(scaffold.ctx.agents.get(grandchildId)).toBeUndefined() const snapshot = await captureStableAria( @@ -420,7 +569,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () = scaffold.workspaceCwd, ) await compareOrRefreshGolden(TREE_EXPECTED, snapshot, MODE) - await page.getByRole('treeitem', { name: new RegExp(NESTED_LABEL) }).click() + await page.getByRole('treeitem', { name: new RegExp(NESTED_LABEL) }).getByText(grandchildId, { exact: true }).click() await page.getByText(NESTED_PROMPT).waitFor() const hierarchy = page.getByRole('navigation', { name: 'Session hierarchy' }) await hierarchy.getByRole('button', { name: `Switch subagent: ${LABEL}`, exact: true }).waitFor() @@ -435,7 +584,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () = await page.getByRole('button', { name: '2 subagents' }).hover() await page.getByRole('treeitem', { name: new RegExp(LABEL) }).getByRole('button', { name: `Open ${LABEL} in sidebar`, exact: true }).click() const sidebarChat = page.locator('[data-sidebar-chat]') - await sidebarChat.getByText(/^Explain event sourcing in one sentence\.Your parent agent id is /).waitFor({ timeout: 15_000 }) + await sidebarChat.getByText(/^Explain event sourcing in one sentence\./).waitFor({ timeout: 15_000 }) await compareOrRefreshGolden( SIDEBAR_CHAT_EXPECTED, await captureStableAria(page, '[data-sidebar-chat]', scaffold.workspaceCwd), @@ -456,16 +605,34 @@ describe('web e2e: persisted subagent conversation and human continuation', () = it('opens the completed child from persistence without activating it', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-subagent-open')) await page.getByRole('button', { name: '2 subagents' }).hover() - await page.getByRole('treeitem', { name: new RegExp(LABEL) }).click() + await page.getByRole('treeitem', { name: new RegExp(LABEL) }).getByText(LABEL, { exact: true }).click() await expect.poll( - () => page.getByText(/^Explain event sourcing in one sentence\.Your parent agent id is /).count(), + () => page.getByText(/^Explain event sourcing in one sentence\./).count(), { timeout: 15_000 }, ).toBe(1) if (scaffold.ctx.agents.get(childId) !== undefined) { throw new Error(`viewing the child activated it; API calls: ${apiCalls.join(', ')}`) } const hierarchy = page.getByRole('navigation', { name: 'Session hierarchy' }) - await hierarchy.getByRole('button', { name: `Switch subagent: ${LABEL}` }).waitFor() + const currentCrumb = hierarchy.getByRole('button', { name: `Switch subagent: ${LABEL}` }) + await currentCrumb.waitFor() + const crumbGeometry = await Promise.all([hierarchy.getByRole('button').first(), currentCrumb].map(crumb => crumb.evaluate((element) => { + const style = getComputedStyle(element) + const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT) + let text = walker.nextNode() + while (text !== null && text.textContent?.trim() === '') text = walker.nextNode() + if (text === null) throw new Error('Breadcrumb has no title text') + const range = document.createRange() + range.selectNodeContents(text) + const bounds = range.getBoundingClientRect() + return { fontSize: style.fontSize, lineHeight: style.lineHeight, top: bounds.top, bottom: bounds.bottom } + }))) + expect(crumbGeometry.map(({ fontSize, lineHeight }) => ({ fontSize, lineHeight }))) + .toEqual([{ fontSize: '14px', lineHeight: '20px' }, { fontSize: '14px', lineHeight: '20px' }]) + expect(Math.abs(crumbGeometry[0]!.top - crumbGeometry[1]!.top), JSON.stringify(crumbGeometry)).toBeLessThanOrEqual(1) + expect(Math.abs(crumbGeometry[0]!.bottom - crumbGeometry[1]!.bottom), JSON.stringify(crumbGeometry)).toBeLessThanOrEqual(1) + expect(await hierarchy.getByRole('button', { name: '1 subagent', exact: true }) + .evaluate(element => getComputedStyle(element).fontSize)).toBe('12px') const sidebar = await captureStableAria( page, '[role="tree"][aria-label="Sessions"]', @@ -494,7 +661,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () = await expect.poll(() => requested, { timeout: 15_000 }).toBe(true) expect(await page.getByText('This subagent is read-only for now', { exact: true }).count()).toBe(0) expect(await page.getByRole('navigation', { name: 'Session hierarchy' }).count()).toBe(0) - expect(await page.getByText(/^Explain event sourcing in one sentence\.Your parent agent id is /).count()).toBe(0) + expect(await page.getByText(/^Explain event sourcing in one sentence\./).count()).toBe(0) releaseCatalog() await page.getByRole('button', { name: `Switch subagent: ${LABEL}` }).waitFor({ timeout: 15_000 }) const input = page.getByRole('textbox', { name: 'Message or run a task, / commands, @ files or sessions' }) @@ -549,7 +716,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () = expect(tripwire.warnings).toEqual([]) }) - it('opens an unavailable persisted grandchild after recording the available child', async () => { + it('opens a persisted grandchild with input enabled while its creator is offline', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-subagent-grandchild')) await page.getByRole('button', { name: '1 subagent' }).hover() const tree = page.getByRole('tree', { name: 'Subagent sessions' }) @@ -562,7 +729,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () = const clickArea = nestedRow.locator(':scope > div') expect(await clickArea.count()).toBe(1) const [menuBox, clickAreaBox] = await Promise.all([ - tree.locator('..').boundingBox(), + page.locator('[data-subagent-panel]').boundingBox(), clickArea.boundingBox(), ]) expect(menuBox).not.toBeNull() @@ -570,27 +737,23 @@ describe('web e2e: persisted subagent conversation and human continuation', () = expect([ Math.round(clickAreaBox!.x - menuBox!.x), Math.round(menuBox!.x + menuBox!.width - clickAreaBox!.x - clickAreaBox!.width), - // The outer menu padding insets the scrollable tree rows. - ]).toEqual([4, 4]) + // Menu padding plus the branchless row's one-pixel leading inset. + ]).toEqual([7, 6]) await compareOrRefreshGolden( BRANCHLESS_EXPECTED, await captureStableAria(page, '[role="tree"][aria-label="Subagent sessions"]', scaffold.workspaceCwd), MODE, ) - await nestedRow.click() - await page.getByText('The parent session is offline; reopen it to continue sending messages.').waitFor() - // The offline banner renders from the descriptor alone, so it says nothing - // about the transcript below it. The golden pins that transcript, and - // `captureStableAria` calls two identical polls stable — including two of - // "Loading history…". Wait for the message the golden asserts. + await nestedRow.getByText(grandchildId, { exact: true }).click() await page.getByText(NESTED_PROMPT).waitFor() + expect(await page.getByRole('textbox', { name: 'Message or run a task, / commands, @ files or sessions' }).isEnabled()).toBe(true) const hierarchy = page.getByRole('navigation', { name: 'Session hierarchy' }) const crumbs = await hierarchy.getByRole('button').allTextContents() expect(crumbs.slice(-2)).toEqual([LABEL, NESTED_LABEL]) expect(scaffold.ctx.agents.get(childId)).toBeUndefined() expect(scaffold.ctx.agents.get(grandchildId)).toBeUndefined() await compareOrRefreshGolden( - UNAVAILABLE_GRANDCHILD_EXPECTED, + GRANDCHILD_EXPECTED, await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd), MODE, ) @@ -602,7 +765,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () = .getByRole('treeitem', { name: /Ask a research subagent to/ }) await parentSession.click() await page.getByRole('button', { name: '2 subagents' }).hover() - await page.getByRole('treeitem', { name: new RegExp(ONE_SHOT_LABEL) }).click() + await page.getByRole('treeitem', { name: new RegExp(ONE_SHOT_LABEL) }).getByText(oneShotId, { exact: true }).click() await page.getByText('One-shot tasks do not accept follow-ups; review the full execution record here.').waitFor() expect(scaffold.ctx.agents.get(oneShotId)).toBeUndefined() }) @@ -613,7 +776,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () = .getByRole('treeitem', { name: /Ask a research subagent to/ }) .click() await page.getByRole('button', { name: '2 subagents' }).hover() - await page.getByRole('treeitem', { name: new RegExp(LABEL) }).click() + await page.getByRole('treeitem', { name: new RegExp(LABEL) }).getByText(childId, { exact: true }).click() await page.getByRole('textbox', { name: 'Message or run a task, / commands, @ files or sessions' }).waitFor() const forkResponse = page.waitForResponse(response => new URL(response.url()).pathname === '/api/session/fork') @@ -642,7 +805,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () = const sessions = page.getByRole('tree', { name: 'Sessions' }) await sessions.getByRole('treeitem', { name: /Ask a research subagent to/ }).click() await page.getByRole('button', { name: '2 subagents' }).hover() - await page.getByRole('treeitem', { name: new RegExp(LABEL) }).click() + await page.getByRole('treeitem', { name: new RegExp(LABEL) }).getByText(childId, { exact: true }).click() await page.locator('[data-composer-input][contenteditable="true"]').first().waitFor() expect(scaffold.ctx.agents.get(childId)).toBeUndefined() @@ -659,7 +822,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () = await sessions.getByRole('treeitem', { name: /Ask a research subagent to/ }).click() await page.getByRole('button', { name: '2 subagents' }).press('ArrowDown') - await page.getByRole('treeitem', { name: new RegExp(LABEL) }).click() + await page.getByRole('treeitem', { name: new RegExp(LABEL) }).getByText(childId, { exact: true }).click() const input = page.locator('[data-composer-input][contenteditable="true"]').first() await input.waitFor() const promptResponse = page.waitForResponse(response => @@ -683,6 +846,56 @@ describe('web e2e: persisted subagent conversation and human continuation', () = expect(scaffold.ctx.agents.get(forkId)).not.toBeUndefined() await expect.poll(() => scaffold.ctx.agents.get(childId), { timeout: 10_000 }).toBeUndefined() }) + + it('shows shared tasks only in the root conversation while keeping child navigation', async () => { + const parent = scaffold.ctx.agents.get(parentId) + if (parent === undefined) throw new Error('task-board fixture requires the live parent') + const enabled = scaffold.ctx.agentTeams.isEnabled(parent) + onTestFinished(() => scaffold.ctx.agentTeams.setEnabled(parent, enabled)) + await scaffold.ctx.agentTeams.setEnabled(parent, true) + await scaffold.ctx.agentTeams.createTask(parent, { + subject: 'Root-only shared task', description: 'Inspect from the root conversation.', writeScopes: [], + }) + await page.getByRole('tree', { name: 'Sessions' }) + .getByRole('treeitem', { name: /Ask a research subagent to/ }).click() + await page.getByRole('button', { name: '2 subagents', exact: true }).click() + await page.locator('[data-team-panel]').getByText('Root-only shared task', { exact: true }).waitFor() + const rootHeader = page.locator('[data-subagent-section-header]') + expect(await rootHeader.textContent()).toBe('Subagents1 completed\u2002·\u20021 not running') + expect(await rootHeader.getByRole('button').count()).toBe(0) + expect(await rootHeader.locator('[data-icon="dsw-icon-users-outline-regular"]').count()).toBe(1) + const progress = await rootHeader.textContent() + await page.getByRole('button', { name: `Expand ${LABEL} descendants`, exact: true }).click() + await page.getByRole('treeitem', { name: new RegExp(NESTED_LABEL) }).waitFor() + expect(await rootHeader.textContent()).toBe(progress) + await page.getByRole('treeitem', { name: new RegExp(LABEL) }).getByText(childId, { exact: true }).click() + const switcher = page.getByRole('button', { name: `Switch subagent: ${LABEL}`, exact: true }) + await switcher.click() + const panel = page.locator('[data-subagent-panel]') + await panel.getByRole('treeitem', { name: new RegExp(ONE_SHOT_LABEL) }).waitFor() + expect(await panel.locator('[data-team-panel]').count()).toBe(0) + expect(await panel.evaluate(element => element.getBoundingClientRect().width)).toBe(360) + const header = panel.locator('[data-subagent-section-header]') + expect(await header.count()).toBe(0) + expect(await switcher.textContent()).toBe(LABEL) + const currentRow = panel.getByRole('treeitem', { name: new RegExp(LABEL) }) + const currentCheck = currentRow.locator('[data-subagent-current]') + expect(await currentRow.getAttribute('aria-current')).toBe('true') + expect(await currentCheck.getAttribute('aria-hidden')).toBe('true') + expect(await currentCheck.evaluate(element => element.tagName)).toBe('SPAN') + expect(await currentCheck.locator('button, [role="button"], [tabindex]').count()).toBe(0) + expect(await currentRow.getByRole('button', { name: `Open ${LABEL} in sidebar`, exact: true }).count()).toBe(0) + const otherRow = panel.getByRole('treeitem', { name: new RegExp(ONE_SHOT_LABEL) }) + expect(await otherRow.locator('[data-subagent-current]').count()).toBe(0) + expect(await otherRow.getByRole('button', { name: `Open ${ONE_SHOT_LABEL} in sidebar`, exact: true }).isEnabled()).toBe(true) + await compareOrRefreshGolden( + fileURLToPath(new URL('./expected/subagent-conversation/navigation-only.expected.md', import.meta.url)), + await captureStableAria(page, '[data-subagent-panel]', scaffold.workspaceCwd), MODE, + ) + await page.keyboard.press('Escape') + }) + + }) describe('web e2e: migration-required subagent catalog', () => { @@ -795,7 +1008,7 @@ describe('web e2e: migration-required subagent catalog', () => { expect(observe.mock.calls.some(([id]) => id === childId || id === grandchildId)).toBe(false) expect(projections).not.toHaveBeenCalled() - await childRow.click() + await childRow.getByText(childId, { exact: true }).click() await page.getByRole('button', { name: `Switch subagent: ${childLabel}`, exact: true }).waitFor() await page.getByRole('button', { name: '1 subagent', exact: true }).hover() const grandchildRow = tree.getByRole('treeitem', { name: new RegExp(grandchildLabel) }) diff --git a/apps/web/tests/subagent-fork-schedule.snapshot.ts b/apps/web/tests/subagent-fork-schedule.snapshot.ts index 1a674ab30b..00678b48f4 100644 --- a/apps/web/tests/subagent-fork-schedule.snapshot.ts +++ b/apps/web/tests/subagent-fork-schedule.snapshot.ts @@ -48,7 +48,7 @@ it.skipIf(MODE === 'record')('replays inherited schedule tools and refusals in a const activation = await scaffold.ctx.subagents.startActivation({ provider: 'fork', label: 'Check child reminder refusal', delivery: 'caller', signal: new AbortController().signal, - request: { parent, prompt: [...childPrompt.data.content] }, + parent, prompt: [...childPrompt.data.content], }) try { await activation.result diff --git a/apps/web/tests/subagent-interrupt-ui.e2e.ts b/apps/web/tests/subagent-interrupt-ui.e2e.ts index 7afeaa9a10..8c5f8fb1ea 100644 --- a/apps/web/tests/subagent-interrupt-ui.e2e.ts +++ b/apps/web/tests/subagent-interrupt-ui.e2e.ts @@ -1,7 +1,7 @@ // Web e2e scenario: the composer's independent Stop interrupts a running // continuable child. The child holds its model turn open through a replay -// hang entry; the browser proves Send and Stop coexist, the parent-offline -// disabled-Send-with-Stop composer, the subagents/interruptByParent +// hang entry; the browser proves Send and Stop coexist regardless of the +// creator's list availability, the subagents/interruptByUser // (never session.cancel) transport, the parked follow-up, and the FIFO resume // on a waking send. // @@ -144,7 +144,7 @@ describe.skipIf(MODE === 'record')('web e2e: composer interrupt for a running co await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) await connectFreshWorkspace(page, scaffold.workspaceCwd) - const root = scaffold.ctx.agents.roots()[0] + const root = scaffold.ctx.agents.list().filter(agent => agent.session.header.origin !== 'subagent')[0] if (root === undefined) throw new Error('fresh workspace did not publish its parent Agent') parent = root // The child's first model call claims the primary override and holds. @@ -153,7 +153,7 @@ describe.skipIf(MODE === 'record')('web e2e: composer interrupt for a running co provider: 'spawn', label: LABEL, signal: new AbortController().signal, - request: { prompt: [{ type: 'text', text: INITIAL }], parent }, + prompt: [{ type: 'text', text: INITIAL }], parent, }) childId = started.childId await waitFor(() => existsSync(readyFile), 'the held child turn to open') @@ -188,7 +188,7 @@ describe.skipIf(MODE === 'record')('web e2e: composer interrupt for a running co if (failures.length > 1) throw new AggregateError(failures, 'subagent interrupt UI teardown failed') }) - it('interrupts the live child through the parent-offline composer', async () => { + it('queues and interrupts the live child despite its creator being listed offline', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-subagent-interrupt-offline')) // The parent is unavailable in the Session list while the child stays live. const pattern = '**/api/session/list' @@ -210,31 +210,22 @@ describe.skipIf(MODE === 'record')('web e2e: composer interrupt for a running co await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) acknowledgeReloadConnectionLoss(tripwire, warningStart) await page.getByRole('button', { name: /1 subagent/ }).click() - await page.getByRole('treeitem', { name: new RegExp(LABEL) }).click() - const input = page.getByRole('textbox', { - name: 'Parent session offline; sending is unavailable but you can still stop the run', - }) + await page.getByRole('treeitem', { name: new RegExp(LABEL) }).press('Enter') + const input = page.locator('[data-composer-input]').first() await input.waitFor({ timeout: 15_000 }) - await page.getByText(/^Explain event sourcing in one sentence\.Your parent agent id is /) + await page.getByText(/^Explain event sourcing in one sentence\./) .waitFor({ timeout: 15_000 }) - expect(await input.isDisabled()).toBe(true) + expect(await input.isDisabled()).toBe(false) const stop = page.getByRole('button', { name: 'Stop generating' }) expect(await stop.count()).toBe(1) expect(await stop.isEnabled()).toBe(true) - const send = page.getByRole('button', { name: 'Send message' }) - expect(await send.count()).toBe(1) - expect(await send.isDisabled()).toBe(true) - // Keep the continuable Activation resident after this first abort. The - // direct setup queue also proves the ordinary row controls remain - // available while this parent-offline composer cannot submit new input. - await scaffold.ctx.subagents.prompt({ - requestId: 'interrupt-ui-rearm' as SubagentPromptRequestId, - parentSessionId: parent.id, - childSessionId: childId, - mode: 'continuable', - delivery: 'queue', - content: [{ type: 'text', text: REARM }], - }, new AbortController().signal) + const promptResponse = page.waitForResponse(response => + new URL(response.url()).pathname === '/api/subagents/prompt') + await input.fill(REARM) + const send = page.getByRole('button', { name: 'Queue message' }) + expect(await send.isEnabled()).toBe(true) + await send.click() + expect(((await (await promptResponse).json()) as { result: { ok: boolean } }).result).toMatchObject({ ok: true }) await page.getByRole('button', { name: 'Edit queued message' }).waitFor({ timeout: 15_000 }) expect(await page.getByRole('button', { name: 'Remove queued message' }).count()).toBe(1) expect(await page.getByRole('button', { name: 'Steer queued message' }).count()).toBe(1) @@ -246,7 +237,7 @@ describe.skipIf(MODE === 'record')('web e2e: composer interrupt for a running co const aborted = waitForAbortedTurn(scaffold, childId) const interruptResponse = page.waitForResponse(response => - new URL(response.url()).pathname === '/api/subagents/interruptByParent') + new URL(response.url()).pathname === '/api/subagents/interruptByUser') await stop.click() expect(((await (await interruptResponse).json()) as { result: { ok: boolean; value?: { accepted: boolean } } @@ -256,12 +247,10 @@ describe.skipIf(MODE === 'record')('web e2e: composer interrupt for a running co await expect.poll(() => scaffold.ctx.agents.get(childId)?.status, { timeout: 15_000 }).toBe('idle') // Wake the parked setup message only after cancellation converges. A - // second hang keeps the parent-available case independent from this stop. + // second hang keeps the next interrupt case independent from this stop. await scaffold.ctx.subagents.prompt({ requestId: 'interrupt-ui-rearm-wake' as SubagentPromptRequestId, - parentSessionId: parent.id, childSessionId: childId, - mode: 'continuable', delivery: 'queue', content: [{ type: 'text', text: REARM_WAKE }], }, new AbortController().signal) @@ -272,9 +261,8 @@ describe.skipIf(MODE === 'record')('web e2e: composer interrupt for a running co } }, 60_000) - it('interrupts through subagents/interruptByParent, parks the follow-up, and resumes it FIFO', async () => { + it('interrupts through subagents/interruptByUser, parks the follow-up, and resumes it FIFO', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-subagent-interrupt-flow')) - // A new connection reloads the truthful parent-availability hint. const warningStart = tripwire.warnings.length await page.reload({ waitUntil: 'load' }) await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) @@ -282,7 +270,7 @@ describe.skipIf(MODE === 'record')('web e2e: composer interrupt for a running co await page.getByRole('navigation', { name: 'Session hierarchy' }) .getByRole('button').first().click() await page.getByRole('button', { name: /1 subagent/ }).click() - await page.getByRole('treeitem', { name: new RegExp(LABEL) }).click() + await page.getByRole('treeitem', { name: new RegExp(LABEL) }).press('Enter') // A live continuable child advertises the ordinary steer-all gesture, so // that placeholder is the composer's accessible name in this window. It // changes back as the queue drains, so later interactions address the @@ -343,7 +331,7 @@ describe.skipIf(MODE === 'record')('web e2e: composer interrupt for a running co const stop = page.getByRole('button', { name: 'Stop generating' }) expect(await stop.count()).toBe(1) const interruptResponse = page.waitForResponse(response => - new URL(response.url()).pathname === '/api/subagents/interruptByParent') + new URL(response.url()).pathname === '/api/subagents/interruptByUser') await stop.click() expect(((await (await interruptResponse).json()) as { result: { ok: boolean; value?: { accepted: boolean } } @@ -378,7 +366,7 @@ describe.skipIf(MODE === 'record')('web e2e: composer interrupt for a running co ? event.data.content.flatMap(block => block.type === 'text' ? [block.text] : []) : []) expect(userTexts[0]).toBe(INITIAL) - expect(userTexts[1]).toMatch(/^Your parent agent id is .+send_message\(\{ agent_id: /) + expect(userTexts[1]).toMatch(/^Your final answer is delivered automatically if your parent is still online\./) expect(userTexts.slice(2)).toEqual([REARM, REARM_WAKE, EDITED_FOLLOWUP, WAKING]) const imageMessage = events.find(event => event.type === 'user/message' && event.data.content.some(block => block.type === 'image')) diff --git a/apps/web/tests/subagent-interrupt.e2e.ts b/apps/web/tests/subagent-interrupt.e2e.ts index a1b1284a19..526e115437 100644 --- a/apps/web/tests/subagent-interrupt.e2e.ts +++ b/apps/web/tests/subagent-interrupt.e2e.ts @@ -70,7 +70,7 @@ function textCompletion(text: string): object { } } -describe.skipIf(MODE === 'record')('web e2e: subagents/interruptByParent over the real composition', () => { +describe.skipIf(MODE === 'record')('web e2e: subagents/interruptByUser over the real composition', () => { let scaffold: WebScaffold let sidecarRoot: string let readyFile: string @@ -113,7 +113,7 @@ describe.skipIf(MODE === 'record')('web e2e: subagents/interruptByParent over th provider: 'spawn', label: 'event-sourcing researcher', signal: new AbortController().signal, - request: { prompt: [{ type: 'text', text: INITIAL }], parent }, + prompt: [{ type: 'text', text: INITIAL }], parent, }) childId = started.childId // The hang entry writes readyFile after its prefix chunks, immediately @@ -134,9 +134,7 @@ describe.skipIf(MODE === 'record')('web e2e: subagents/interruptByParent over th const queued = await remote<{ messageId: string }>(scaffold, 'subagents/prompt', { request: { requestId: randomUUID(), - parentSessionId: parentId, childSessionId: childId, - mode: 'continuable', delivery: 'queue', content: [{ type: 'text', text: FOLLOWUP }], }, @@ -144,10 +142,8 @@ describe.skipIf(MODE === 'record')('web e2e: subagents/interruptByParent over th expect(queued).toMatchObject({ ok: true }) const settled = scaffold.whenTurnSettled() - const interrupted = await remote<{ accepted: true }>(scaffold, 'subagents/interruptByParent', { + const interrupted = await remote<{ accepted: true }>(scaffold, 'subagents/interruptByUser', { childSessionId: childId, - parentSessionId: parentId, - mode: 'continuable', }) expect(interrupted).toMatchObject({ ok: true, value: { accepted: true } }) // accepted acknowledges the admitted cancel, not quiescence: wait for the @@ -169,9 +165,7 @@ describe.skipIf(MODE === 'record')('web e2e: subagents/interruptByParent over th const waking = await remote<{ messageId: string }>(scaffold, 'subagents/prompt', { request: { requestId: randomUUID(), - parentSessionId: parentId, childSessionId: childId, - mode: 'continuable', delivery: 'queue', content: [{ type: 'text', text: WAKING }], }, @@ -189,7 +183,7 @@ describe.skipIf(MODE === 'record')('web e2e: subagents/interruptByParent over th ? event.data.content.flatMap(block => block.type === 'text' ? [block.text] : []) : []) expect(userTexts[0]).toBe(INITIAL) - expect(userTexts[1]).toMatch(/^Your parent agent id is .+send_message\(\{ agent_id: /) + expect(userTexts[1]).toMatch(/^Your final answer is delivered automatically if your parent is still online\./) expect(userTexts.slice(2)).toEqual([FOLLOWUP, WAKING]) const turnEndKinds = events .filter(event => event.type === 'turn/end') diff --git a/apps/web/tests/subagent-result-notice.e2e.ts b/apps/web/tests/subagent-result-notice.e2e.ts new file mode 100644 index 0000000000..cff9482035 --- /dev/null +++ b/apps/web/tests/subagent-result-notice.e2e.ts @@ -0,0 +1,129 @@ +/** Keyless browser coverage of structured subagent outcomes and historical notice fallback. */ +import { mkdir } from 'node:fs/promises' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { chromium, type Browser, type BrowserContext } from 'playwright' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { createAssistantMessage, createUserMessage } from '@deepseek-ai/dsh-llm' +import { SessionId } from '@deepseek-ai/dsh-session' +import { createSettlementMessage } from '@deepseek-ai/dsh-subagent/src/continuation-messages.ts' +import type { SubagentStopReason } from '@deepseek-ai/dsh-subagent' +import { + acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden, + launchWebScaffold, watchConsole, webSnapshotMode, +} from './scaffold.ts' +import { connectFreshWorkspace, connectFreshWorkspaceZh } from './support.ts' + +const DIR = fileURLToPath(new URL('./expected/subagent-result-notice/', import.meta.url)) +const MODE = webSnapshotMode() +const OUTPUT = '## Research result\nThe original child reply stays available.' +const OUTCOMES = [ + { reason: 'completed', en: 'Subagent run completed', zh: '子智能体已完成任务' }, + { reason: 'aborted', en: 'Subagent run stopped', zh: '子智能体任务已停止' }, + { reason: 'error', en: 'Subagent run failed', zh: '子智能体执行失败' }, + { reason: 'max-tokens', en: 'Subagent reached the token limit', zh: '子智能体已达 token 上限' }, + { reason: 'refusal', en: 'Subagent declined the task', zh: '子智能体拒绝执行' }, +] as const satisfies readonly { reason: SubagentStopReason; en: string; zh: string }[] + +describe.skipIf(MODE === 'record')('web e2e: subagent result notices', () => { + let browser: Browser + beforeAll(async () => { browser = await chromium.launch() }) + afterAll(async () => { await browser?.close() }) + + it.each(['en', 'zh'] as const)('shows recorded outcomes and preserves notice bodies in %s', async (locale) => { + const scaffold = await launchWebScaffold() + let context: BrowserContext | undefined + try { + context = await browser.newContext({ + locale: locale === 'en' ? 'en-US' : 'zh-CN', timezoneId: 'Asia/Shanghai', viewport: { width: 1280, height: 900 }, + }) + const page = await context.newPage() + const tripwire = watchConsole(page) + await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) + await (locale === 'en' ? connectFreshWorkspace : connectFreshWorkspaceZh)(page, scaffold.workspaceCwd) + const agent = scaffold.ctx.agents.list()[0] + if (agent === undefined) throw new Error('Notice fixture has no connected Agent') + const session = agent.session + const appendTurn = (turn: number, message: ReturnType): void => { + const waking = message.source.kind !== 'user' + if (waking) session.append('agent/inbox/spliced', { target: 'next-turn', start: 0, inserted: [message] }) + session.append('turn/start', { turn }) + if (waking) session.append('agent/inbox/spliced', { target: 'next-turn', start: 0, removedCount: 1, inserted: [] }) + session.append('step/start', { turn, step: 1 }) + session.append('user/message', message, { surfaceOp: 'append' }) + session.append('assistant/message', { + turn, step: 1, stream: [], message: createAssistantMessage({ + content: [{ type: 'text', text: `Acknowledged ${turn}.` }], source: { provider: 'fixture', model: 'fixture' }, + }), + }, { surfaceOp: 'append' }) + session.append('step/end', { turn, step: 1 }) + session.append('turn/end', { turn, reason: { kind: 'completed' } }) + } + appendTurn(1, createUserMessage({ content: [{ type: 'text', text: 'Inspect child execution results.' }], source: { kind: 'user' } })) + const childId = SessionId('notice-result-child') + const notices: { title: string; message: ReturnType }[] = OUTCOMES.map(outcome => ({ + title: outcome[locale], + message: createSettlementMessage(childId, { + stopReason: outcome.reason, output: [{ type: 'text', text: OUTPUT }], + }), + })) + notices.push({ + title: OUTCOMES[0][locale], + message: createSettlementMessage(childId, { stopReason: 'completed', output: [] }), + }) + notices.push({ + title: locale === 'en' ? 'Subtask status updated' : '子任务状态更新', + message: createUserMessage({ + source: { kind: 'subagent-settled', form: 'notice', senderSessionId: childId, summary: 'Background subagent finished.' }, + content: [{ type: 'text', text: 'Background subagent finished.' }], + }), + }) + for (const [index, notice] of notices.entries()) appendTurn(index + 2, notice.message) + await scaffold.ctx.sessions.flush(session) + const snapshots: string[] = [] + for (const [index, notice] of notices.entries()) { + const selector = `[data-chat-turn="${index + 2}"] [data-turn-trigger]` + const card = page.locator(selector) + await card.waitFor({ state: 'attached' }) + const header = card.locator('button').first() + await expect.poll(() => header.locator(':scope > span').nth(1).textContent()).toBe(notice.title) + await header.click() + const original = notice.message.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('') + expect(await card.locator('[data-context-text]').textContent()).toBe(original) + snapshots.push(await captureStableAria(page, selector, scaffold.workspaceCwd)) + await header.click() + } + await page.setViewportSize({ width: 390, height: 844 }) + for (const card of await page.locator('[data-turn-trigger]').all()) { + const geometry = await card.evaluate((element) => { + const header = element.querySelector('button')! + const title = header.querySelectorAll('span')[1]! + const time = header.querySelector('time')! + return { + fits: element.scrollWidth <= element.clientWidth, + separate: title.getBoundingClientRect().right <= time.getBoundingClientRect().left, + } + }) + expect(geometry).toEqual({ fits: true, separate: true }) + } + const warningCount = tripwire.warnings.length + await page.reload({ waitUntil: 'load' }) + await page.locator('[data-turn-trigger]').first().waitFor({ state: 'attached' }) + for (const [index, notice] of notices.entries()) { + await expect.poll(() => page.locator(`[data-chat-turn="${index + 2}"] [data-turn-trigger] > button`).textContent()) + .toContain(notice.title) + } + acknowledgeReloadConnectionLoss(tripwire, warningCount) + if (MODE === 'refresh') await mkdir(DIR, { recursive: true }) + await compareOrRefreshGolden(join(DIR, `${locale}.expected.md`), snapshots.join('\n\n'), MODE) + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + } finally { + try { await context?.close() } finally { await scaffold.close() } + } + }) + + it('keeps the fixture inventory closed', async () => { + await assertFixtureInventory(DIR, ['en.expected.md', 'zh.expected.md']) + }) +}) diff --git a/apps/web/tests/subagent-summary.e2e.ts b/apps/web/tests/subagent-summary.e2e.ts new file mode 100644 index 0000000000..5d3c564b09 --- /dev/null +++ b/apps/web/tests/subagent-summary.e2e.ts @@ -0,0 +1,271 @@ +/** Keyless browser coverage of subagent task summaries and durable creation receipts. */ +import { mkdir } from 'node:fs/promises' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import type { Agent, AssistantStreamFrame } from '@deepseek-ai/dsh-agent' +import { + AssistantStreamAccumulator, LlmAttemptId, ToolCallId, + createAssistantMessage, createToolResultMessage, createUserMessage, + type StreamChunk, +} from '@deepseek-ai/dsh-llm' +import type { SessionEvent } from '@deepseek-ai/dsh-session' +import type { Browser, Locator, Page } from 'playwright' +import { chromium } from 'playwright' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { + acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden, + launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold, +} from './scaffold.ts' +import { connectFreshWorkspace, expandOwningTurnProcess, newEnglishPage } from './support.ts' + +const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/subagent-summary/', import.meta.url)) +const MODE = webSnapshotMode() +const DESCRIPTION = 'Review subagent task summaries' +const PROMPT = 'Inspect the subagent tool card across its lifecycle.\nCheck streaming arguments, creation receipts, and restored history.\nReturn findings without changing unrelated files.' +const SOURCE = { provider: 'deepseek-official', model: 'deepseek-v4-flash' } + +async function snapshotGolden(name: string, actual: string): Promise { + if (MODE === 'refresh') await mkdir(SNAPSHOT_DIR, { recursive: true }) + await compareOrRefreshGolden(join(SNAPSHOT_DIR, `${name}.expected.md`), actual, MODE) +} + +function startTurn(agent: Agent, turn: number): void { + agent.session.append('turn/start', { turn }) + agent.session.append('user/message', createUserMessage({ + content: [{ type: 'text', text: `Inspect subagent summary case ${turn}.` }], source: { kind: 'user' }, + }), { surfaceOp: 'append' }) + agent.session.append('step/start', { turn, step: 1 }) +} + +function appendCall(agent: Agent, turn: number, callId: ToolCallId, args: string, name = 'subagent'): SessionEvent<'tool/call'> { + agent.session.append('assistant/message', { + turn, step: 1, stream: [], + message: createAssistantMessage({ + content: [{ type: 'tool-call', id: callId, name, arguments: args }], source: SOURCE, + }), + }, { surfaceOp: 'append' }) + return agent.session.append('tool/call', { turn, step: 1, callId, name, arguments: args }) +} + +function finishCall(agent: Agent, call: SessionEvent<'tool/call'>, text: string, isError = false, meta?: SessionEvent<'tool/result'>['data']['meta']): void { + const { turn, step, callId } = call.data + agent.session.append('tool/result', { + turn, step, message: createToolResultMessage({ callId, content: [{ type: 'text', text }], isError }), + ...meta === undefined ? {} : { meta }, + }, { surfaceOp: 'append', sourceEventSeqs: [call.seq] }) + agent.session.append('step/end', { turn, step }) + agent.session.append('turn/end', { turn, reason: { kind: 'completed' } }) +} + +function toolSelector(callId: ToolCallId): string { + return `[data-chat-call-id="${callId}"] [data-tool="subagent"]` +} + +async function expectSummary(card: Locator, text: string): Promise { + const header = card.locator('[data-disclosure-row]').first() + await expect.poll(() => header.textContent(), { timeout: 10_000 }).toBe(`Create subagent${text}`) +} + +async function expandTask(card: Locator, prompt: string): Promise { + await expandOwningTurnProcess(card.page(), card) + const toggle = card.locator('[data-expandable]').first() + if (await toggle.getAttribute('aria-expanded') !== 'true') await toggle.click() + const task = card.locator('details').filter({ has: card.page().locator('summary', { hasText: /^Task$/u }) }) + await task.waitFor() + if (await task.getAttribute('open') === null) await task.locator('summary').click() + await expect.poll(() => task.locator('p').textContent()).toBe(prompt) +} + +describe.skipIf(MODE === 'record')('web e2e: subagent task summary', () => { + let scaffold: WebScaffold + let browser: Browser + let page: Page + let tripwire: ReturnType + let agent: Agent + + beforeAll(async () => { + scaffold = await launchWebScaffold() + browser = await chromium.launch() + page = await newEnglishPage(browser) + tripwire = watchConsole(page) + await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) + await page.locator('[class*="frame"]').waitFor() + await connectFreshWorkspace(page, scaffold.workspaceCwd) + const connected = scaffold.ctx.agents.list()[0] + if (connected === undefined) throw new Error('Subagent summary fixture has no connected Agent') + agent = connected + }) + + afterAll(async () => { + try { await browser?.close() } finally { await scaffold?.close() } + }) + + it('streams the description and preserves it through creation and restored history', async () => { + const callId = ToolCallId('subagent-summary-described') + const selector = toolSelector(callId) + const card = page.locator(selector) + const snapshots: string[] = [] + const capture = async (stage: string): Promise => { + await expandOwningTurnProcess(page, card) + snapshots.push(`## ${stage}\n\n${await captureStableAria(page, selector, scaffold.workspaceCwd)}`) + } + startTurn(agent, 1) + await scaffold.ctx.sessions.flush(agent.session) + await page.locator('[data-chat-turn="1"]').getByText('Inspect subagent summary case 1.', { exact: true }).waitFor() + + const attemptId = LlmAttemptId('subagent-summary-stream') + const stream = new AssistantStreamAccumulator() + let revision = 0 + let index = 0 + const emit = (frame: AssistantStreamFrame): void => { scaffold.ctx.emit('agent/assistant-stream', { agent, frame }) } + const push = (chunk: StreamChunk): void => { + const time = Date.now() + stream.push({ time, chunk }) + emit({ type: 'chunk', attemptId, revision: ++revision, index: index++, time, chunk }) + } + emit({ type: 'start', attemptId, revision: ++revision, turn: 1, step: 1 }) + push({ type: 'block-start', index: 0, blockType: 'tool-call' }) + push({ type: 'tool-call-delta', index: 0, id: callId, name: 'subagent', argumentsDelta: `{"prompt":${JSON.stringify(PROMPT)},` }) + await card.waitFor({ state: 'attached' }) + await expandOwningTurnProcess(page, card) + await expect.poll(() => card.getAttribute('data-state')).toBe('preparing') + await expectSummary(card, '') + await capture('Preparing without description') + push({ type: 'tool-call-delta', index: 0, id: callId, argumentsDelta: '"description":"Review subagent' }) + await expectSummary(card, 'Review subagent') + push({ type: 'tool-call-delta', index: 0, id: callId, argumentsDelta: ' task summaries\\nAdditional description context' }) + await expectSummary(card, DESCRIPTION) + await capture('Preparing with partial description') + push({ type: 'tool-call-delta', index: 0, id: callId, argumentsDelta: '"}' }) + const args = JSON.stringify({ prompt: PROMPT, description: `${DESCRIPTION}\nAdditional description context` }) + const block = { type: 'tool-call' as const, id: callId, name: 'subagent', arguments: args } + push({ type: 'block-end', index: 0, block }) + push({ type: 'finish', reason: { kind: 'tool-calls' } }) + const message = agent.session.append('assistant/message', { + turn: 1, step: 1, stream: [...stream.snapshot()], + message: createAssistantMessage({ content: [block], source: SOURCE }), + }, { surfaceOp: 'append' }) + emit({ type: 'end', attemptId, revision: ++revision, index, outcome: { kind: 'committed', eventType: 'assistant/message', seq: message.seq } }) + const call = agent.session.append('tool/call', { turn: 1, step: 1, callId, name: 'subagent', arguments: args }) + await scaffold.ctx.sessions.flush(agent.session) + await expect.poll(() => card.getAttribute('data-state')).toBe('running') + await expectSummary(card, DESCRIPTION) + await capture('Running') + + finishCall(agent, call, 'Delegation accepted.', false, { kind: 'activation', subagentId: 'child-foo' }) + await scaffold.ctx.sessions.flush(agent.session) + await expect.poll(() => card.getAttribute('data-state')).toBe('ok') + await expectSummary(card, DESCRIPTION) + await capture('Created') + await expandTask(card, PROMPT) + await expectSummary(card, DESCRIPTION) + expect(await card.getByText('Agent response', { exact: true }).count()).toBe(0) + await card.getByText('Started', { exact: true }).waitFor() + expect(await card.locator('dt').allTextContents()).toEqual(['Agent task description', 'ID']) + expect(await card.locator('dd').allTextContents()).toEqual([`${DESCRIPTION}\nAdditional description context`, 'child-foo']) + await capture('Creation receipt and complete task') + + const beforeReload = tripwire.warnings.length + await page.reload({ waitUntil: 'load' }) + await card.waitFor({ state: 'attached' }) + await expandOwningTurnProcess(page, card) + await expectSummary(card, DESCRIPTION) + await expandTask(card, PROMPT) + await expectSummary(card, DESCRIPTION) + expect(await card.getByText('Agent response', { exact: true }).count()).toBe(0) + await capture('Restored creation receipt') + acknowledgeReloadConnectionLoss(tripwire, beforeReload) + await snapshotGolden('lifecycle', snapshots.join('\n\n')) + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + }) + + it('falls back to the prompt, keeps synchronous replies in details, and preserves errors', async () => { + const cases = [ + { id: 'fallback', args: { prompt: 'Review the fallback summary.\nKeep the complete task in details.' }, summary: 'Review the fallback summary.', result: 'started subagent fallback-worker' }, + { id: 'reply', args: { description: 'Check synchronous agent reply', prompt: 'Review the synchronous report.\nKeep every instruction available.' }, summary: 'Check synchronous agent reply', result: 'The synchronous agent found two issues.\nBoth are documented in the report.' }, + { id: 'title-only', args: {}, summary: '', result: 'started subagent empty-worker' }, + { id: 'error', args: { description: 'Delegate a failing task', prompt: 'Attempt the requested delegation.' }, summary: 'Subagent capacity exceeded', result: 'Subagent capacity exceeded\nWait for an active agent to finish.', error: true }, + ] + const snapshots: string[] = [] + for (const [index, fixture] of cases.entries()) { + const turn = index + 2 + const callId = ToolCallId(`subagent-summary-${fixture.id}`) + const selector = toolSelector(callId) + const card = page.locator(selector) + startTurn(agent, turn) + const call = appendCall(agent, turn, callId, JSON.stringify(fixture.args)) + await scaffold.ctx.sessions.flush(agent.session) + await card.waitFor({ state: 'attached' }) + await expandOwningTurnProcess(page, card) + await expectSummary(card, fixture.args.description ?? fixture.args.prompt?.split('\n')[0] ?? '') + finishCall(agent, call, fixture.result, fixture.error) + await scaffold.ctx.sessions.flush(agent.session) + await expect.poll(() => card.getAttribute('data-state')).toBe(fixture.error === true ? 'error' : 'ok') + await expectSummary(card, fixture.summary) + if (fixture.args.prompt !== undefined && fixture.error !== true) { + await expandTask(card, fixture.args.prompt) + await expectSummary(card, fixture.summary) + if (fixture.id === 'reply') { + await card.getByText('Agent response', { exact: true }).waitFor() + await card.getByText('The synchronous agent found two issues.', { exact: false }).waitFor() + await card.getByText('Both are documented in the report.', { exact: false }).waitFor() + expect(await card.getByText('Started', { exact: true }).count()).toBe(0) + } else expect(await card.getByText('Agent response', { exact: true }).count()).toBe(0) + } + await expandOwningTurnProcess(page, card) + snapshots.push(`## ${fixture.id}\n\n${await captureStableAria(page, selector, scaffold.workspaceCwd)}`) + } + await snapshotGolden('variants', snapshots.join('\n\n')) + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + }) + + it('presents fork creation with a distinct title, task summary, and restored receipt', async () => { + const name = 'subagent_fork' + const callId = ToolCallId('subagent-fork-summary') + const selector = `[data-chat-call-id="${callId}"] [data-tool="${name}"]` + const card = page.locator(selector) + const description = 'Review inherited conversation context' + const prompt = 'Summarize the earlier agreed requirements.\nDo not modify files.' + const snapshots: string[] = [] + const capture = async (stage: string): Promise => { + snapshots.push(`## ${stage}\n\n${await captureStableAria(page, selector, scaffold.workspaceCwd)}`) + } + const expectForkSummary = async (): Promise => { + await expect.poll(() => card.locator('[data-disclosure-row]').first().textContent()) + .toBe(`Fork subagent${description}`) + } + startTurn(agent, 6) + const call = appendCall(agent, 6, callId, JSON.stringify({ description, prompt }), name) + await scaffold.ctx.sessions.flush(agent.session) + await card.waitFor({ state: 'attached' }) + await expandOwningTurnProcess(page, card) + await expect.poll(() => card.getAttribute('data-state')).toBe('running') + await expectForkSummary() + await capture('Running') + finishCall(agent, call, 'started subagent fork-worker') + await scaffold.ctx.sessions.flush(agent.session) + await expect.poll(() => card.getAttribute('data-state')).toBe('ok') + await expandTask(card, prompt) + await expectForkSummary() + expect(await card.locator('dt').allTextContents()).toEqual(['Agent task description', 'ID']) + expect(await card.locator('dd').allTextContents()).toEqual([description, 'fork-worker']) + expect(await card.getByText('Agent response', { exact: true }).count()).toBe(0) + await capture('Creation receipt and complete task') + const beforeReload = tripwire.warnings.length + await page.reload({ waitUntil: 'load' }) + await card.waitFor({ state: 'attached' }) + await expandTask(card, prompt) + await expectForkSummary() + await capture('Restored creation receipt') + acknowledgeReloadConnectionLoss(tripwire, beforeReload) + await snapshotGolden('fork', snapshots.join('\n\n')) + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + }) + + it('keeps the fixture inventory closed', async () => { + await assertFixtureInventory(SNAPSHOT_DIR, ['lifecycle.expected.md', 'variants.expected.md', 'fork.expected.md']) + }) +}) diff --git a/apps/web/tests/support-timezone.e2e.ts b/apps/web/tests/support-timezone.e2e.ts index 37e4de79b1..706c96a49d 100644 --- a/apps/web/tests/support-timezone.e2e.ts +++ b/apps/web/tests/support-timezone.e2e.ts @@ -1,6 +1,6 @@ import { chromium } from 'playwright' import { expect, it } from 'vitest' -import { newEnglishPage } from './support.ts' +import { newEnglishPage, pinBrowserClock, pinHostClock, WEB_FIXTURE_TIME } from './support.ts' it.each(['UTC', 'America/Los_Angeles'])('isolates the recorded browser timezone from %s', async (hostTimeZone) => { const browser = await chromium.launch({ env: { ...process.env, TZ: hostTimeZone } }) @@ -20,3 +20,40 @@ it.each(['UTC', 'America/Los_Angeles'])('isolates the recorded browser timezone await browser.close() } }) + +it('keeps browser timers and animation frames live with the Host date pinned, including reload', async () => { + const originalNow = Date.now + const hostClock = pinHostClock() + try { + const browser = await chromium.launch() + try { + const page = await newEnglishPage(browser) + await pinBrowserClock(page) + await page.goto('data:text/html,clock') + let previousTime = WEB_FIXTURE_TIME + for (const reload of [false, true]) { + if (reload) await page.reload() + const now = await page.evaluate(() => Date.now()) + if (reload) expect(now).toBeGreaterThan(previousTime) + else expect(now).toBeGreaterThanOrEqual(previousTime) + expect(now).toBeLessThan(WEB_FIXTURE_TIME + 60_000) + previousTime = now + await page.evaluate(() => { + setTimeout(() => { document.body.dataset.timer = 'fired' }, 0) + requestAnimationFrame(() => { document.body.dataset.frame = 'fired' }) + }) + await expect.poll(() => page.evaluate(() => ({ + timer: document.body.dataset.timer, frame: document.body.dataset.frame, + })), { timeout: 5_000 }).toEqual({ timer: 'fired', frame: 'fired' }) + } + await page.close() + const nextPage = await newEnglishPage(browser) + expect(await nextPage.evaluate(() => Math.abs(Date.now() - new Date().getTime()))).toBeLessThan(100) + } finally { + await browser.close() + } + } finally { + hostClock.mockRestore() + } + expect(Date.now).toBe(originalNow) +}) diff --git a/apps/web/tests/support.ts b/apps/web/tests/support.ts index ad741f474d..048e3e1f6d 100644 --- a/apps/web/tests/support.ts +++ b/apps/web/tests/support.ts @@ -25,6 +25,36 @@ export function experimentalBundleDescription(bundle: string, locale: string): s const installationRequire = createRequire(join(REPO_ROOT, 'apps/cli/package.json')) +/** + * Check the shared warning title, theme color, and compact body alignment. + * @param dialog - visible warning dialog under test. + */ +export async function assertWarningDialogLayout(dialog: Locator): Promise { + const layout = await dialog.evaluate((element) => { + const title = element.querySelector('h2')! + const icon = title.querySelector('svg')! + const paragraph = element.querySelector('p')! + const titleRect = title.getBoundingClientRect() + const bodyRect = paragraph.getBoundingClientRect() + const color = getComputedStyle(icon).color + const previous = icon.style.color + icon.style.color = 'var(--dsw-alias-state-warn-primary)' + const warningColor = getComputedStyle(icon).color + icon.style.color = previous + return { + decorative: icon.getAttribute('aria-hidden'), + matchesWarningColor: color === warningColor, + leftGap: Math.abs(bodyRect.left - titleRect.left), + topGap: bodyRect.top - titleRect.bottom, + } + }) + expect(layout.decorative).toBe('true') + expect(layout.matchesWarningColor).toBe(true) + expect(layout.leftGap).toBeLessThan(1) + expect(layout.topGap).toBeGreaterThanOrEqual(0) + expect(layout.topGap).toBeLessThanOrEqual(16) +} + /** * The built copy of a workspace package, as the dsh installation resolves it. * The Host plugins a scaffold profile loads run from built packages through diff --git a/apps/web/tests/workflow-run.e2e.ts b/apps/web/tests/workflow-run.e2e.ts index e43a7c7fb8..7fbd53a759 100644 --- a/apps/web/tests/workflow-run.e2e.ts +++ b/apps/web/tests/workflow-run.e2e.ts @@ -220,7 +220,7 @@ describe.skipIf(MODE === 'record')('web e2e: durable workflow run in Chat', () = await page.getByRole('button', { name: '1 subagent', exact: true }).hover() const catalog = page.getByRole('tree', { name: 'Subagent sessions' }) - await catalog.getByRole('treeitem', { name: /snapshot-flow child 1/ }).click() + await catalog.getByRole('treeitem', { name: /snapshot-flow child 1/ }).press('Enter') await page.getByText(CHILD_PROMPT, { exact: true }).waitFor({ timeout: 15_000 }) expect(await page.locator('[data-composer-input][contenteditable="true"]').count()).toBe(1) }, 60_000) diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 47acf9c48c..9b3977d4ee 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -49,6 +49,8 @@ "tests/code-card-layout.e2e.ts", "tests/code-language.snapshot.ts", "tests/scaffold.ts", + "tests/subagent-summary.e2e.ts", + "tests/subagent-result-notice.e2e.ts", "tests/clock.e2e.ts", "tests/connection-recovery.e2e.ts", "tests/server-restart.e2e.ts", diff --git a/benchmarks/agent-continuation/child-catalog.worker.ts b/benchmarks/agent-continuation/child-catalog.worker.ts index aedb279937..34808c2d85 100644 --- a/benchmarks/agent-continuation/child-catalog.worker.ts +++ b/benchmarks/agent-continuation/child-catalog.worker.ts @@ -81,7 +81,12 @@ async function run(root: string, mode: string): Promise ctx.subagents.listChildren(PARENT_ID) + const readCatalog = async () => { + using observation = await ctx.sessionQuery.observeSession(PARENT_ID) + const catalog = observation.projections?.values.subagentCatalog + if (catalog === undefined) throw new Error('child-catalog projection is unavailable') + return catalog + } const cpuStart = process.cpuUsage() const start = performance.now() const first = await readCatalog() diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index 72dc6b08a6..19453e15a9 100644 --- a/docs/architecture.i18n.yaml +++ b/docs/architecture.i18n.yaml @@ -21,8 +21,8 @@ en: 20ef882e44b06a10 zh: c10c3934ed5570e9 /deepseek-harness-architecture/events: - en: 693407efb233f961 - zh: 37b9dd0677d4405c + en: ddebac7cfd41ec73 + zh: 51e6d0866a9e7839 /deepseek-harness-architecture/turn-flow: en: 1a79a70b62c04cdb zh: c86d765e21430405 @@ -30,8 +30,8 @@ en: b3b63d8fe7cab9fe zh: 3703fcf30b1639d0 /deepseek-harness-architecture/capability-seams: - en: a5bc20c3f2a1b370 - zh: 91773a281410b2f4 + en: 1d425c445bfcc931 + zh: 834d486f88e68edf /deepseek-harness-architecture/where-new-behavior-goes: en: ee73b1861378b099 zh: 9a182be609485f67 diff --git a/docs/architecture.md b/docs/architecture.md index e3f5cd24a3..446c50e5bd 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -66,15 +66,17 @@ Electron starts the private Desktop Host in Electron Node mode. The Host invokes ## Events -Events are the extension points, and picking the right domain is the first decision in most changes. +Events provide extension points in three domains. - **Session events** are durable facts appended to the log and broadcast through `session/event`. Use one when the fact must survive a reload. - **Agent events** (`agent/*`) carry a live `Agent`: inbox, step, status, request, validation, continuation. Use one to observe or intercept work in flight. - **Capability events** attach policy and adapters to a seam (`fs/*`, `tools/*`, `telemetry/*`) without importing the loop. -AgentLoop initializes Agents with rollback and awaits serial `agent/created` before queued work. Unload retains resources through Agent teardown; [agent-loop](../packages/core/agent-loop/README.md#understand-the-implementation) defines ordering. +Subagent Activations own runtime tree membership. -The [event map](event-producer-consumer.md) lists every event's producers and consumers. +AgentLoop rolls back failed initialization and awaits serial `agent/created` before queued work; unload retains resources through teardown ([ordering](../packages/core/agent-loop/README.md#understand-the-implementation)). + +[Event producers and consumers](event-producer-consumer.md). ## Turn flow @@ -137,7 +139,7 @@ A **seam** is a swappable capability with three roles: a **Service Definition** Seams are why one provider swap changes the whole product. Filesystem and subprocess providers share one execution world, so pointing them at a remote sandbox moves Bash, PTY, and LSP with them, with no provider forks. [Subagent providers](subsystems/subagent.md) vary just as widely behind one interface, from a fresh child agent to a delegated turn in another product. -[Experimental Agent Teams](subsystems/agent-team.md) exposes opt-in `ctx.agentTeams` coordination with durable roster and task state plus direct inbox messaging over continuable subagents. +[Agent Teams](subsystems/agent-team.md) adds task policy and a shared task board when enabled for a root Session. Ordinary subagent controls own identity, messaging, discovery, interruption, and inbox waiting. ## Where new behavior goes diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index 4ab73a419d..275e1c863d 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -68,15 +68,17 @@ Electron 使用 Electron Node 模式启动私有 Desktop Host。Host 调用共 ## 事件 -事件就是扩展点,而选对事件域是大多数改动的第一个决定。 +事件在三个域内提供扩展点。 - **会话事件**是追加到日志并通过 `session/event` 广播的持久事实。当某个事实必须在重新加载后仍然存在时,使用它。 - **Agent 事件**(`agent/*`)携带活跃 `Agent`:inbox、步骤、状态、请求、验证、续跑。要观察或拦截进行中的工作时,使用它。 - **能力事件**无需导入循环即可向某个 seam(`fs/*`、`tools/*`、`telemetry/*`)附加策略和适配器。 -AgentLoop 初始化 Agent,失败则回滚,并在启动已排队工作前等待串行 `agent/created`。卸载在 Agent teardown 期间保留所需资源;[agent-loop](../packages/core/agent-loop/README.zh.md#understand-the-implementation)定义顺序。 +Subagent Activation 拥有运行时树成员关系。 -[事件映射](event-producer-consumer.zh.md)列出每个事件的生产方与消费方。 +AgentLoop 回滚失败的初始化,并在启动已排队工作前等待串行 `agent/created`;卸载在 teardown 期间保留所需资源([顺序](../packages/core/agent-loop/README.zh.md#understand-the-implementation))。 + +[事件生产者与消费者](event-producer-consumer.zh.md)。 @@ -141,7 +143,7 @@ Session 消费方只了解当前逻辑格式。仅 header 的 `stat` 与 `list` seam 正是替换一个提供方就能改变整个产品的原因。文件系统与进程提供方共享同一个执行世界,因此把它们指向远程沙箱,也就把 Bash、PTY 和 LSP 一并搬了过去,无需提供方专用 fork。[subagent 提供方](subsystems/subagent.zh.md)在同一个接口之后同样千差万别,从新建一个子 agent,到把一个轮次委派给另一个产品。 -[实验性 Agent Teams](subsystems/agent-team.zh.md) 通过显式启用的 `ctx.agentTeams`,在可继续 subagent 之上提供持久 roster、任务状态和直接 inbox 消息协作。 +[Agent Teams](subsystems/agent-team.zh.md)在根 Session 开启后提供任务策略和共享任务板。普通 subagent 控制工具负责身份、通信、发现、中断和收件箱等待。 ## 新行为的归属位置 diff --git a/docs/capability-seams.i18n.yaml b/docs/capability-seams.i18n.yaml index 07016423f1..1083094774 100644 --- a/docs/capability-seams.i18n.yaml +++ b/docs/capability-seams.i18n.yaml @@ -6,5 +6,5 @@ en: 41d772192075e133 zh: 5ef3da69b4de1d8a /capability-seams-and-core-services: - en: 45dd67234744e29d - zh: a250e73f5b159175 + en: 3c972fccec17a890 + zh: 94c82847a3c8b83c diff --git a/docs/capability-seams.md b/docs/capability-seams.md index 9da61dd3e7..e333a79f71 100644 --- a/docs/capability-seams.md +++ b/docs/capability-seams.md @@ -267,9 +267,9 @@ flowchart LR pkg_experimental_speech_to_text["experimental-speech-to-text"] svc_speechToText["ctx.speechToText
Experimental speech recognition providers"] pkg_experimental_speech_to_text_sensevoice["experimental-speech-to-text-sensevoice"] - pkg_experimental_agent_team["experimental-agent-team"] + pkg_agent_team["agent-team"] svc_agentTeams["ctx.agentTeams
Agent Teams coordination domain"] - pkg_experimental_tool_agent_team["experimental-tool-agent-team"] + pkg_tool_agent_team["tool-agent-team"] pkg_experimental_claude_code_mods["experimental-claude-code-mods"] svc_claudeCodeMods["ctx.claudeCodeMods
Claude Code mods bridge"] pkg_inspector["inspector"] @@ -320,6 +320,7 @@ flowchart LR pkg_agent_default_model --> svc_agentDefaultModel pkg_agent_loop --> svc_agentLoop pkg_agent_preset_registry --> svc_agentPresets + pkg_agent_team --> svc_agentTeams pkg_api_gateway --> svc_typertGateway pkg_api_job_controller --> svc_jobController pkg_api_remote_access --> svc_remoteAccess @@ -359,7 +360,6 @@ flowchart LR pkg_deepseek_account --> svc_deepseekAccount pkg_deepseek_account_platform --> svc_deepseekAccount pkg_deepseek_llm_api_extensions --> svc_deepseekLlmApiExtensions - pkg_experimental_agent_team --> svc_agentTeams pkg_experimental_api_speech_to_text --> svc_speechController pkg_experimental_browser_use_chrome_devtools_mcp --> svc_browserUse pkg_experimental_browser_use_playwright_mcp --> svc_browserUse @@ -488,7 +488,7 @@ flowchart LR svc_agentDefaultModel --> pkg_headless svc_agentLoop --> pkg_base svc_agentLoop --> pkg_sdk_minimal - svc_agentTeams --> pkg_experimental_tool_agent_team + svc_agentTeams --> pkg_tool_agent_team svc_agents --> pkg_acp svc_agents --> pkg_agent_loop svc_agents --> pkg_subagent @@ -742,7 +742,7 @@ flowchart LR | `ctx.compaction` | `seam` | [`compaction`](../packages/compaction/compaction) | [`compaction-basic`](../packages/compaction/compaction-basic) | [`compaction-basic`](../packages/compaction/compaction-basic) | - | The basic backend consumes post-step pressure and request-error recovery events; there is no model-facing compact tool. | | `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process), [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code), [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`tool-ralph`](../packages/experimental/tool-ralph) | - | Providers prepare local children or execute external tasks; one activation manager owns both lifecycles. The delegation tool selects a backend, control tools deliver local follow-ups, and workflows await activation results. | | `ctx.speechToText` | `seam` | [`experimental-speech-to-text`](../packages/experimental/speech-to-text) | [`experimental-speech-to-text-sensevoice`](../packages/experimental/speech-to-text-sensevoice) | [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text) | - | Routes explicit recognizers; the browser uses the authenticated Remote and keeps transcripts in the draft until submission. | -| `ctx.agentTeams` | `core` | [`experimental-agent-team`](../packages/experimental/agent-team) | - | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | - | Owns the implicit-root roster, direct peer inbox delivery, shared task DAG, and continuable-child lifecycle; tool-agent-team contributes model controls. | +| `ctx.agentTeams` | `core` | [`agent-team`](../packages/team/agent-team) | - | [`tool-agent-team`](../packages/team/tool-agent-team) | - | Owns the shared task DAG for a subagent tree; tool-agent-team contributes task controls. | | `ctx.claudeCodeMods` | `core` | [`experimental-claude-code-mods`](../packages/experimental/claude-code-mods) | - | - | - | Loads mods that defineMod plugins add, raises their hook chains from harness extension points, and draws the band above the prompt over its Remote. | | `ctx.inspector` | `core` | `inspector` | - | - | - | Owns the Worker-hosted CDP target and the transport-independent Host and Client observation and Cordis-tree query API. | | `ctx.jobs` | `seam` | [`jobs`](../packages/jobs/jobs) | [`jobs-local`](../packages/jobs/jobs-local) | [`tool-bash`](../packages/shell/tool-bash), [`tool-pwsh`](../packages/shell/tool-pwsh), [`tool-terminal`](../packages/experimental/tool-terminal), [`tool-jobs`](../packages/jobs/tool-jobs), [`api-job-controller`](../packages/api/job-controller) | - | Producers (background bash/pwsh and PTY sends) register running work; record-declaring jobs additionally stream raw output for non-consuming observers; tool-jobs is the model-facing controller that reads, lists, and kills it; jobs-local is the process-local registry. | diff --git a/docs/capability-seams.zh.md b/docs/capability-seams.zh.md index f2036d67fb..cf3f2e67fc 100644 --- a/docs/capability-seams.zh.md +++ b/docs/capability-seams.zh.md @@ -269,9 +269,9 @@ flowchart LR pkg_experimental_speech_to_text["experimental-speech-to-text"] svc_speechToText["ctx.speechToText
Experimental speech recognition providers"] pkg_experimental_speech_to_text_sensevoice["experimental-speech-to-text-sensevoice"] - pkg_experimental_agent_team["experimental-agent-team"] + pkg_agent_team["agent-team"] svc_agentTeams["ctx.agentTeams
Agent Teams coordination domain"] - pkg_experimental_tool_agent_team["experimental-tool-agent-team"] + pkg_tool_agent_team["tool-agent-team"] pkg_experimental_claude_code_mods["experimental-claude-code-mods"] svc_claudeCodeMods["ctx.claudeCodeMods
Claude Code mods bridge"] pkg_inspector["inspector"] @@ -322,6 +322,7 @@ flowchart LR pkg_agent_default_model --> svc_agentDefaultModel pkg_agent_loop --> svc_agentLoop pkg_agent_preset_registry --> svc_agentPresets + pkg_agent_team --> svc_agentTeams pkg_api_gateway --> svc_typertGateway pkg_api_job_controller --> svc_jobController pkg_api_remote_access --> svc_remoteAccess @@ -361,7 +362,6 @@ flowchart LR pkg_deepseek_account --> svc_deepseekAccount pkg_deepseek_account_platform --> svc_deepseekAccount pkg_deepseek_llm_api_extensions --> svc_deepseekLlmApiExtensions - pkg_experimental_agent_team --> svc_agentTeams pkg_experimental_api_speech_to_text --> svc_speechController pkg_experimental_browser_use_chrome_devtools_mcp --> svc_browserUse pkg_experimental_browser_use_playwright_mcp --> svc_browserUse @@ -490,7 +490,7 @@ flowchart LR svc_agentDefaultModel --> pkg_headless svc_agentLoop --> pkg_base svc_agentLoop --> pkg_sdk_minimal - svc_agentTeams --> pkg_experimental_tool_agent_team + svc_agentTeams --> pkg_tool_agent_team svc_agents --> pkg_acp svc_agents --> pkg_agent_loop svc_agents --> pkg_subagent @@ -744,7 +744,7 @@ flowchart LR | `ctx.compaction` | `seam` | [`compaction`](../packages/compaction/compaction) | [`compaction-basic`](../packages/compaction/compaction-basic) | [`compaction-basic`](../packages/compaction/compaction-basic) | - | 基础后端消费步骤后的压力事件和请求错误恢复事件;不存在面向模型的压缩工具。 | | `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process), [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code), [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`tool-ralph`](../packages/experimental/tool-ralph) | - | 提供方准备本地子级或执行外部任务;一个 activation 管理器持有两类生命周期。委派工具选择后端,控制工具投递本地后续消息,工作流等待 activation 结果。 | | `ctx.speechToText` | `seam` | [`experimental-speech-to-text`](../packages/experimental/speech-to-text) | [`experimental-speech-to-text-sensevoice`](../packages/experimental/speech-to-text-sensevoice) | [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text) | - | 路由显式选择的识别器;浏览器使用带认证的 Remote,并在提交前将转写保留在草稿中。 | -| `ctx.agentTeams` | `core` | [`experimental-agent-team`](../packages/experimental/agent-team) | - | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | - | 负责隐式 Root roster、直接 peer inbox 投递、共享任务 DAG 与 continuable child 生命周期;tool-agent-team 提供模型控制工具。 | +| `ctx.agentTeams` | `core` | [`agent-team`](../packages/team/agent-team) | - | [`tool-agent-team`](../packages/team/tool-agent-team) | - | 负责子代理树共享的任务 DAG;tool-agent-team 提供任务控制工具。 | | `ctx.claudeCodeMods` | `core` | [`experimental-claude-code-mods`](../packages/experimental/claude-code-mods) | - | - | - | 加载 defineMod 插件添加的模组,从 harness 扩展点触发其钩子链,并通过其 Remote 绘制提示框上方的横幅。 | | `ctx.inspector` | `core` | `inspector` | - | - | - | 负责 Worker 托管的 CDP target,以及独立于传输的 Host 和 Client observation 与 Cordis tree query API。 | | `ctx.jobs` | `seam` | [`jobs`](../packages/jobs/jobs) | [`jobs-local`](../packages/jobs/jobs-local) | [`tool-bash`](../packages/shell/tool-bash), [`tool-pwsh`](../packages/shell/tool-pwsh), [`tool-terminal`](../packages/experimental/tool-terminal), [`tool-jobs`](../packages/jobs/tool-jobs), [`api-job-controller`](../packages/api/job-controller) | - | 生产方(后台 bash/pwsh 与 PTY 发送)登记正在运行的工作;声明 record 的 job 还为非消费观察者流式提供原始输出;tool-jobs 是面向模型的控制器,用于读取、列出和终止这些工作;jobs-local 是进程本地注册表。 | diff --git a/docs/config-catalog.md b/docs/config-catalog.md index a1423c4bfd..57e8f5bccb 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -111,7 +111,7 @@ export interface Config { - `inject`: `agents` · `sessions` · `llm` · `tools` · `systemPrompt` · `sessionProjections` - `refs`: [`AgentOptions`](subsystems/core.md) · [`SessionId`](subsystems/core.md) · `Volatile` (`@deepseek-ai/cosmokit`) -- `source`: [`packages/core/agent-loop/src/index.ts:253`](../packages/core/agent-loop/src/index.ts) +- `source`: [`packages/core/agent-loop/src/index.ts:252`](../packages/core/agent-loop/src/index.ts) ```ts config-catalog /** Agent-loop plugin configuration. */ @@ -171,6 +171,23 @@ export interface Config { ``` + + + +## `@deepseek-ai/dsh-agent-team` + +- `inject`: `agents` · `sessions` · `sessionPersistence` · `sessionProjections` · `subagents` +- `source`: [`packages/team/agent-team/src/types.ts:132`](../packages/team/agent-team/src/types.ts) + +```ts config-catalog +/** Team-service deployment limits. */ +export interface Config { + /** Maximum non-deleted tasks retained by one Team. */ + readonly maxTasks?: number +} +``` + + @@ -875,29 +892,6 @@ export interface Config { ``` - - - -## `@deepseek-ai/dsh-experimental-agent-team` - -- `inject`: `agents` · `sessions` · `sessionPersistence` · `sessionProjections` · `subagents` -- `source`: [`packages/experimental/agent-team/src/types.ts:153`](../packages/experimental/agent-team/src/types.ts) - -```ts config-catalog -/** Team-service deployment limits. */ -export interface Config { - /** Maximum immutable teammate names retained by one Team. */ - readonly maxMembers?: number - /** Maximum non-deleted tasks retained by one Team. */ - readonly maxTasks?: number - /** Maximum UTF-8 bytes in one complete sender-framed delivery. */ - readonly maxMessageBytes?: number - /** Maximum milliseconds for shared operation settlement and for each Team child drain. */ - readonly disposalTimeoutMs?: number -} -``` - - @@ -1116,10 +1110,24 @@ export interface Config { - `source`: [`packages/experimental/cua-macos/src/index.ts:18`](../packages/experimental/cua-macos/src/index.ts) ```ts config-catalog -/** Native preparation, operation bounds, and profile-local application grants. */ +/** Native preparation, source-build signing, operation bounds, and profile-local application grants. */ export interface Config extends CuaConfig { /** Interval in milliseconds between permission and position checks while the native guide is open. */ permissionPollIntervalMs: number + /** + * Source-build signing: `ad-hoc` (default) binds grants to the code hash; + * an object selects an existing certificate in an explicitly configured keychain. + * Installed packages and `helperPath` keep their own signature. + */ + sourceSigning: SourceSigning +} + +/** Signature selected for source builds; certificates are created and unlocked by the developer. */ +export type SourceSigning = 'ad-hoc' | { + /** Existing codesign certificate name or SHA-1 fingerprint, without a leading dash or whitespace. */ + identity: string + /** Absolute keychain path. */ + keychain: string } ``` @@ -1433,27 +1441,6 @@ export interface Config { ``` - - - -## `@deepseek-ai/dsh-experimental-tool-agent-team` - -- `inject`: `agents` · `agentTeams` · `tools` · `systemPrompt` -- `source`: [`packages/experimental/tool-agent-team/src/index.ts:17`](../packages/experimental/tool-agent-team/src/index.ts) - -```ts config-catalog -/** Tool routing configuration. */ -export interface Config { - /** Register `interrupt_agent` and its policy guidance. Defaults to true; false leaves the domain API available. */ - readonly interrupt?: boolean - /** Continuable-subagent provider used for fresh teammates. */ - readonly freshProvider?: string - /** Continuable-subagent provider used for completed-prefix fork teammates. */ - readonly forkProvider?: string -} -``` - - @@ -3572,7 +3559,7 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist' - `inject`: `workingDirectory` · `sessionProjections` - `refs`: `Volatile` (`@deepseek-ai/cordis`) -- `source`: [`packages/subagent/subagent/src/index.ts:186`](../packages/subagent/subagent/src/index.ts) +- `source`: [`packages/subagent/subagent/src/index.ts:181`](../packages/subagent/subagent/src/index.ts) ```ts config-catalog /** Host configuration for continuable subagent capacity. */ @@ -3788,7 +3775,7 @@ export interface Config { ## `@deepseek-ai/dsh-system-prompt` -- `source`: [`packages/core/system-prompt/src/index.ts:265`](../packages/core/system-prompt/src/index.ts) +- `source`: [`packages/core/system-prompt/src/index.ts:264`](../packages/core/system-prompt/src/index.ts) ```ts config-catalog /** Plugin config: the deployment-authored fragment of the system prompt (see {@link Config.personaPrefix} for its contract). */ @@ -3927,6 +3914,20 @@ export type TokenMeterConfig = Record ``` + + + +## `@deepseek-ai/dsh-tool-agent-team` + +- `inject`: `agentTeams` · `subagents` · `tools` · `systemPrompt` +- `source`: [`packages/team/tool-agent-team/src/index.ts:18`](../packages/team/tool-agent-team/src/index.ts) + +```ts config-catalog +/** The Team tool plugin takes no configuration. */ +export type Config = Record +``` + + @@ -4373,6 +4374,23 @@ export interface PresetSelectionConfig { ``` + + + +## `@deepseek-ai/dsh-tool-subagent-control` + +- `inject`: `tools` · `subagents` · `agents` · `systemPrompt` +- `source`: [`packages/subagent/tool-subagent-control/src/index.ts:26`](../packages/subagent/tool-subagent-control/src/index.ts) + +```ts config-catalog +/** Optional subagent control tools. */ +export interface Config { + /** Register `interrupt_agent`. Defaults to true; false leaves the service API available. */ + interrupt?: boolean +} +``` + + @@ -4849,6 +4867,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co | `@deepseek-ai/dsh-client-resources` | — | [`packages/client/resources/src/index.ts`](../packages/client/resources/src/index.ts) | | `@deepseek-ai/dsh-client-ui-action-cards` | — | [`packages/experimental/client-ui-action-cards/src/index.ts`](../packages/experimental/client-ui-action-cards/src/index.ts) | | `@deepseek-ai/dsh-client-ui-agent-preset` | — | [`packages/client/ui-agent-preset/src/index.ts`](../packages/client/ui-agent-preset/src/index.ts) | +| `@deepseek-ai/dsh-client-ui-agent-team` | — | [`packages/client/ui-agent-team/src/index.ts`](../packages/client/ui-agent-team/src/index.ts) | | `@deepseek-ai/dsh-client-ui-approval` | — | [`packages/client/ui-approval/src/index.ts`](../packages/client/ui-approval/src/index.ts) | | `@deepseek-ai/dsh-client-ui-attachment` | — | [`packages/client/ui-attachment/src/index.ts`](../packages/client/ui-attachment/src/index.ts) | | `@deepseek-ai/dsh-client-ui-brand-official` | — | [`packages/client/ui-brand-official/src/index.ts`](../packages/client/ui-brand-official/src/index.ts) | @@ -4914,7 +4933,6 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co | `@deepseek-ai/dsh-deepseek-llm-api-extensions` | — | [`packages/llm/deepseek-llm-api-extensions/src/index.ts`](../packages/llm/deepseek-llm-api-extensions/src/index.ts) | | `@deepseek-ai/dsh-deliverable-file-references` | `systemPrompt` | [`packages/deliverables/deliverable-file-references/src/index.ts`](../packages/deliverables/deliverable-file-references/src/index.ts) | | `@deepseek-ai/dsh-experimental-auto-review` | `approval` · `llm` · `permissionPresets` · `sessions` · `tools` · `workingDirectory` | [`packages/experimental/auto-review/src/index.ts`](../packages/experimental/auto-review/src/index.ts) | -| `@deepseek-ai/dsh-experimental-client-ui-agent-team` | — | [`packages/experimental/client-ui-agent-team/src/index.ts`](../packages/experimental/client-ui-agent-team/src/index.ts) | | `@deepseek-ai/dsh-experimental-client-ui-claude-code-mods` | — | [`packages/experimental/client-ui-claude-code-mods/src/index.ts`](../packages/experimental/client-ui-claude-code-mods/src/index.ts) | | `@deepseek-ai/dsh-experimental-client-ui-stats` | — | [`packages/experimental/client-ui-stats/src/index.ts`](../packages/experimental/client-ui-stats/src/index.ts) | | `@deepseek-ai/dsh-experimental-client-ui-voice-input` | — | [`packages/experimental/client-ui-voice-input/src/index.ts`](../packages/experimental/client-ui-voice-input/src/index.ts) | @@ -4950,7 +4968,6 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co | `@deepseek-ai/dsh-tool-cordis` | `tools` · `cordisInspect` | [`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts) | | `@deepseek-ai/dsh-tool-executor-native` | `tools` | [`packages/core/tool-executor-native/src/index.ts`](../packages/core/tool-executor-native/src/index.ts) | | `@deepseek-ai/dsh-tool-schedule` | `tools` · `systemPrompt` | [`packages/schedule/tool-schedule/src/index.ts`](../packages/schedule/tool-schedule/src/index.ts) | -| `@deepseek-ai/dsh-tool-subagent-control` | `tools` · `subagents` · `agents` | [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/subagent/tool-subagent-control/src/index.ts) | | `@deepseek-ai/dsh-tool-working-directory` | `tools` · `workingDirectory` | [`packages/session/tool-working-directory/src/index.ts`](../packages/session/tool-working-directory/src/index.ts) | | `@deepseek-ai/dsh-tools` | `systemPrompt` | [`packages/core/tools/src/index.ts`](../packages/core/tools/src/index.ts) | | `@deepseek-ai/dsh-user-questions` | — | [`packages/interaction/user-questions/src/index.ts`](../packages/interaction/user-questions/src/index.ts) | @@ -5006,7 +5023,6 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them. | `@deepseek-ai/dsh-cmdline` | — | [`packages/boot/cmdline/src/index.ts`](../packages/boot/cmdline/src/index.ts) | | `@deepseek-ai/dsh-cua-bundle` | — | [`packages/experimental/cua-bundle/src/index.ts`](../packages/experimental/cua-bundle/src/index.ts) | | `@deepseek-ai/dsh-deque` | — | [`packages/util/deque/src/index.ts`](../packages/util/deque/src/index.ts) | -| `@deepseek-ai/dsh-experimental-agent-team-profile` | — | [`packages/experimental/agent-team-profile/src/index.ts`](../packages/experimental/agent-team-profile/src/index.ts) | | `@deepseek-ai/dsh-experimental-badge-skill-bundle` | — | [`packages/experimental/badge-skill-bundle/src/index.ts`](../packages/experimental/badge-skill-bundle/src/index.ts) | | `@deepseek-ai/dsh-experimental-browser-use-runtime` | — | [`packages/experimental/browser-use-runtime/src/index.ts`](../packages/experimental/browser-use-runtime/src/index.ts) | | `@deepseek-ai/dsh-experimental-cot-translation-bundle` | — | [`packages/experimental/cot-translation-bundle/src/index.ts`](../packages/experimental/cot-translation-bundle/src/index.ts) | diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index adf1f1ef02..bc03ae4b7a 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -113,7 +113,7 @@ export interface Config { - `inject`: `agents` · `sessions` · `llm` · `tools` · `systemPrompt` · `sessionProjections` - `refs`: [`AgentOptions`](subsystems/core.zh.md) · [`SessionId`](subsystems/core.zh.md) · `Volatile` (`@deepseek-ai/cosmokit`) -- `source`: [`packages/core/agent-loop/src/index.ts:253`](../packages/core/agent-loop/src/index.ts) +- `source`: [`packages/core/agent-loop/src/index.ts:252`](../packages/core/agent-loop/src/index.ts) ```ts config-catalog /** Agent-loop plugin configuration. */ @@ -173,6 +173,23 @@ export interface Config { ``` + + + +## `@deepseek-ai/dsh-agent-team` + +- `inject`: `agents` · `sessions` · `sessionPersistence` · `sessionProjections` · `subagents` +- `source`: [`packages/team/agent-team/src/types.ts:132`](../packages/team/agent-team/src/types.ts) + +```ts config-catalog +/** Team-service deployment limits. */ +export interface Config { + /** Maximum non-deleted tasks retained by one Team. */ + readonly maxTasks?: number +} +``` + + @@ -877,29 +894,6 @@ export interface Config { ``` - - - -## `@deepseek-ai/dsh-experimental-agent-team` - -- `inject`: `agents` · `sessions` · `sessionPersistence` · `sessionProjections` · `subagents` -- `source`: [`packages/experimental/agent-team/src/types.ts:153`](../packages/experimental/agent-team/src/types.ts) - -```ts config-catalog -/** Team-service deployment limits. */ -export interface Config { - /** Maximum immutable teammate names retained by one Team. */ - readonly maxMembers?: number - /** Maximum non-deleted tasks retained by one Team. */ - readonly maxTasks?: number - /** Maximum UTF-8 bytes in one complete sender-framed delivery. */ - readonly maxMessageBytes?: number - /** Maximum milliseconds for shared operation settlement and for each Team child drain. */ - readonly disposalTimeoutMs?: number -} -``` - - @@ -1118,10 +1112,24 @@ export interface Config { - `source`: [`packages/experimental/cua-macos/src/index.ts:18`](../packages/experimental/cua-macos/src/index.ts) ```ts config-catalog -/** Native preparation, operation bounds, and profile-local application grants. */ +/** Native preparation, source-build signing, operation bounds, and profile-local application grants. */ export interface Config extends CuaConfig { /** Interval in milliseconds between permission and position checks while the native guide is open. */ permissionPollIntervalMs: number + /** + * Source-build signing: `ad-hoc` (default) binds grants to the code hash; + * an object selects an existing certificate in an explicitly configured keychain. + * Installed packages and `helperPath` keep their own signature. + */ + sourceSigning: SourceSigning +} + +/** Signature selected for source builds; certificates are created and unlocked by the developer. */ +export type SourceSigning = 'ad-hoc' | { + /** Existing codesign certificate name or SHA-1 fingerprint, without a leading dash or whitespace. */ + identity: string + /** Absolute keychain path. */ + keychain: string } ``` @@ -1435,27 +1443,6 @@ export interface Config { ``` - - - -## `@deepseek-ai/dsh-experimental-tool-agent-team` - -- `inject`: `agents` · `agentTeams` · `tools` · `systemPrompt` -- `source`: [`packages/experimental/tool-agent-team/src/index.ts:17`](../packages/experimental/tool-agent-team/src/index.ts) - -```ts config-catalog -/** Tool routing configuration. */ -export interface Config { - /** Register `interrupt_agent` and its policy guidance. Defaults to true; false leaves the domain API available. */ - readonly interrupt?: boolean - /** Continuable-subagent provider used for fresh teammates. */ - readonly freshProvider?: string - /** Continuable-subagent provider used for completed-prefix fork teammates. */ - readonly forkProvider?: string -} -``` - - @@ -3574,7 +3561,7 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist' - `inject`: `workingDirectory` · `sessionProjections` - `refs`: `Volatile` (`@deepseek-ai/cordis`) -- `source`: [`packages/subagent/subagent/src/index.ts:186`](../packages/subagent/subagent/src/index.ts) +- `source`: [`packages/subagent/subagent/src/index.ts:181`](../packages/subagent/subagent/src/index.ts) ```ts config-catalog /** Host configuration for continuable subagent capacity. */ @@ -3790,7 +3777,7 @@ export interface Config { ## `@deepseek-ai/dsh-system-prompt` -- `source`: [`packages/core/system-prompt/src/index.ts:265`](../packages/core/system-prompt/src/index.ts) +- `source`: [`packages/core/system-prompt/src/index.ts:264`](../packages/core/system-prompt/src/index.ts) ```ts config-catalog /** Plugin config: the deployment-authored fragment of the system prompt (see {@link Config.personaPrefix} for its contract). */ @@ -3929,6 +3916,20 @@ export type TokenMeterConfig = Record ``` + + + +## `@deepseek-ai/dsh-tool-agent-team` + +- `inject`: `agentTeams` · `subagents` · `tools` · `systemPrompt` +- `source`: [`packages/team/tool-agent-team/src/index.ts:18`](../packages/team/tool-agent-team/src/index.ts) + +```ts config-catalog +/** The Team tool plugin takes no configuration. */ +export type Config = Record +``` + + @@ -4375,6 +4376,23 @@ export interface PresetSelectionConfig { ``` + + + +## `@deepseek-ai/dsh-tool-subagent-control` + +- `inject`: `tools` · `subagents` · `agents` · `systemPrompt` +- `source`: [`packages/subagent/tool-subagent-control/src/index.ts:26`](../packages/subagent/tool-subagent-control/src/index.ts) + +```ts config-catalog +/** Optional subagent control tools. */ +export interface Config { + /** Register `interrupt_agent`. Defaults to true; false leaves the service API available. */ + interrupt?: boolean +} +``` + + @@ -4851,6 +4869,7 @@ export interface Config { | `@deepseek-ai/dsh-client-resources` | — | [`packages/client/resources/src/index.ts`](../packages/client/resources/src/index.ts) | | `@deepseek-ai/dsh-client-ui-action-cards` | — | [`packages/experimental/client-ui-action-cards/src/index.ts`](../packages/experimental/client-ui-action-cards/src/index.ts) | | `@deepseek-ai/dsh-client-ui-agent-preset` | — | [`packages/client/ui-agent-preset/src/index.ts`](../packages/client/ui-agent-preset/src/index.ts) | +| `@deepseek-ai/dsh-client-ui-agent-team` | — | [`packages/client/ui-agent-team/src/index.ts`](../packages/client/ui-agent-team/src/index.ts) | | `@deepseek-ai/dsh-client-ui-approval` | — | [`packages/client/ui-approval/src/index.ts`](../packages/client/ui-approval/src/index.ts) | | `@deepseek-ai/dsh-client-ui-attachment` | — | [`packages/client/ui-attachment/src/index.ts`](../packages/client/ui-attachment/src/index.ts) | | `@deepseek-ai/dsh-client-ui-brand-official` | — | [`packages/client/ui-brand-official/src/index.ts`](../packages/client/ui-brand-official/src/index.ts) | @@ -4916,7 +4935,6 @@ export interface Config { | `@deepseek-ai/dsh-deepseek-llm-api-extensions` | — | [`packages/llm/deepseek-llm-api-extensions/src/index.ts`](../packages/llm/deepseek-llm-api-extensions/src/index.ts) | | `@deepseek-ai/dsh-deliverable-file-references` | `systemPrompt` | [`packages/deliverables/deliverable-file-references/src/index.ts`](../packages/deliverables/deliverable-file-references/src/index.ts) | | `@deepseek-ai/dsh-experimental-auto-review` | `approval` · `llm` · `permissionPresets` · `sessions` · `tools` · `workingDirectory` | [`packages/experimental/auto-review/src/index.ts`](../packages/experimental/auto-review/src/index.ts) | -| `@deepseek-ai/dsh-experimental-client-ui-agent-team` | — | [`packages/experimental/client-ui-agent-team/src/index.ts`](../packages/experimental/client-ui-agent-team/src/index.ts) | | `@deepseek-ai/dsh-experimental-client-ui-claude-code-mods` | — | [`packages/experimental/client-ui-claude-code-mods/src/index.ts`](../packages/experimental/client-ui-claude-code-mods/src/index.ts) | | `@deepseek-ai/dsh-experimental-client-ui-stats` | — | [`packages/experimental/client-ui-stats/src/index.ts`](../packages/experimental/client-ui-stats/src/index.ts) | | `@deepseek-ai/dsh-experimental-client-ui-voice-input` | — | [`packages/experimental/client-ui-voice-input/src/index.ts`](../packages/experimental/client-ui-voice-input/src/index.ts) | @@ -4952,7 +4970,6 @@ export interface Config { | `@deepseek-ai/dsh-tool-cordis` | `tools` · `cordisInspect` | [`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts) | | `@deepseek-ai/dsh-tool-executor-native` | `tools` | [`packages/core/tool-executor-native/src/index.ts`](../packages/core/tool-executor-native/src/index.ts) | | `@deepseek-ai/dsh-tool-schedule` | `tools` · `systemPrompt` | [`packages/schedule/tool-schedule/src/index.ts`](../packages/schedule/tool-schedule/src/index.ts) | -| `@deepseek-ai/dsh-tool-subagent-control` | `tools` · `subagents` · `agents` | [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/subagent/tool-subagent-control/src/index.ts) | | `@deepseek-ai/dsh-tool-working-directory` | `tools` · `workingDirectory` | [`packages/session/tool-working-directory/src/index.ts`](../packages/session/tool-working-directory/src/index.ts) | | `@deepseek-ai/dsh-tools` | `systemPrompt` | [`packages/core/tools/src/index.ts`](../packages/core/tools/src/index.ts) | | `@deepseek-ai/dsh-user-questions` | — | [`packages/interaction/user-questions/src/index.ts`](../packages/interaction/user-questions/src/index.ts) | @@ -5008,7 +5025,6 @@ export interface Config { | `@deepseek-ai/dsh-cmdline` | — | [`packages/boot/cmdline/src/index.ts`](../packages/boot/cmdline/src/index.ts) | | `@deepseek-ai/dsh-cua-bundle` | — | [`packages/experimental/cua-bundle/src/index.ts`](../packages/experimental/cua-bundle/src/index.ts) | | `@deepseek-ai/dsh-deque` | — | [`packages/util/deque/src/index.ts`](../packages/util/deque/src/index.ts) | -| `@deepseek-ai/dsh-experimental-agent-team-profile` | — | [`packages/experimental/agent-team-profile/src/index.ts`](../packages/experimental/agent-team-profile/src/index.ts) | | `@deepseek-ai/dsh-experimental-badge-skill-bundle` | — | [`packages/experimental/badge-skill-bundle/src/index.ts`](../packages/experimental/badge-skill-bundle/src/index.ts) | | `@deepseek-ai/dsh-experimental-browser-use-runtime` | — | [`packages/experimental/browser-use-runtime/src/index.ts`](../packages/experimental/browser-use-runtime/src/index.ts) | | `@deepseek-ai/dsh-experimental-cot-translation-bundle` | — | [`packages/experimental/cot-translation-bundle/src/index.ts`](../packages/experimental/cot-translation-bundle/src/index.ts) | diff --git a/docs/development.i18n.yaml b/docs/development.i18n.yaml index 682746fcee..591bcdd5ce 100644 --- a/docs/development.i18n.yaml +++ b/docs/development.i18n.yaml @@ -21,8 +21,8 @@ en: 3221d1076554001f zh: 29f166be6a5c04d9 /development-guide/contributor-reference/typescript-project-layout: - en: eb79b6f8ea745a9f - zh: de676c78231c823a + en: 16d67911ecaa50d3 + zh: f5259b0fbf022db3 /development-guide/contributor-reference/environment-variables: en: dba2c72ee6ffa930 zh: b65c27fbb9129ec8 @@ -45,5 +45,5 @@ en: 85b38f78d87fc88f zh: 00d5eb625e7dfb61 /development-guide/contributor-reference/documenting-types-verbatim-ts-type-equiv: - en: 9d578b6797be921b - zh: 823cbfcfd9a75c17 + en: 6b9b948e0b80cf76 + zh: 2642f4127ed9b719 diff --git a/docs/development.md b/docs/development.md index 18130990d5..9aec6e601d 100644 --- a/docs/development.md +++ b/docs/development.md @@ -89,7 +89,7 @@ tsdown --env.DSH_BUILD_FACE client pnpm run build:web ``` -`pnpm run build:check` runs only `tsc -p tsconfig.host.json` and `tsc -p tsconfig.client.json` concurrently. Project mode checks each aggregate's repository-wide test and script program against the package declarations already on disk and never compiles a reference, so `build:check` neither detects nor rebuilds missing or stale `build:artifacts` output; missing output surfaces as compiler errors. `pnpm run build` runs both step sets as one [scheduled](../scripts/run-gates.ts) dependency graph: each aggregate check starts once its own face's package compilation finishes and overlaps the remaining artifact steps. In all three commands `DSH_GATE_CONCURRENCY` bounds concurrent steps (`1` runs them in dependency order) and `DSH_GATE_FAIL_FAST=1` stops the remaining steps at the first failure; otherwise a failure skips only its dependents, so independent errors are all reported. A failed step prints its own output, and any failure exits non-zero without writing the client build record. +`pnpm run build:check` runs only `tsc -p tsconfig.host.json` and `tsc -p tsconfig.client.json` concurrently. Project mode checks each aggregate's repository-wide test and script program against the package declarations already on disk and never compiles a reference, so `build:check` neither detects nor rebuilds missing or stale `build:artifacts` output; missing output surfaces as compiler errors. `pnpm run build` runs both step sets as one [scheduled](../scripts/run-gates.ts) dependency graph: each aggregate check starts once its own face's package compilation finishes and overlaps the remaining artifact steps. In all three commands `DSH_GATE_CONCURRENCY` bounds concurrent steps (`1` runs them in dependency order) and `DSH_GATE_FAIL_FAST=1` stops the remaining steps at the first failure; otherwise a failure skips only its dependents, so independent errors are all reported. A failed step prints its own output, and any failure exits non-zero without writing the client build record. Host TypeScript compiler processes receive an 8 GiB V8 old-space limit for the package graph and cold aggregate checks. Choose the command by what the change needs: @@ -218,4 +218,4 @@ The [subsystems](subsystems/README.md) pages paste source-equivalent declaration { "doc": "docs/subsystems/session.md", "symbol": "SessionEvent", "source": "packages/core/session/src/types.ts" } ``` -`pnpm run verify-type-equiv` (part of `doc-sync`) then extracts that symbol's declaration and attached JSDoc from source via the TypeScript parser and asserts the block matches both. For a class whose implementation bodies do not belong in the catalog, use ` ```ts public-api ` and set `"projection": "public-api"`; the checked projection retains the public fields, constructor, accessors, methods, and original class/member JSDoc while omitting bodies and private or protected members. Comparison ignores whitespace and non-JSDoc comments but requires every original JSDoc comment, including member documentation, so readers see the source contract beside the exact type definition. The gate enforces a 1:1 correspondence by document, symbol, and projection between primary blocks and manifest entries; a paired `.zh.md` block reuses its unsuffixed sibling's entry only when the whole tracked fence sequence is byte-identical and ordered identically. `doc-typecheck` applies the same derivative rule to compilable fences, while skipping both source-equivalence fence kinds from compilation and its opt-out ratio. When you change a documented declaration or its JSDoc, the gate fails until you update the paste; when you add or remove a primary block, update the manifest in the same change. +`pnpm run verify-type-equiv` (part of `doc-sync`) then extracts that symbol's declaration and attached JSDoc from source via the TypeScript parser and asserts the block matches both. For a class whose implementation bodies do not belong in the catalog, use ` ```ts public-api ` and set `"projection": "public-api"`; the checked projection retains the public fields, constructor, accessors, methods, and original class/member JSDoc while omitting bodies and private or protected members. Comparison ignores whitespace and non-JSDoc comments but requires every original JSDoc comment, including member documentation, so readers see the source contract beside the exact type definition. The gate enforces a 1:1 correspondence by document, symbol, and projection between primary blocks and manifest entries; a paired `.zh.md` block reuses its unsuffixed sibling's entry only when the whole tracked fence sequence is byte-identical and ordered identically. `doc-typecheck` applies the same derivative rule to compilable fences, while skipping both source-equivalence fence kinds from compilation and its opt-out ratio. Upgrade guides in version directories older than the root package version are frozen; their examples are excluded from current-API compilation. When you change a documented declaration or its JSDoc, the gate fails until you update the paste; when you add or remove a primary block, update the manifest in the same change. diff --git a/docs/development.zh.md b/docs/development.zh.md index 47428b923b..92f0b71b99 100644 --- a/docs/development.zh.md +++ b/docs/development.zh.md @@ -93,7 +93,7 @@ tsdown --env.DSH_BUILD_FACE client pnpm run build:web ``` -`pnpm run build:check` 只并发运行 `tsc -p tsconfig.host.json` 与 `tsc -p tsconfig.client.json`。project mode 依据磁盘上已有的包声明检查各 aggregate 的仓库级测试和脚本 program,从不编译 reference,因此 `build:check` 既不检测也不重建缺失或过期的 `build:artifacts` 产物;产物缺失时表现为编译器错误。`pnpm run build` 把两组步骤作为一张[调度](../scripts/run-gates.ts)依赖图运行:每个 aggregate 检查在本 face 的包编译完成后立即开始,与其余产物步骤重叠执行。三个命令中,`DSH_GATE_CONCURRENCY` 限制并发步骤数(`1` 按依赖顺序执行),`DSH_GATE_FAIL_FAST=1` 在第一个失败时停止其余步骤;否则失败只跳过其依赖方,因此会报告所有相互独立的错误。失败步骤打印自身输出,任一失败都以非零状态退出且不写 client build record。 +`pnpm run build:check` 只并发运行 `tsc -p tsconfig.host.json` 与 `tsc -p tsconfig.client.json`。project mode 依据磁盘上已有的包声明检查各 aggregate 的仓库级测试和脚本 program,从不编译 reference,因此 `build:check` 既不检测也不重建缺失或过期的 `build:artifacts` 产物;产物缺失时表现为编译器错误。`pnpm run build` 把两组步骤作为一张[调度](../scripts/run-gates.ts)依赖图运行:每个 aggregate 检查在本 face 的包编译完成后立即开始,与其余产物步骤重叠执行。三个命令中,`DSH_GATE_CONCURRENCY` 限制并发步骤数(`1` 按依赖顺序执行),`DSH_GATE_FAIL_FAST=1` 在第一个失败时停止其余步骤;否则失败只跳过其依赖方,因此会报告所有相互独立的错误。失败步骤打印自身输出,任一失败都以非零状态退出且不写 client build record。 Host TypeScript 编译进程的 V8 old-space 上限为 8 GiB,用于包依赖图和无缓存的 aggregate 检查。 按改动需要选择命令: @@ -225,4 +225,4 @@ Web 命令后面的参数会传给 `dsh web`,例如 `pnpm run dev:web --no-ope { "doc": "docs/subsystems/session.md", "symbol": "SessionEvent", "source": "packages/core/session/src/types.ts" } ``` -`pnpm run verify-type-equiv`(`doc-sync` 的一环)随后通过 TypeScript 解析器从源码提取该符号的声明及其附带的 JSDoc,并断言代码块同时匹配两者。对于不应把实现体写进目录的类,请使用 ` ```ts public-api ` 并设置 `"projection": "public-api"`;门禁检查的投影会保留公共字段、构造函数、访问器、方法以及类和成员的原始 JSDoc,同时省略实现体和私有或受保护成员。比对会忽略空白和非 JSDoc 注释,但要求保留每条原始 JSDoc(包括成员文档),让读者同时看到源码约定和确切类型定义。该门禁按文档、符号和投影,在主块与 manifest 条目之间强制 1:1 对应;只有当配对 `.zh.md` 块的完整受跟踪围栏序列与其无后缀兄弟文件按字节一致且顺序相同时,才会复用后者的条目。`doc-typecheck` 对可编译围栏应用同一派生规则,同时跳过两种源码等价围栏的编译,并将其排除在 opt-out 比例的计算之外。当你改动一个已记录的类型声明或其 JSDoc 时,门禁会失败直到你更新粘贴内容;当你增删一个主块时,请在同一个变更里更新 manifest。 +`pnpm run verify-type-equiv`(`doc-sync` 的一环)随后通过 TypeScript 解析器从源码提取该符号的声明及其附带的 JSDoc,并断言代码块同时匹配两者。对于不应把实现体写进目录的类,请使用 ` ```ts public-api ` 并设置 `"projection": "public-api"`;门禁检查的投影会保留公共字段、构造函数、访问器、方法以及类和成员的原始 JSDoc,同时省略实现体和私有或受保护成员。比对会忽略空白和非 JSDoc 注释,但要求保留每条原始 JSDoc(包括成员文档),让读者同时看到源码约定和确切类型定义。该门禁按文档、符号和投影,在主块与 manifest 条目之间强制 1:1 对应;只有当配对 `.zh.md` 块的完整受跟踪围栏序列与其无后缀兄弟文件按字节一致且顺序相同时,才会复用后者的条目。`doc-typecheck` 对可编译围栏应用同一派生规则,同时跳过两种源码等价围栏的编译,并将其排除在 opt-out 比例的计算之外。版本目录早于根包版本的升级指南已冻结,其中的示例不参与当前 API 的编译检查。当你改动一个已记录的类型声明或其 JSDoc 时,门禁会失败直到你更新粘贴内容;当你增删一个主块时,请在同一个变更里更新 manifest。 diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 976971787d..cc7a806a71 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -10,18 +10,18 @@ Application listeners are outside this package matrix. Desktop Host subscribes t | Event | Mode | Declared in | Dispatchers | Listeners | | --- | --- | --- | --- | --- | -| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:199`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | +| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:198`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | | `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:66`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | [`cua`](../packages/experimental/cua), `remotes` | | `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:399`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` | -| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), `liveshare`, [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | -| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team` | +| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `browser-use-runtime`, `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), `liveshare`, [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller`, [`subagent`](../packages/subagent/subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | | `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:429`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) | | `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:328`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) | | `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:336`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) | -| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:317`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | `client-ui-forge`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) | -| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:350`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`action-cards`](../packages/experimental/action-cards), [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, `client-ui-forge`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), `liveshare`, [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) | +| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:317`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | `client-ui-forge`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | +| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:350`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`action-cards`](../packages/experimental/action-cards), [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, `client-ui-forge`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), `liveshare`, [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`subagent`](../packages/subagent/subagent), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) | | `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:389`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) | -| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:309`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` | +| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:309`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` | | `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:417`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:700`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:680`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | @@ -71,15 +71,15 @@ Application listeners are outside this package matrix. Desktop Host subscribes t | `schedule/resolve-agent` | `waterfall` | [`packages/schedule/schedule/src/index.ts:71`](../packages/schedule/schedule/src/index.ts) | [`schedule`](../packages/schedule/schedule) (`waterfall`) | `server` | | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - | | `session/created` | `emit` | [`packages/core/session/src/index.ts:56`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `server`, `session-controller`, [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry) | -| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:65`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `forge-session`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), `ui-trajectory`, [`workspace-changes`](../packages/deliverables/workspace-changes) | -| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, `claude-code-mods`, `client-ui-forge`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, `forge-session`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter), [`tool-executor-ptc`](../packages/core/tool-executor-ptc), [`tool-jobs`](../packages/jobs/tool-jobs), `ui-trajectory`, [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:65`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `file-upload`, `forge-session`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), `ui-trajectory`, [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `claude-code-mods`, `client-ui-forge`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, `forge-session`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter), [`tool-executor-ptc`](../packages/core/tool-executor-ptc), [`tool-jobs`](../packages/jobs/tool-jobs), `ui-trajectory`, [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) | | `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:75`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`emit`) | `remotes` | | `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:296`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - | -| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:166`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), `server` | -| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:142`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:148`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:158`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | +| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:161`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), `server` | +| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:137`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:143`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:153`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | | `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | `browser-use-runtime`, [`working-directory`](../packages/session/working-directory) | | `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - | | `tools/change` | `emit` | [`packages/core/tools/src/events.ts:56`](../packages/core/tools/src/events.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime` | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index 8f2a0d34a9..6c437ea8b6 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -12,18 +12,18 @@ | Event | Mode | Declared in | Dispatchers | Listeners | | --- | --- | --- | --- | --- | -| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:199`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | +| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:198`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | | `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:66`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | [`cua`](../packages/experimental/cua), `remotes` | | `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:399`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` | -| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), `liveshare`, [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | -| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team` | +| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `browser-use-runtime`, `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), `liveshare`, [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller`, [`subagent`](../packages/subagent/subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | | `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:429`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) | | `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:328`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) | | `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:336`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) | -| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:317`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | `client-ui-forge`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) | -| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:350`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`action-cards`](../packages/experimental/action-cards), [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, `client-ui-forge`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), `liveshare`, [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) | +| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:317`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | `client-ui-forge`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | +| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:350`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`action-cards`](../packages/experimental/action-cards), [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, `client-ui-forge`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), `liveshare`, [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`subagent`](../packages/subagent/subagent), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) | | `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:389`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) | -| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:309`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` | +| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:309`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` | | `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:417`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:700`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:680`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | @@ -73,15 +73,15 @@ | `schedule/resolve-agent` | `waterfall` | [`packages/schedule/schedule/src/index.ts:71`](../packages/schedule/schedule/src/index.ts) | [`schedule`](../packages/schedule/schedule) (`waterfall`) | `server` | | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - | | `session/created` | `emit` | [`packages/core/session/src/index.ts:56`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `server`, `session-controller`, [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry) | -| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:65`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `forge-session`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), `ui-trajectory`, [`workspace-changes`](../packages/deliverables/workspace-changes) | -| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, `claude-code-mods`, `client-ui-forge`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, `forge-session`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter), [`tool-executor-ptc`](../packages/core/tool-executor-ptc), [`tool-jobs`](../packages/jobs/tool-jobs), `ui-trajectory`, [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:65`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `file-upload`, `forge-session`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), `ui-trajectory`, [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `claude-code-mods`, `client-ui-forge`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, `forge-session`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter), [`tool-executor-ptc`](../packages/core/tool-executor-ptc), [`tool-jobs`](../packages/jobs/tool-jobs), `ui-trajectory`, [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) | | `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:75`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`emit`) | `remotes` | | `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:296`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - | -| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:166`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), `server` | -| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:142`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:148`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:158`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | +| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:161`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), `server` | +| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:137`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:143`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:153`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | | `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | `browser-use-runtime`, [`working-directory`](../packages/session/working-directory) | | `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - | | `tools/change` | `emit` | [`packages/core/tools/src/events.ts:56`](../packages/core/tools/src/events.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime` | diff --git a/docs/module-graph.md b/docs/module-graph.md index e73a0bb594..3236cc783c 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -161,6 +161,7 @@ flowchart TD pkg_client_shortcuts["client-shortcuts"] pkg_client_store["client-store"] pkg_client_ui_agent_preset["client-ui-agent-preset"] + pkg_client_ui_agent_team["client-ui-agent-team"] pkg_client_ui_approval["client-ui-approval"] pkg_client_ui_attachment["client-ui-attachment"] pkg_client_ui_brand_official["client-ui-brand-official"] @@ -263,8 +264,6 @@ flowchart TD pkg_creation_recommendations["creation-recommendations"] pkg_cua["cua"] pkg_cua_bundle["cua-bundle"] - pkg_experimental_agent_team["experimental-agent-team"] - pkg_experimental_agent_team_profile["experimental-agent-team-profile"] pkg_experimental_api_speech_to_text["experimental-api-speech-to-text"] pkg_experimental_auto_review["experimental-auto-review"] pkg_experimental_badge_skill_bundle["experimental-badge-skill-bundle"] @@ -273,7 +272,6 @@ flowchart TD pkg_experimental_browser_use_runtime["experimental-browser-use-runtime"] pkg_experimental_browser_use_stagehand_native["experimental-browser-use-stagehand-native"] pkg_experimental_claude_code_mods["experimental-claude-code-mods"] - pkg_experimental_client_ui_agent_team["experimental-client-ui-agent-team"] pkg_experimental_client_ui_claude_code_mods["experimental-client-ui-claude-code-mods"] pkg_experimental_client_ui_cot_translation["experimental-client-ui-cot-translation"] pkg_experimental_client_ui_forge["experimental-client-ui-forge"] @@ -301,7 +299,6 @@ flowchart TD pkg_experimental_speech_to_text_sensevoice["experimental-speech-to-text-sensevoice"] pkg_experimental_subagent_oar["experimental-subagent-oar"] pkg_experimental_terminal_bundle["experimental-terminal-bundle"] - pkg_experimental_tool_agent_team["experimental-tool-agent-team"] pkg_experimental_tool_subagent_oar["experimental-tool-subagent-oar"] pkg_experimental_tool_worktree["experimental-tool-worktree"] pkg_experimental_translator["experimental-translator"] @@ -452,6 +449,10 @@ flowchart TD pkg_subprocess_local["subprocess-local"] pkg_win32_process["win32-process"] end + subgraph group_team["packages/team"] + pkg_agent_team["agent-team"] + pkg_tool_agent_team["tool-agent-team"] + end subgraph group_telemetry["packages/telemetry"] pkg_otel["otel"] end @@ -1329,6 +1330,7 @@ flowchart TD pkg_tool_subagent_control --> pkg_llm pkg_tool_subagent_control --> pkg_session pkg_tool_subagent_control --> pkg_subagent + pkg_tool_subagent_control --> pkg_system_prompt pkg_tool_subagent_control --> pkg_tools pkg_api_session_controller --> pkg_agent pkg_api_session_controller --> pkg_agent_default_model @@ -1395,12 +1397,6 @@ flowchart TD pkg_session_reference --> pkg_subagent pkg_session_reference --> pkg_system_prompt pkg_session_reference --> pkg_typert_protocol - pkg_experimental_agent_team --> pkg_agent - pkg_experimental_agent_team --> pkg_llm - pkg_experimental_agent_team --> pkg_session - pkg_experimental_agent_team --> pkg_session_persistence - pkg_experimental_agent_team --> pkg_session_projection - pkg_experimental_agent_team --> pkg_subagent pkg_experimental_subagent_oar --> pkg_llm pkg_experimental_subagent_oar --> pkg_session pkg_experimental_subagent_oar --> pkg_subagent @@ -1425,6 +1421,13 @@ flowchart TD pkg_sdk_protocol --> pkg_session pkg_sdk_protocol --> pkg_subagent pkg_sdk_protocol --> pkg_user_questions + pkg_agent_team --> pkg_agent + pkg_agent_team --> pkg_llm + pkg_agent_team --> pkg_session + pkg_agent_team --> pkg_session_persistence + pkg_agent_team --> pkg_session_projection + pkg_agent_team --> pkg_subagent + pkg_agent_team --> pkg_typert_protocol pkg_workflow_ptc --> pkg_agent pkg_workflow_ptc --> pkg_llm pkg_workflow_ptc --> pkg_ptc_runtime @@ -1435,11 +1438,6 @@ flowchart TD pkg_workflow_ptc --> pkg_tools pkg_workflow_ptc --> pkg_workflow pkg_workflow_ptc --> pkg_working_directory - pkg_experimental_tool_agent_team --> pkg_agent - pkg_experimental_tool_agent_team --> pkg_experimental_agent_team - pkg_experimental_tool_agent_team --> pkg_session - pkg_experimental_tool_agent_team --> pkg_system_prompt - pkg_experimental_tool_agent_team --> pkg_tools pkg_schedule --> pkg_agent pkg_schedule --> pkg_api_session_controller pkg_schedule --> pkg_brand @@ -1456,6 +1454,12 @@ flowchart TD pkg_sdk_client --> pkg_llm pkg_sdk_client --> pkg_sdk_protocol pkg_sdk_client --> pkg_session + pkg_tool_agent_team --> pkg_agent + pkg_tool_agent_team --> pkg_agent_team + pkg_tool_agent_team --> pkg_session + pkg_tool_agent_team --> pkg_subagent + pkg_tool_agent_team --> pkg_system_prompt + pkg_tool_agent_team --> pkg_tools pkg_client_test_runtime --> pkg_api_gateway pkg_client_test_runtime --> pkg_api_session_controller pkg_client_test_runtime --> pkg_api_workspace_controller @@ -1549,6 +1553,7 @@ flowchart TD | [`client-shortcuts`](../packages/client/shortcuts) | `client` | — | | [`client-store`](../packages/client/store) | `client` | — | | [`client-ui-agent-preset`](../packages/client/ui-agent-preset) | `client` | — | +| [`client-ui-agent-team`](../packages/client/ui-agent-team) | `client` | — | | [`client-ui-approval`](../packages/client/ui-approval) | `client` | — | | [`client-ui-attachment`](../packages/client/ui-attachment) | `client` | — | | [`client-ui-brand-official`](../packages/client/ui-brand-official) | `client` | — | @@ -1614,9 +1619,7 @@ flowchart TD | [`client-ui-creation-recommendations`](../packages/experimental/client-ui-creation-recommendations) | `experimental` | — | | [`client-ui-plugin-recommendations`](../packages/experimental/client-ui-plugin-recommendations) | `experimental` | — | | [`cua-bundle`](../packages/experimental/cua-bundle) | `experimental` | — | -| [`experimental-agent-team-profile`](../packages/experimental/agent-team-profile) | `experimental` | — | | [`experimental-badge-skill-bundle`](../packages/experimental/badge-skill-bundle) | `experimental` | — | -| [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | — | | [`experimental-client-ui-claude-code-mods`](../packages/experimental/client-ui-claude-code-mods) | `experimental` | — | | [`experimental-client-ui-stats`](../packages/experimental/client-ui-stats) | `experimental` | — | | [`experimental-client-ui-voice-input`](../packages/experimental/client-ui-voice-input) | `experimental` | — | @@ -1858,22 +1861,22 @@ flowchart TD | [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process) | `subagent` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`subagent`](../packages/subagent/subagent) | | [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | +| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-file-upload`](../packages/client/file-upload), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`settings`](../packages/settings/settings), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-time`](../packages/util/time), [`util-values`](../packages/util/values), [`util-workspace-path`](../packages/util/workspace-path), [`working-directory`](../packages/session/working-directory), [`workspace`](../packages/workspace/workspace) | | [`plugin-market`](../packages/boot/plugin-market) | `boot` | [`plugin-manager`](../packages/boot/plugin-manager), [`settings`](../packages/settings/settings), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) | | [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`subagent`](../packages/subagent/subagent), [`user-approval`](../packages/interaction/user-approval) | | [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | | [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`spill`](../packages/spill/spill), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) | -| [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent) | | [`experimental-subagent-oar`](../packages/experimental/subagent-oar) | `experimental` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) | | [`experimental-tool-subagent-oar`](../packages/experimental/tool-subagent-oar) | `experimental` | [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | `experimental` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/experimental/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`tool-ralph`](../packages/experimental/tool-ralph) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`commands`](../packages/interaction/commands), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) | +| [`agent-team`](../packages/team/agent-team) | `team` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) | | [`workflow-ptc`](../packages/workflow/workflow-ptc) | `workflow` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow), [`working-directory`](../packages/session/working-directory) | -| [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | `experimental` | [`agent`](../packages/core/agent), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`util-time`](../packages/util/time), [`workspace`](../packages/workspace/workspace) | | [`sdk-client`](../packages/sdk/client) | `sdk` | [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) | +| [`tool-agent-team`](../packages/team/tool-agent-team) | `team` | [`agent`](../packages/core/agent), [`agent-team`](../packages/team/agent-team), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-hmr`](../packages/client/hmr), [`client-modules`](../packages/client/modules), [`client-store`](../packages/client/store), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-context-menu`](../packages/client/ui-context-menu), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web`](../packages/client/web), [`remote-mock`](../packages/test-support/remote-mock), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) | | [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) | | [`tool-schedule`](../packages/schedule/tool-schedule) | `schedule` | [`llm`](../packages/llm/llm), [`schedule`](../packages/schedule/schedule), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index 818130b1fe..8ce9f2337b 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -163,6 +163,7 @@ flowchart TD pkg_client_shortcuts["client-shortcuts"] pkg_client_store["client-store"] pkg_client_ui_agent_preset["client-ui-agent-preset"] + pkg_client_ui_agent_team["client-ui-agent-team"] pkg_client_ui_approval["client-ui-approval"] pkg_client_ui_attachment["client-ui-attachment"] pkg_client_ui_brand_official["client-ui-brand-official"] @@ -265,8 +266,6 @@ flowchart TD pkg_creation_recommendations["creation-recommendations"] pkg_cua["cua"] pkg_cua_bundle["cua-bundle"] - pkg_experimental_agent_team["experimental-agent-team"] - pkg_experimental_agent_team_profile["experimental-agent-team-profile"] pkg_experimental_api_speech_to_text["experimental-api-speech-to-text"] pkg_experimental_auto_review["experimental-auto-review"] pkg_experimental_badge_skill_bundle["experimental-badge-skill-bundle"] @@ -275,7 +274,6 @@ flowchart TD pkg_experimental_browser_use_runtime["experimental-browser-use-runtime"] pkg_experimental_browser_use_stagehand_native["experimental-browser-use-stagehand-native"] pkg_experimental_claude_code_mods["experimental-claude-code-mods"] - pkg_experimental_client_ui_agent_team["experimental-client-ui-agent-team"] pkg_experimental_client_ui_claude_code_mods["experimental-client-ui-claude-code-mods"] pkg_experimental_client_ui_cot_translation["experimental-client-ui-cot-translation"] pkg_experimental_client_ui_forge["experimental-client-ui-forge"] @@ -303,7 +301,6 @@ flowchart TD pkg_experimental_speech_to_text_sensevoice["experimental-speech-to-text-sensevoice"] pkg_experimental_subagent_oar["experimental-subagent-oar"] pkg_experimental_terminal_bundle["experimental-terminal-bundle"] - pkg_experimental_tool_agent_team["experimental-tool-agent-team"] pkg_experimental_tool_subagent_oar["experimental-tool-subagent-oar"] pkg_experimental_tool_worktree["experimental-tool-worktree"] pkg_experimental_translator["experimental-translator"] @@ -454,6 +451,10 @@ flowchart TD pkg_subprocess_local["subprocess-local"] pkg_win32_process["win32-process"] end + subgraph group_team["packages/team"] + pkg_agent_team["agent-team"] + pkg_tool_agent_team["tool-agent-team"] + end subgraph group_telemetry["packages/telemetry"] pkg_otel["otel"] end @@ -1331,6 +1332,7 @@ flowchart TD pkg_tool_subagent_control --> pkg_llm pkg_tool_subagent_control --> pkg_session pkg_tool_subagent_control --> pkg_subagent + pkg_tool_subagent_control --> pkg_system_prompt pkg_tool_subagent_control --> pkg_tools pkg_api_session_controller --> pkg_agent pkg_api_session_controller --> pkg_agent_default_model @@ -1397,12 +1399,6 @@ flowchart TD pkg_session_reference --> pkg_subagent pkg_session_reference --> pkg_system_prompt pkg_session_reference --> pkg_typert_protocol - pkg_experimental_agent_team --> pkg_agent - pkg_experimental_agent_team --> pkg_llm - pkg_experimental_agent_team --> pkg_session - pkg_experimental_agent_team --> pkg_session_persistence - pkg_experimental_agent_team --> pkg_session_projection - pkg_experimental_agent_team --> pkg_subagent pkg_experimental_subagent_oar --> pkg_llm pkg_experimental_subagent_oar --> pkg_session pkg_experimental_subagent_oar --> pkg_subagent @@ -1427,6 +1423,13 @@ flowchart TD pkg_sdk_protocol --> pkg_session pkg_sdk_protocol --> pkg_subagent pkg_sdk_protocol --> pkg_user_questions + pkg_agent_team --> pkg_agent + pkg_agent_team --> pkg_llm + pkg_agent_team --> pkg_session + pkg_agent_team --> pkg_session_persistence + pkg_agent_team --> pkg_session_projection + pkg_agent_team --> pkg_subagent + pkg_agent_team --> pkg_typert_protocol pkg_workflow_ptc --> pkg_agent pkg_workflow_ptc --> pkg_llm pkg_workflow_ptc --> pkg_ptc_runtime @@ -1437,11 +1440,6 @@ flowchart TD pkg_workflow_ptc --> pkg_tools pkg_workflow_ptc --> pkg_workflow pkg_workflow_ptc --> pkg_working_directory - pkg_experimental_tool_agent_team --> pkg_agent - pkg_experimental_tool_agent_team --> pkg_experimental_agent_team - pkg_experimental_tool_agent_team --> pkg_session - pkg_experimental_tool_agent_team --> pkg_system_prompt - pkg_experimental_tool_agent_team --> pkg_tools pkg_schedule --> pkg_agent pkg_schedule --> pkg_api_session_controller pkg_schedule --> pkg_brand @@ -1458,6 +1456,12 @@ flowchart TD pkg_sdk_client --> pkg_llm pkg_sdk_client --> pkg_sdk_protocol pkg_sdk_client --> pkg_session + pkg_tool_agent_team --> pkg_agent + pkg_tool_agent_team --> pkg_agent_team + pkg_tool_agent_team --> pkg_session + pkg_tool_agent_team --> pkg_subagent + pkg_tool_agent_team --> pkg_system_prompt + pkg_tool_agent_team --> pkg_tools pkg_client_test_runtime --> pkg_api_gateway pkg_client_test_runtime --> pkg_api_session_controller pkg_client_test_runtime --> pkg_api_workspace_controller @@ -1551,6 +1555,7 @@ flowchart TD | [`client-shortcuts`](../packages/client/shortcuts) | `client` | — | | [`client-store`](../packages/client/store) | `client` | — | | [`client-ui-agent-preset`](../packages/client/ui-agent-preset) | `client` | — | +| [`client-ui-agent-team`](../packages/client/ui-agent-team) | `client` | — | | [`client-ui-approval`](../packages/client/ui-approval) | `client` | — | | [`client-ui-attachment`](../packages/client/ui-attachment) | `client` | — | | [`client-ui-brand-official`](../packages/client/ui-brand-official) | `client` | — | @@ -1616,9 +1621,7 @@ flowchart TD | [`client-ui-creation-recommendations`](../packages/experimental/client-ui-creation-recommendations) | `experimental` | — | | [`client-ui-plugin-recommendations`](../packages/experimental/client-ui-plugin-recommendations) | `experimental` | — | | [`cua-bundle`](../packages/experimental/cua-bundle) | `experimental` | — | -| [`experimental-agent-team-profile`](../packages/experimental/agent-team-profile) | `experimental` | — | | [`experimental-badge-skill-bundle`](../packages/experimental/badge-skill-bundle) | `experimental` | — | -| [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | — | | [`experimental-client-ui-claude-code-mods`](../packages/experimental/client-ui-claude-code-mods) | `experimental` | — | | [`experimental-client-ui-stats`](../packages/experimental/client-ui-stats) | `experimental` | — | | [`experimental-client-ui-voice-input`](../packages/experimental/client-ui-voice-input) | `experimental` | — | @@ -1860,22 +1863,22 @@ flowchart TD | [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process) | `subagent` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`subagent`](../packages/subagent/subagent) | | [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | +| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-file-upload`](../packages/client/file-upload), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`settings`](../packages/settings/settings), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-time`](../packages/util/time), [`util-values`](../packages/util/values), [`util-workspace-path`](../packages/util/workspace-path), [`working-directory`](../packages/session/working-directory), [`workspace`](../packages/workspace/workspace) | | [`plugin-market`](../packages/boot/plugin-market) | `boot` | [`plugin-manager`](../packages/boot/plugin-manager), [`settings`](../packages/settings/settings), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) | | [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`subagent`](../packages/subagent/subagent), [`user-approval`](../packages/interaction/user-approval) | | [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | | [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`spill`](../packages/spill/spill), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) | -| [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent) | | [`experimental-subagent-oar`](../packages/experimental/subagent-oar) | `experimental` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) | | [`experimental-tool-subagent-oar`](../packages/experimental/tool-subagent-oar) | `experimental` | [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | `experimental` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/experimental/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`tool-ralph`](../packages/experimental/tool-ralph) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`commands`](../packages/interaction/commands), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) | +| [`agent-team`](../packages/team/agent-team) | `team` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) | | [`workflow-ptc`](../packages/workflow/workflow-ptc) | `workflow` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow), [`working-directory`](../packages/session/working-directory) | -| [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | `experimental` | [`agent`](../packages/core/agent), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`util-time`](../packages/util/time), [`workspace`](../packages/workspace/workspace) | | [`sdk-client`](../packages/sdk/client) | `sdk` | [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) | +| [`tool-agent-team`](../packages/team/tool-agent-team) | `team` | [`agent`](../packages/core/agent), [`agent-team`](../packages/team/agent-team), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-hmr`](../packages/client/hmr), [`client-modules`](../packages/client/modules), [`client-store`](../packages/client/store), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-context-menu`](../packages/client/ui-context-menu), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web`](../packages/client/web), [`remote-mock`](../packages/test-support/remote-mock), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) | | [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) | | [`tool-schedule`](../packages/schedule/tool-schedule) | `schedule` | [`llm`](../packages/llm/llm), [`schedule`](../packages/schedule/schedule), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index 3b4f1d1275..8f7366da1c 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -9,8 +9,8 @@ en: 1387c4505dde2faf zh: 48570fa1c13d6910 /session-persistence-event-catalog/persistence-type-fingerprints: - en: 31c6dab0878b05a8 - zh: 176d0f7cbdc3fce3 + en: d334560286daab57 + zh: ddece2494dd2131c /session-persistence-event-catalog/event-envelope: en: 56414ffd00cdc32a zh: 6dc11eb013e95e41 @@ -27,8 +27,8 @@ en: 49891ca9be5ac123 zh: 49891ca9be5ac123 /session-persistence-event-catalog/events/agent-preset/agent-preset-selected-log-only: - en: 9eb4e8ec6b503d83 - zh: 6d96288a30d92b81 + en: 50a6b116a29bc6b0 + zh: 7fce128060557296 /session-persistence-event-catalog/events/approval: en: 9d2ec4c4c3664b9f zh: 9d2ec4c4c3664b9f @@ -225,17 +225,20 @@ en: a640800a38b3c874 zh: a640800a38b3c874 /session-persistence-event-catalog/events/team/team-member-log-only: - en: a2fe5b8582ef5f46 - zh: 42bfffcde3f3d886 + en: de16dcf2046b402c + zh: b6c73eb866413523 /session-persistence-event-catalog/events/team/team-message-delivered-log-only: - en: 4a716b14e33eab18 - zh: dcde26e4dc40b62e + en: 9c9d9238de0eeb89 + zh: 1f52e5da217dae3d /session-persistence-event-catalog/events/team/team-message-queued-log-only: - en: 05bf873244099da5 - zh: 8a0b62912087f4aa + en: 184b700f0a0363ae + zh: 74b7939bf1bface1 +/session-persistence-event-catalog/events/team/team-mode-log-only: + en: 9481b9f0697707e4 + zh: 83aa2d33782a7b00 /session-persistence-event-catalog/events/team/team-task-log-only: - en: 5ffeb64225feda2a - zh: d58129e189a726fb + en: 16a961e45e7ff021 + zh: c61e51809823dc17 /session-persistence-event-catalog/events/todo: en: 308d4bb4b6b96e35 zh: 308d4bb4b6b96e35 @@ -684,8 +687,11 @@ en: 49351fa0be662888 zh: 49351fa0be662888 /session-persistence-event-catalog/resolved-persistence-types/recall: - en: 5c5a7a7e6517bdf0 - zh: 5c5a7a7e6517bdf0 + en: c04491605decd774 + zh: c04491605decd774 +/session-persistence-event-catalog/resolved-persistence-types/refusal: + en: e55aa9d6983aecde + zh: e55aa9d6983aecde /session-persistence-event-catalog/resolved-persistence-types/rejected: en: b982810b4d04978d zh: b982810b4d04978d @@ -816,8 +822,11 @@ en: 6795a23aa9bc8d2e zh: 6795a23aa9bc8d2e /session-persistence-event-catalog/resolved-persistence-types/team-message-queued: - en: d683235390472024 - zh: d683235390472024 + en: efe4678972a1a64e + zh: efe4678972a1a64e +/session-persistence-event-catalog/resolved-persistence-types/team-mode: + en: 91a387e367e07f73 + zh: 91a387e367e07f73 /session-persistence-event-catalog/resolved-persistence-types/team-task: en: 7aa9286372341561 zh: 7aa9286372341561 @@ -1011,11 +1020,11 @@ en: a6a2e38fdd6d127a zh: 1fe82356d89f5769 /session-persistence-event-catalog/resolved-persistence-types/deepseeksearchllmrequest: - en: fd75d1f4b9bd643c - zh: dde892ea9752f298 + en: 054dbd6367e1403d + zh: 5b071d10ae1ec245 /session-persistence-event-catalog/resolved-persistence-types/developermessage: - en: c6171fd8144d74b1 - zh: 2f3dd34cc8d7ed92 + en: 58bc64eb0f7fb589 + zh: 8cc8279eb5786ef5 /session-persistence-event-catalog/resolved-persistence-types/epochheader: en: 3bdc8ecd0bdf0c9c zh: 0fe5b75c174ebab4 @@ -1125,11 +1134,11 @@ en: 6dbe099c62f7c226 zh: 6fd788f63c038c91 /session-persistence-event-catalog/resolved-persistence-types/llmretrystartedeventdata: - en: 06b510217b6056c4 - zh: 5b3a1e022f96de57 + en: a3754a186db0e1bb + zh: e9467b50a27889d5 /session-persistence-event-catalog/resolved-persistence-types/message: - en: 7a8ec632289bc7f9 - zh: 5c90cb4f3d5d0a67 + en: 0be581a4095c3493 + zh: 2c815426f3d3717a /session-persistence-event-catalog/resolved-persistence-types/messagefeedbackdelete: en: c582953270d4ca44 zh: 966ccfe2e7660a2a @@ -1140,14 +1149,14 @@ en: f58575a1fc96df42 zh: 9f0c6ed9033e5c9d /session-persistence-event-catalog/resolved-persistence-types/messagefeedbackrating: - en: 65fb012fcc0fa7db - zh: b829c586714e45e2 + en: 702995c31653ab91 + zh: cf60875749663bd3 /session-persistence-event-catalog/resolved-persistence-types/messagesource: - en: ddeed90cb6112496 - zh: cefb41765c45b22f + en: 1ec5fd7b1b8050a9 + zh: ee4a3f61be48cd17 /session-persistence-event-catalog/resolved-persistence-types/message~2: - en: 8e96551640079569 - zh: a06d587638d42237 + en: 9e25f2f1513f7b15 + zh: 2723b6aa3d8b723e /session-persistence-event-catalog/resolved-persistence-types/modelmessagesource: en: 5f9154ca34235ab9 zh: a833105910ebfa82 @@ -1206,11 +1215,11 @@ en: 9a54c4478ad3b620 zh: 02c4c1fd7e252770 /session-persistence-event-catalog/resolved-persistence-types/sessiontitleeventdata: - en: 1f9f610fa0e05abf - zh: 11560710a1b871de + en: a537c3568f3f07db + zh: 20c97f3c3fd82e8b /session-persistence-event-catalog/resolved-persistence-types/sessiontitlellmrequesteventdata: - en: 465fe144e60dd62f - zh: 29e1029426106c3b + en: efa083dfdcf04208 + zh: eed525395ff9f2e8 /session-persistence-event-catalog/resolved-persistence-types/sessiontitlemodelidentity: en: 9531468264499498 zh: 9da23c0ad2776563 @@ -1236,11 +1245,11 @@ en: 725539dcd5c335ed zh: 5ca08d3667f84ff9 /session-persistence-event-catalog/resolved-persistence-types/subagentdescriptordata: - en: c56821e57987ae9e - zh: 0364f59a472d51da + en: 2b9c68682eec6f78 + zh: 8ef5e64326c1eae6 /session-persistence-event-catalog/resolved-persistence-types/subagentsettledmessagesource: - en: d34acab659e74957 - zh: 25cabc6c9fb44a7e + en: e6d9113dc09f4098 + zh: 3426e4f2f3cc5612 /session-persistence-event-catalog/resolved-persistence-types/surfaceop: en: 737806b6ec54263d zh: b4e44293950ddf8c @@ -1248,26 +1257,26 @@ en: 94876079d6696df9 zh: d25c7ad9f5054eb7 /session-persistence-event-catalog/resolved-persistence-types/systempromptmessagesource: - en: a3eabd0e9fc565dd - zh: fa763aefdbc1bdb8 + en: 967c82dcda03d0d4 + zh: ad590611be701d97 /session-persistence-event-catalog/resolved-persistence-types/teammemberphase: - en: 2bf8f06fe328ab33 - zh: ded602f18e0deadf + en: bf78ae1a14d64469 + zh: 0d5c34aff23a133c /session-persistence-event-catalog/resolved-persistence-types/teammembersnapshot: - en: 8a37845006830548 - zh: b8e7caf6189b92df + en: 1815ad87650025e6 + zh: d2a0dceabcc2090e /session-persistence-event-catalog/resolved-persistence-types/teammessagesnapshot: - en: 9b94faac43ba068f - zh: a83582be6a6a3c1a + en: d7ba2fe5b24c90cb + zh: b26f7e1ef0eccce0 /session-persistence-event-catalog/resolved-persistence-types/teammessagesource: - en: 269f0a4fcbea1c0c - zh: 5f2c50c03c951995 + en: 2ca6c43fc056fd37 + zh: 3d5b5f2917f326fc /session-persistence-event-catalog/resolved-persistence-types/teamtasksnapshot: - en: cfa8230ef5d661b2 - zh: cb40ce86d6778f09 + en: 0c6aac89e3dcb25c + zh: d772017667ffc2a1 /session-persistence-event-catalog/resolved-persistence-types/teamtaskstatus: - en: 9bff57038f472770 - zh: 4eec0e793ae16d53 + en: b6bd8e9986773217 + zh: ed177c2f090964af /session-persistence-event-catalog/resolved-persistence-types/textblock: en: 7ad0f4915397b511 zh: bcaa8bd449927989 @@ -1320,14 +1329,14 @@ en: 778debd61e0284a2 zh: 2cc82ad9e80f18b1 /session-persistence-event-catalog/resolved-persistence-types/turnendreason: - en: 953127f7cf1e0866 - zh: 3613ef1aafd8191a + en: bcd450bc3ade2a1a + zh: e442be61e4391383 /session-persistence-event-catalog/resolved-persistence-types/usermessage: - en: a15a08beb2b824da - zh: 4fd8dc6b9680c4cf + en: 6f29a4e108379f9a + zh: 2bf0f494b8fcd2b2 /session-persistence-event-catalog/resolved-persistence-types/usermessage~2: - en: 85e537b82f1da7d8 - zh: 73a1b9d4b5d29ab0 + en: e4fc10ce549b51ad + zh: cb29ffcb8003b355 /session-persistence-event-catalog/resolved-persistence-types/workflowagentoutcome: en: d0a3f9aab18c3de5 zh: 76ac69a835ca4e29 @@ -1350,11 +1359,11 @@ en: 0223d7ffb67ac272 zh: c2a8221caab6d350 /session-persistence-event-catalog/resolved-persistence-types/number~2: - en: 2786fef560851bb4 - zh: 5d787475356b2606 + en: 4147b170f52814af + zh: e78991ac1c41ebec /session-persistence-event-catalog/resolved-persistence-types/string: - en: 1e484bd12747db9f - zh: 1e6fc6aa9066a702 + en: c178efefdf01cade + zh: 200b1ab00ed8bfd3 /session-persistence-event-catalog/resolved-persistence-types/string~2: en: 0089620bf4638263 zh: 1922bef1713094bb @@ -1392,8 +1401,11 @@ en: 9e296094df7462ba zh: e7e3255a3ada9279 /session-persistence-event-catalog/resolved-persistence-types/union-3-variants~4: - en: 5fc89a7ec1cecd78 - zh: dc66aa8e4aae1847 + en: 4ac5570e2b5181be + zh: 447832dc281d0caf +/session-persistence-event-catalog/resolved-persistence-types/union-5-variants: + en: f60d7d997e33f12d + zh: ec049a1c1ab9f8b3 /session-persistence-event-catalog/resolved-persistence-types/union-6-variants: en: ca99faddb359b6bb zh: 5c7ac9b445d9e5be @@ -1410,8 +1422,8 @@ en: c861888d6239b700 zh: a9b2271d3c6b8737 /session-persistence-event-catalog/resolved-persistence-types/agentpreset: - en: 1afbe8196f161601 - zh: a320a36a8928817c + en: 1282a0b3d5fa8d4d + zh: aa533aea643dbefb /session-persistence-event-catalog/resolved-persistence-types/allowedmodels: en: 6178321110ceb69d zh: 0a9d94cab9d57487 @@ -1491,8 +1503,11 @@ en: 9748b4f0facfe9f8 zh: 83ae2cd2cd02b804 /session-persistence-event-catalog/resolved-persistence-types/displayname-providerid-subjectid-tenantid: - en: d2c98ed9b98325ae - zh: bf8c2f7967cf9cc8 + en: c6fbe4268a4430db + zh: 8fec9d6def3c775c +/session-persistence-event-catalog/resolved-persistence-types/enabled: + en: b8b8f28b5bfcd153 + zh: 602d970c3c090522 /session-persistence-event-catalog/resolved-persistence-types/end-start: en: 4b7a8793663d3fef zh: 249459c825d330c3 @@ -1512,11 +1527,11 @@ en: 4cb1c48095df98dc zh: 5a03e44761af559e /session-persistence-event-catalog/resolved-persistence-types/header-reason-startsseries: - en: 7d1610d20e987988 - zh: 1297cba0f2a2c73d + en: c21561adcf9398a5 + zh: fac8b21392750aae /session-persistence-event-catalog/resolved-persistence-types/headerseq-message-step-turn: - en: 4f9635604ed5b84d - zh: 74810a2d049907e6 + en: 081d257a87d8fb2d + zh: 49bdba3da8d5f7e4 /session-persistence-event-catalog/resolved-persistence-types/height-width: en: a5df50710083ac25 zh: aad3ccf6f6de70e4 @@ -1533,11 +1548,11 @@ en: 41f73c58a77dfa28 zh: af77f4aee3cb4a40 /session-persistence-event-catalog/resolved-persistence-types/inherited: - en: 8b5c65ce7b57514f - zh: 231494d71e5d635a + en: 7c2c00c4635970ca + zh: 19818a2c43b4c57d /session-persistence-event-catalog/resolved-persistence-types/inserted-outcome-removedcount-start: - en: 5a3be677a8dc51ff - zh: c5fc8b80f73e72d9 + en: 67b7eda689a3a115 + zh: 5ffdf39f3ee061b2 /session-persistence-event-catalog/resolved-persistence-types/interrupted-message-step-stream: en: 8e86cd838e3e4a49 zh: f5fec2a8adf1f3cc @@ -1905,17 +1920,17 @@ en: 3216b23c131d0070 zh: 73ca171c6014f16e /session-persistence-event-catalog/resolved-persistence-types/member-teamid-version: - en: 82b0989dad6bbfc0 - zh: 0789b63f1d59b2ed + en: 815fc4d70ba0375b + zh: ae9f6d2a163ff62b /session-persistence-event-catalog/resolved-persistence-types/message-step-turn: en: 86fe91fe83eeb08a zh: 2ebf2f1904829753 /session-persistence-event-catalog/resolved-persistence-types/message-teamid-version: - en: f778769acb264dd5 - zh: eba9b81d04e9f57b + en: edb364403e9e9f84 + zh: d1298ec2e67a0eeb /session-persistence-event-catalog/resolved-persistence-types/messageid-targetid-teamid-version: - en: 91a948ede28ebcb5 - zh: 8a2b0ac6bb68a09e + en: 42a1e343b5e87854 + zh: ae777d3cd40835a6 /session-persistence-event-catalog/resolved-persistence-types/mode-source: en: 3028b222670ce5ad zh: cae4c9437e560d61 @@ -1947,8 +1962,8 @@ en: 5360af614fe753e3 zh: 96427c489cc71de6 /session-persistence-event-catalog/resolved-persistence-types/task-teamid-version: - en: a2fcd46e54b14120 - zh: d9a6e0e50bbff9ad + en: 4952b12d354b8aaf + zh: b8c2ef51b7ab082e /session-persistence-event-catalog/resolved-persistence-types/todos: en: 8ab873d30e062fd5 zh: a138832c6e299ebb @@ -1956,11 +1971,11 @@ en: 374b8adee8d571fa zh: b21057efc58e6490 /session-persistence-event-catalog/resolved-persistence-types/type-agent-preset-selected: - en: da6e084997b20cdc - zh: 1780f6fcf3496de1 + en: 2ce26699bb57cf69 + zh: a5fc9ea510a3d458 /session-persistence-event-catalog/resolved-persistence-types/type-agent-inbox-spliced: - en: 1a79ed1a97145a14 - zh: d6f658a85c1e65f7 + en: ae0b65d2268c758b + zh: 8244a6d65cfb01f3 /session-persistence-event-catalog/resolved-persistence-types/type-approval-asked: en: 9c77f18b0dad22f0 zh: 8894cf2878fe90db @@ -2004,11 +2019,11 @@ en: ba73fb10b7e2ffc9 zh: 8d5c97e63e5f16da /session-persistence-event-catalog/resolved-persistence-types/type-deliverables-presented: - en: b9c35ef9a9e1d585 - zh: 7b8ad008c8b7389c + en: f5c7b6c1d3610861 + zh: 2baec8a404127ce8 /session-persistence-event-catalog/resolved-persistence-types/type-developer-message: - en: b0a3c504326f42f9 - zh: f13a1c1ca28f7da3 + en: fe3390ea4e6769e3 + zh: 5a25cbdf990ef85f /session-persistence-event-catalog/resolved-persistence-types/type-disabled: en: 585627a50ece4e5f zh: 157837aee3da0ac3 @@ -2082,11 +2097,11 @@ en: acf7e21e4d21ebff zh: fb83a853634d204a /session-persistence-event-catalog/resolved-persistence-types/type-session-title: - en: 5a42c1482248e385 - zh: c7ba7d071e4756de + en: d26d1cd97d87f4a6 + zh: 992f1290578da1f4 /session-persistence-event-catalog/resolved-persistence-types/type-session-title-llm-request: - en: d88fd740ff167f04 - zh: fe6598a1bf4cd9bf + en: 211b5793ff00bed6 + zh: e8382b4f9adb08fd /session-persistence-event-catalog/resolved-persistence-types/type-step-end: en: 7fc233c4a569dae2 zh: bc2b3fed4a397c00 @@ -2112,8 +2127,11 @@ en: 2a00c9dbe70dcd8e zh: 96573157cd613c91 /session-persistence-event-catalog/resolved-persistence-types/type-team-message-queued: - en: 3baf69fe52f2d751 - zh: 185be1b973fac77e + en: 735e6acfb6180948 + zh: de7626c523ae57a2 +/session-persistence-event-catalog/resolved-persistence-types/type-team-mode: + en: 0b50dd6b99450d8e + zh: a9f8d2b85e2092af /session-persistence-event-catalog/resolved-persistence-types/type-team-task: en: 872aad5b748d70d5 zh: 396f986a703b76fd @@ -2163,11 +2181,11 @@ en: 1b045de96172be4b zh: 8a15c6f35b576b37 /session-persistence-event-catalog/resolved-persistence-types/type-usage: - en: a1d9300dd7a7b043 - zh: 3cb1e0e6834e9765 + en: 1f43a7944fa86bcf + zh: faa883c372923440 /session-persistence-event-catalog/resolved-persistence-types/type-user-message: - en: 44035d460a9d926b - zh: 8006b6dbfc58d5ae + en: 28e1108b02b392dc + zh: 0dde507cef7d790e /session-persistence-event-catalog/resolved-persistence-types/type-web-deepseek-search-llm-request: en: 99c8837344178a47 zh: bf746f5678346596 diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 73baf8afad..16c1cb90f8 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -23,7 +23,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover | `JsonlHeaderLine` | header | `18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f` | [`HeaderLine`](#persistence-type-sha256-18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f) | | `SessionEventEnvelope` | envelope | `5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc` | [`union (3 variants)`](#persistence-type-sha256-5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc) | | `event:agent-preset/selected` | event | `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b` | [`{ type: "agent-preset/selected" }`](#persistence-type-sha256-a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b) | -| `event:agent/inbox/spliced` | event | `cdf988172b589b230f2bce868489def1bb575eb0501076ac757a8f369ac5c64e` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-cdf988172b589b230f2bce868489def1bb575eb0501076ac757a8f369ac5c64e) | +| `event:agent/inbox/spliced` | event | `ddce54a3873dc57113b0cd38ad9b23b3c259186139e8847e5e5f415d86f52482` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-ddce54a3873dc57113b0cd38ad9b23b3c259186139e8847e5e5f415d86f52482) | | `event:approval/asked` | event | `3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1` | [`{ type: "approval/asked" }`](#persistence-type-sha256-3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1) | | `event:approval/decided` | event | `bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3` | [`{ type: "approval/decided" }`](#persistence-type-sha256-bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3) | | `event:approval/policy` | event | `26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78` | [`{ type: "approval/policy" }`](#persistence-type-sha256-26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78) | @@ -36,7 +36,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover | `event:compaction/start` | event | `db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965` | [`{ type: "compaction/start" }`](#persistence-type-sha256-db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965) | | `event:compaction/summary` | event | `e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602` | [`{ type: "compaction/summary" }`](#persistence-type-sha256-e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602) | | `event:deliverables/presented` | event | `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176` | [`{ type: "deliverables/presented" }`](#persistence-type-sha256-13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176) | -| `event:developer/message` | event | `70d2acdec1ebcfea56d02f7233ebb9ec0f7f494447fb952884c22c3afdc01d6f` | [`{ type: "developer/message" }`](#persistence-type-sha256-70d2acdec1ebcfea56d02f7233ebb9ec0f7f494447fb952884c22c3afdc01d6f) | +| `event:developer/message` | event | `70ea8e7943c5e2d76905bcc2e694e8c3d5b138aaae3609f3e261b8d369a6e35a` | [`{ type: "developer/message" }`](#persistence-type-sha256-70ea8e7943c5e2d76905bcc2e694e8c3d5b138aaae3609f3e261b8d369a6e35a) | | `event:feedback/message-delete` | event | `3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796` | [`{ type: "feedback/message-delete" }`](#persistence-type-sha256-3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796) | | `event:feedback/message-put` | event | `b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf` | [`{ type: "feedback/message-put" }`](#persistence-type-sha256-b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf) | | `event:feedback/record` | event | `b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8` | [`{ type: "feedback/record" }`](#persistence-type-sha256-b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8) | @@ -58,7 +58,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover | `event:session-log-deepseek/delivery-accepted` | event | `d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9` | [`{ type: "session-log-deepseek/delivery-accepted" }`](#persistence-type-sha256-d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9) | | `event:session/end-seed` | event | `5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e` | [`{ type: "session/end-seed" }`](#persistence-type-sha256-5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e) | | `event:session/title` | event | `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | [`{ type: "session/title" }`](#persistence-type-sha256-1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5) | -| `event:session/title-llm-request` | event | `2e505098f9209e52eebd577f1a6f9d4e773ec4d78987aff28a7c786442b9f54c` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-2e505098f9209e52eebd577f1a6f9d4e773ec4d78987aff28a7c786442b9f54c) | +| `event:session/title-llm-request` | event | `d95c624eddecdacf1ee74356196c09d2f3fcd625714be5e852a6b643fa12a7ca` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-d95c624eddecdacf1ee74356196c09d2f3fcd625714be5e852a6b643fa12a7ca) | | `event:step/end` | event | `e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05` | [`{ type: "step/end" }`](#persistence-type-sha256-e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05) | | `event:step/start` | event | `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | [`{ type: "step/start" }`](#persistence-type-sha256-4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986) | | `event:subagent/catalog` | event | `53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac) | @@ -68,6 +68,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover | `event:team/member` | event | `4fb59762612c3e3ac3a3bd4f84c9c148d3c3893bd422ba2b201cc039fabd49bc` | [`{ type: "team/member" }`](#persistence-type-sha256-4fb59762612c3e3ac3a3bd4f84c9c148d3c3893bd422ba2b201cc039fabd49bc) | | `event:team/message/delivered` | event | `48f9c19417a1abbedfa59f4667bba36b93ac2db407adf5e84cb3ba0de30942cb` | [`{ type: "team/message/delivered" }`](#persistence-type-sha256-48f9c19417a1abbedfa59f4667bba36b93ac2db407adf5e84cb3ba0de30942cb) | | `event:team/message/queued` | event | `21fb6a90d5068f6a0003b7ab316ed2f56342477146a65c00db0f13c4d8df667d` | [`{ type: "team/message/queued" }`](#persistence-type-sha256-21fb6a90d5068f6a0003b7ab316ed2f56342477146a65c00db0f13c4d8df667d) | +| `event:team/mode` | event | `920d87e22b7409bcc7224f8b1dc7f5101c33574c75627c65baab12b85cc96e75` | [`{ type: "team/mode" }`](#persistence-type-sha256-920d87e22b7409bcc7224f8b1dc7f5101c33574c75627c65baab12b85cc96e75) | | `event:team/task` | event | `d595ec73b32b016a6055333c67a5d646032b22e672da1d4a09c5b1ae398a093a` | [`{ type: "team/task" }`](#persistence-type-sha256-d595ec73b32b016a6055333c67a5d646032b22e672da1d4a09c5b1ae398a093a) | | `event:todo/write` | event | `b978cff734e62143eb56c9125423ec275405eda969802d42aaf73ecb987d3b26` | [`{ type: "todo/write" }`](#persistence-type-sha256-b978cff734e62143eb56c9125423ec275405eda969802d42aaf73ecb987d3b26) | | `event:tool-workflow/agent-end` | event | `babf9ee4d1af62bf6c3a8103737f7a5e4e78ce179be835ce05a38803e15884b7` | [`{ type: "tool-workflow/agent-end" }`](#persistence-type-sha256-babf9ee4d1af62bf6c3a8103737f7a5e4e78ce179be835ce05a38803e15884b7) | @@ -80,7 +81,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover | `event:tool/result` | event | `7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397` | [`{ type: "tool/result" }`](#persistence-type-sha256-7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397) | | `event:turn/end` | event | `0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce` | [`{ type: "turn/end" }`](#persistence-type-sha256-0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce) | | `event:turn/start` | event | `aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8` | [`{ type: "turn/start" }`](#persistence-type-sha256-aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8) | -| `event:user/message` | event | `5bb231bc5afdb532a6898a20604e76bf0c1db438bfcdc17d4847dceaddc02451` | [`{ type: "user/message" }`](#persistence-type-sha256-5bb231bc5afdb532a6898a20604e76bf0c1db438bfcdc17d4847dceaddc02451) | +| `event:user/message` | event | `dcb07e04496586d2664d72dab0e94312917da56927b2a0dc3ee53d6a33e32450` | [`{ type: "user/message" }`](#persistence-type-sha256-dcb07e04496586d2664d72dab0e94312917da56927b2a0dc3ee53d6a33e32450) | | `event:web/deepseek-search-llm-request` | event | `0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52` | [`{ type: "web/deepseek-search-llm-request" }`](#persistence-type-sha256-0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52) | | `event:working-directory/change` | event | `124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3` | [`{ type: "working-directory/change" }`](#persistence-type-sha256-124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3) | | `event:workspace/changes` | event | `e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72` | [`{ type: "workspace/changes" }`](#persistence-type-sha256-e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72) | @@ -198,15 +199,14 @@ Source: [`packages/core/agent/src/types.ts:110`](../packages/core/agent/src/type * The session's agent preset was chosen after creation, while the session * was still blank. Log-only: it records the composition later turns ran * under, so a resumed or forked session rebuilds the same one instead of - * the header's creation-time value. In a child Session, an occurrence - * before its first own `subagent/descriptor` marks an independent spawn - * selection that cold resume restores instead of joining the parent's - * composition (`dsh-subagent` README, "Restoring child composition"). + * the header's creation-time value. Subagent restoration reads this saved + * selection independently of its creator (`dsh-subagent` README, + * "Restoring child composition"). */ 'agent-preset/selected': { agentPreset: string } ``` -Source: [`packages/preset/agent-preset-registry/src/session.ts:31`](../packages/preset/agent-preset-registry/src/session.ts) +Source: [`packages/preset/agent-preset-registry/src/session.ts:30`](../packages/preset/agent-preset-registry/src/session.ts) ### `approval/*` @@ -1009,13 +1009,13 @@ Source: [`packages/core/session/src/types.ts:338`](../packages/core/session/src/ #### `team/member` — log-only ```ts persistence-catalog -/** Whole teammate lifecycle value, stored only in the Team Lead Session. */ +/** Historical teammate lifecycle value; new teammates are continuable entries in the Lead's subagent catalog. */ 'team/member': { version: 2; teamId: TeamId; member: TeamMemberSnapshot } ``` Types: [TeamId](subsystems/agent-team.md) · [TeamMemberSnapshot](subsystems/agent-team.md) -Source: [`packages/experimental/agent-team/src/types.ts:230`](../packages/experimental/agent-team/src/types.ts) +Source: [`packages/team/agent-team/src/types.ts:173`](../packages/team/agent-team/src/types.ts) @@ -1033,7 +1033,7 @@ Source: [`packages/experimental/agent-team/src/types.ts:230`](../packages/experi Types: [TeamId](subsystems/agent-team.md) · [TeamMessageId](subsystems/agent-team.md) -Source: [`packages/experimental/agent-team/src/types.ts:236`](../packages/experimental/agent-team/src/types.ts) +Source: [`packages/team/agent-team/src/types.ts:179`](../packages/team/agent-team/src/types.ts) @@ -1046,7 +1046,18 @@ Source: [`packages/experimental/agent-team/src/types.ts:236`](../packages/experi Types: [TeamId](subsystems/agent-team.md) · [TeamMessageSnapshot](subsystems/agent-team.md) -Source: [`packages/experimental/agent-team/src/types.ts:234`](../packages/experimental/agent-team/src/types.ts) +Source: [`packages/team/agent-team/src/types.ts:177`](../packages/team/agent-team/src/types.ts) + + + +#### `team/mode` — log-only + +```ts persistence-catalog +/** User-selected Team mode; child Agents use the selection of their runtime root. */ +'team/mode': { enabled: boolean } +``` + +Source: [`packages/team/agent-team/src/types.ts:171`](../packages/team/agent-team/src/types.ts) @@ -1059,7 +1070,7 @@ Source: [`packages/experimental/agent-team/src/types.ts:234`](../packages/experi Types: [TeamId](subsystems/agent-team.md) · [TeamTaskSnapshot](subsystems/agent-team.md) -Source: [`packages/experimental/agent-team/src/types.ts:232`](../packages/experimental/agent-team/src/types.ts) +Source: [`packages/team/agent-team/src/types.ts:175`](../packages/team/agent-team/src/types.ts) ### `todo/*` @@ -2355,6 +2366,14 @@ SHA-256: `383cf73ae4748e5beb281ae74857ba1ffb92ae01075b9fbf51af78b666007a8f` `"recall"` + + +### `"refusal"` + +SHA-256: `4a561c111a78d4520e850c93795369ea169ff86abd082aee86c8fd38121a65aa` + +`"refusal"` + ### `"rejected"` @@ -2707,6 +2726,14 @@ SHA-256: `4e011753f8263f436efaabfbb3b4db53a98fc55342c5ded2daf7208b217be159` `"team/message/queued"` + + +### `"team/mode"` + +SHA-256: `d8a476d2c6fd30df6ec0e799f91716d1518c780d23b4121dce545abfcfb7c5a5` + +`"team/mode"` + ### `"team/task"` @@ -3383,7 +3410,7 @@ Sources: [`packages/web/web-search-deepseek/src/provider.ts:55`](../packages/web | `body` | required | [`{ max_tokens, messages, model, thinking?, … }`](#persistence-type-sha256-9c001ecb5e1d3c3894410db2a1e383842e5f8454fb52182338686158abc1b3ee) | | `endpoint` | required | `string` | - + @@ -3391,7 +3418,7 @@ Sources: [`packages/web/web-search-deepseek/src/provider.ts:55`](../packages/web ### `DeveloperMessage` -SHA-256: `64ef542b1bfc988382d337f2ea85aab8ad76ea55341cf082163919d315688428` +SHA-256: `01c161379a3c3394a642807843e108ef911a8b7dbffd6d7de43f4484c88b0796` Sources: [`packages/llm/llm/src/message.ts:157`](../packages/llm/llm/src/message.ts) @@ -3400,7 +3427,7 @@ Sources: [`packages/llm/llm/src/message.ts:157`](../packages/llm/llm/src/message | `content` | required | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) | | `id` | required | `string` | | `role` | required | `"developer"` | -| `source` | required | [`MessageSource`](#persistence-type-sha256-7e9f1072c17f7aa86547c9c8b02766c1006d866a1859a3d677a336e932f70d1a) | +| `source` | required | [`MessageSource`](#persistence-type-sha256-b120362fd216a16a40fe787f6d3286086aa131266322bab79c026a130ccd0f72) | Source compatibility: `source` — `session-source-attribution` v1; `session.developer-message.source`; `kind`; `preserve`. @@ -4099,7 +4126,7 @@ Sources: [`packages/llm/llm-retry/src/types.ts:43`](../packages/llm/llm-retry/sr | `step` | required | `number` | | `turn` | required | `number` | - + @@ -4107,15 +4134,15 @@ Sources: [`packages/llm/llm-retry/src/types.ts:43`](../packages/llm/llm-retry/sr ### `Message` -SHA-256: `c78b9097bfae83cd08f0a98a92730236ba80ac61d712046959e4fc7f28aa1f36` +SHA-256: `e272493e3ea0b33b2e1d64b6a80e9d8bd3d1d466cc115f2facbdd598f8644a39` Sources: [`packages/llm/llm/src/message.ts:196`](../packages/llm/llm/src/message.ts) One of: - [`ToolResultMessage`](#persistence-type-sha256-4b259f7f8f0a44ad5e90b70a73553ced1df27012d126921d397ff403b6c86731) -- [`DeveloperMessage`](#persistence-type-sha256-64ef542b1bfc988382d337f2ea85aab8ad76ea55341cf082163919d315688428) -- [`UserMessage`](#persistence-type-sha256-4035903993dfee62cca2142c0ca51ab57075909ddee2817d0620daa97ac4d4ce) +- [`DeveloperMessage`](#persistence-type-sha256-01c161379a3c3394a642807843e108ef911a8b7dbffd6d7de43f4484c88b0796) +- [`UserMessage`](#persistence-type-sha256-613660455704fb82a825b5b766e000aa64b5b4d44b4f10e55cffe968d4ec6ab1) - [`SystemMessage`](#persistence-type-sha256-09f3f8dc417f278257202714132f98ba7b52957d7b558c4a04456603890eb363) - [`AssistantMessage`](#persistence-type-sha256-fa87537f4a9c1e25602990a8bf686b383e9e00352ec8946e16ae24ba7c1625be) @@ -4192,7 +4219,7 @@ One of: - `"negative"` - `"positive"` - + @@ -4200,7 +4227,7 @@ One of: ### `MessageSource` -SHA-256: `7e9f1072c17f7aa86547c9c8b02766c1006d866a1859a3d677a336e932f70d1a` +SHA-256: `b120362fd216a16a40fe787f6d3286086aa131266322bab79c026a130ccd0f72` Sources: [`packages/llm/llm/src/message.ts:136`](../packages/llm/llm/src/message.ts) @@ -4279,7 +4306,7 @@ Sources: [`packages/llm/llm/src/message.ts:136`](../packages/llm/llm/src/message | `session-reference` | `"recall"` | `references`: [`{ capturedFormatVersion?, capturedThroughSeq, compacted, inputIndex, … }[]`](#persistence-type-sha256-df843efd87c78cf8de98f8e0d0216956cd568d0d0e56a22c853663e274aa1a18); `version`: `1` | [`SessionReferenceSource`](#persistence-type-sha256-f56c5b59ca0161310c338d31dd16cf14287ccd602ff4181058df178c1c286d7f) | | `skill-catalog` | `"catalog"` | `entries`: [`{ description, name }[]`](#persistence-type-sha256-aba175fb20ffc71112c55efe70458ff5d7dbfb83501892b4ba8d077f0ea5d7ba) | [`SkillCatalogSource`](#persistence-type-sha256-65d583e66518c5236015e0846470fb32efcf92375ed306aef14e50a9d2cd2099) | | `skill-invocation` | `"instructions"` | `name`: `string` | [`SkillInvocationSource`](#persistence-type-sha256-672c4aa2a9b736e3d6e5da6346d2e2ed5e3573f2d0baafa18e7407c9f3fee3db) | -| `subagent-settled` | `"notice"` | `senderSessionId`: `string`; `summary`: `string` | [`SubagentSettledMessageSource`](#persistence-type-sha256-4e97c3d85c0fc817ee58873c38c25a2474abaaa0af4839311b50b68db3b8cf1a) | +| `subagent-settled` | `"notice"` | `senderSessionId`: `string`; `summary`: `string` | [`SubagentSettledMessageSource`](#persistence-type-sha256-848639f8787f035b480ec53cb5686850897c5e8006d87ca231cd8a9ee8634936) | | `system-prompt` | not declared | none | [`SystemPromptMessageSource`](#persistence-type-sha256-65a8400e451e667e5b6e34df72ceb81fca8c12330330fa72083ad40279b189ad) | | `team-message` | not declared | `messageId`: `string`; `senderId`: `string`; `senderName`: `string`; `teamId`: `string` | [`TeamMessageSource`](#persistence-type-sha256-2152caf1a71e588e4df2358a806dea24936c1c47131134f01a12939d12cdc0c0) | | `time-context` | `"catalog"` | none | [`{ kind: "time-context", form: "catalog" }`](#persistence-type-sha256-85fe96bf15d5c7559eec3bae4d6cf1a76fda9f5be05a29c2830cff560cab0c0d) | @@ -4325,13 +4352,13 @@ Sources: [`packages/llm/llm/src/message.ts:136`](../packages/llm/llm/src/message | `webhook` | `"notice"` | `deliveryId`: `string`; `provider`: `string`; `ruleId`: `string`; `source`: `string`; `summary`: `string` | [`{ kind: "webhook", form: "notice" }`](#persistence-type-sha256-9edc162949abf93bd5ff049191b345454ba104f583937ce5580fb50f067d2b94) | | `working-directory` | not declared | none | [`{ kind: "working-directory" }`](#persistence-type-sha256-2235d957bf4b7c6cf959231d480f0ba1f3a6d72d82981493fb6c7c963021d3d5) | - + ### `Message[]` -SHA-256: `ff2cbb53582d1827b02622482246979942f59e07e0393b76a161251423549e72` +SHA-256: `95b5cf90ea1f57da3319184d6f5f3811d0f651aa9113591c10e81c3d1e9b9f70` -Array of [`Message`](#persistence-type-sha256-c78b9097bfae83cd08f0a98a92730236ba80ac61d712046959e4fc7f28aa1f36). +Array of [`Message`](#persistence-type-sha256-e272493e3ea0b33b2e1d64b6a80e9d8bd3d1d466cc115f2facbdd598f8644a39). @@ -4699,7 +4726,7 @@ Sources: [`packages/session/session-title/src/types.ts:41`](../packages/session/ | `source` | required | [`SessionTitleSource`](#persistence-type-sha256-30d5a537b296685770b1241c4b4bbde19d5a31ff81aa23df73fab5e90b8fc65f) | | `title` | required | `string` | - + @@ -4707,7 +4734,7 @@ Sources: [`packages/session/session-title/src/types.ts:41`](../packages/session/ ### `SessionTitleLlmRequestEventData` -SHA-256: `7ff62f6599c083ceb5a0d7a8940ff85ba80dc1d7ddd6ba9339903e93f74cba54` +SHA-256: `601327bedca5f98a0fa4c27133bd35749f3af2baaaacf53badf074e59cea5d25` Sources: [`packages/session/session-title-llm/src/index.ts:43`](../packages/session/session-title-llm/src/index.ts) @@ -4715,7 +4742,7 @@ Sources: [`packages/session/session-title-llm/src/index.ts:43`](../packages/sess |---|---|---| | `maxTokens` | required | `number` | | `messageSeqs` | required | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | -| `messages` | required | [`Message[]`](#persistence-type-sha256-ff2cbb53582d1827b02622482246979942f59e07e0393b76a161251423549e72) | +| `messages` | required | [`Message[]`](#persistence-type-sha256-95b5cf90ea1f57da3319184d6f5f3811d0f651aa9113591c10e81c3d1e9b9f70) | | `reasoningEffort` | optional | `string` | | `route` | required | [`SessionTitleModelIdentity`](#persistence-type-sha256-07e1a58c58b593f507b8e7f25723bb0aa42baae6e00f5ecfddf28cb1e64d3235) | | `system` | required | `string` | @@ -4884,7 +4911,7 @@ One of: - [`ContinuableSubagentDescriptorData`](#persistence-type-sha256-6487b10ba7542f3c447b1e41a1c6a590b7bfafe0b9b66b49f34028da85bb970f) - [`OneShotSubagentDescriptorData`](#persistence-type-sha256-5f758f3de5f00c18968a7c6d347083aac5c9ae13f6a79c78f6611b23c4973564) - + @@ -4892,7 +4919,7 @@ One of: ### `SubagentSettledMessageSource` -SHA-256: `4e97c3d85c0fc817ee58873c38c25a2474abaaa0af4839311b50b68db3b8cf1a` +SHA-256: `848639f8787f035b480ec53cb5686850897c5e8006d87ca231cd8a9ee8634936` Sources: [`packages/subagent/subagent/src/continuation-messages.ts:29`](../packages/subagent/subagent/src/continuation-messages.ts) @@ -4901,6 +4928,7 @@ Sources: [`packages/subagent/subagent/src/continuation-messages.ts:29`](../packa | `form` | required | `"notice"` | | `kind` | required | `"subagent-settled"` | | `senderSessionId` | required | `string` | +| `stopReason` | optional | [`union (5 variants)`](#persistence-type-sha256-866f7b2dbdc5b421eab3e629312d1c839b75c259633b4b43b50d23bac44d92a0) | | `summary` | required | `string` | @@ -4957,7 +4985,7 @@ Sources: [`packages/llm/llm/src/message.ts:35`](../packages/llm/llm/src/message. - + @@ -4965,7 +4993,7 @@ Sources: [`packages/llm/llm/src/message.ts:35`](../packages/llm/llm/src/message. SHA-256: `dfce5db4aa42b7ebe0bb79eca92553587287e18c06b9eba85cbfb9d332e26acd` -Sources: [`packages/experimental/agent-team/src/types.ts:45`](../packages/experimental/agent-team/src/types.ts) +Sources: [`packages/team/agent-team/src/types.ts:47`](../packages/team/agent-team/src/types.ts) One of: @@ -4975,7 +5003,7 @@ One of: - + @@ -4983,7 +5011,7 @@ One of: SHA-256: `6598cd6da253948d6b207c6374a45335804c79b576866d6215649c2d1bc85e25` -Sources: [`packages/experimental/agent-team/src/types.ts:48`](../packages/experimental/agent-team/src/types.ts) +Sources: [`packages/team/agent-team/src/types.ts:50`](../packages/team/agent-team/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -4997,7 +5025,7 @@ Sources: [`packages/experimental/agent-team/src/types.ts:48`](../packages/experi - + @@ -5005,7 +5033,7 @@ Sources: [`packages/experimental/agent-team/src/types.ts:48`](../packages/experi SHA-256: `2eafefdc6f79a4922d68a787a09eaf4dd2b2369c3777fba4c05e800b5678ac8e` -Sources: [`packages/experimental/agent-team/src/types.ts:129`](../packages/experimental/agent-team/src/types.ts) +Sources: [`packages/team/agent-team/src/types.ts:108`](../packages/team/agent-team/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -5017,7 +5045,7 @@ Sources: [`packages/experimental/agent-team/src/types.ts:129`](../packages/exper - + @@ -5025,7 +5053,7 @@ Sources: [`packages/experimental/agent-team/src/types.ts:129`](../packages/exper SHA-256: `2152caf1a71e588e4df2358a806dea24936c1c47131134f01a12939d12cdc0c0` -Sources: [`packages/experimental/agent-team/src/types.ts:138`](../packages/experimental/agent-team/src/types.ts) +Sources: [`packages/team/agent-team/src/types.ts:117`](../packages/team/agent-team/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -5037,7 +5065,7 @@ Sources: [`packages/experimental/agent-team/src/types.ts:138`](../packages/exper - + @@ -5045,7 +5073,7 @@ Sources: [`packages/experimental/agent-team/src/types.ts:138`](../packages/exper SHA-256: `51edec100659da3a9bd9c3f6ba1180f5e64ec9a1585e5057080df45c180bd357` -Sources: [`packages/experimental/agent-team/src/types.ts:75`](../packages/experimental/agent-team/src/types.ts) +Sources: [`packages/team/agent-team/src/types.ts:64`](../packages/team/agent-team/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -5060,7 +5088,7 @@ Sources: [`packages/experimental/agent-team/src/types.ts:75`](../packages/experi - + @@ -5068,7 +5096,7 @@ Sources: [`packages/experimental/agent-team/src/types.ts:75`](../packages/experi SHA-256: `b2d2a01e51341a3bfec809198952c8c830a7899f4cb6ecd3c1dc0b5d9a2af943` -Sources: [`packages/experimental/agent-team/src/types.ts:72`](../packages/experimental/agent-team/src/types.ts) +Sources: [`packages/team/agent-team/src/types.ts:61`](../packages/team/agent-team/src/types.ts) One of: @@ -5390,7 +5418,7 @@ Sources: [`packages/core/session/src/types.ts:238`](../packages/core/session/src | `interrupted` | not declared | none | [`{ kind: "interrupted" }`](#persistence-type-sha256-56b7c047694875a843ba020dfea3c8380d54ae4ff88ab349e72004dc21e07c23) | | `max-tokens` | not declared | none | [`{ kind: "max-tokens" }`](#persistence-type-sha256-ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7) | - + @@ -5398,7 +5426,7 @@ Sources: [`packages/core/session/src/types.ts:238`](../packages/core/session/src ### `UserMessage` -SHA-256: `4035903993dfee62cca2142c0ca51ab57075909ddee2817d0620daa97ac4d4ce` +SHA-256: `613660455704fb82a825b5b766e000aa64b5b4d44b4f10e55cffe968d4ec6ab1` Sources: [`packages/llm/llm/src/message.ts:162`](../packages/llm/llm/src/message.ts) @@ -5407,19 +5435,19 @@ Sources: [`packages/llm/llm/src/message.ts:162`](../packages/llm/llm/src/message | `content` | required | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) | | `id` | required | `string` | | `role` | required | `"user"` | -| `source` | required | [`MessageSource`](#persistence-type-sha256-7e9f1072c17f7aa86547c9c8b02766c1006d866a1859a3d677a336e932f70d1a) | +| `source` | required | [`MessageSource`](#persistence-type-sha256-b120362fd216a16a40fe787f6d3286086aa131266322bab79c026a130ccd0f72) | Source compatibility: `source` — `session-source-attribution` v1; `session.user-message.source`; `kind`; `preserve`. Attribution-only additions: `action-cards`, `tmux-context`, `user-question-reply`, `working-directory`. - + ### `UserMessage[]` -SHA-256: `7c203f3d4dfa6638ff2601247cc7ca5d46731d2c614aa61217475b9eb1fbd0b3` +SHA-256: `690583582246babf6120f16eefd849dd760c5ec84619014951af403a1b3032c0` -Array of [`UserMessage`](#persistence-type-sha256-4035903993dfee62cca2142c0ca51ab57075909ddee2817d0620daa97ac4d4ce). +Array of [`UserMessage`](#persistence-type-sha256-613660455704fb82a825b5b766e000aa64b5b4d44b4f10e55cffe968d4ec6ab1). @@ -5537,12 +5565,6 @@ Array of `number`. - - - - - - @@ -5575,6 +5597,12 @@ Array of `number`. + + + + + + @@ -5611,7 +5639,7 @@ Array of `number`. SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b` -Sources: [`packages/api/session-controller/src/types.ts:388`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:21`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:33`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:9`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts) +Sources: [`packages/api/session-controller/src/types.ts:388`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/team/agent-team/src/types.ts:20`](../packages/team/agent-team/src/types.ts) · [`packages/team/agent-team/src/types.ts:32`](../packages/team/agent-team/src/types.ts) · [`packages/team/agent-team/src/types.ts:8`](../packages/team/agent-team/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts) `string` @@ -5755,6 +5783,20 @@ One of: - `"in_progress"` - `"pending"` + + +### `union (5 variants)` + +SHA-256: `866f7b2dbdc5b421eab3e629312d1c839b75c259633b4b43b50d23bac44d92a0` + +One of: + +- `"aborted"` +- `"completed"` +- `"error"` +- `"max-tokens"` +- `"refusal"` + ### `union (6 variants)` @@ -5820,7 +5862,7 @@ Sources: [`packages/plan/plan-mode/src/index.ts:53`](../packages/plan/plan-mode/ SHA-256: `6e52dd55a1d48954ed5464ec46fe34d9eb65f709baabc9561e7a279910b0c406` -Sources: [`packages/preset/agent-preset-registry/src/session.ts:31`](../packages/preset/agent-preset-registry/src/session.ts) +Sources: [`packages/preset/agent-preset-registry/src/session.ts:30`](../packages/preset/agent-preset-registry/src/session.ts) | Property | Presence | Type | |---|---|---| @@ -6234,6 +6276,18 @@ Sources: [`packages/experimental/liveshare/src/host/viewer-principal.ts:29`](../ | `subjectId` | required | `string` | | `tenantId` | required | `string` | + + +### `{ enabled }` + +SHA-256: `c2a13242efc3bea4b36a843ddf1524ff4d33c765a71b582fa1fdbfa4fa148434` + +Sources: [`packages/team/agent-team/src/types.ts:171`](../packages/team/agent-team/src/types.ts) + +| Property | Presence | Type | +|---|---|---| +| `enabled` | required | `boolean` | + ### `{ end, start }` @@ -6330,18 +6384,18 @@ Sources: [`packages/core/session/src/types.ts:398`](../packages/core/session/src | `reason` | required | [`RequestHeaderReason`](#persistence-type-sha256-530da6c0267813bb4511af72883cc518834e624e3b3220a7062955fd57a788a6) | | `startsSeries` | optional | `true` | - + ### `{ headerSeq?, message, step, turn }` -SHA-256: `53d4f8256bc58109468ea358ac54d437760cd765012360cafe72a401e6a41d4a` +SHA-256: `65377a76c6a58f785d95b89f89f2e13d289cb88a70ea74939d0ec08f429f0a6e` Sources: [`packages/core/session/src/types.ts:317`](../packages/core/session/src/types.ts) | Property | Presence | Type | |---|---|---| | `headerSeq` | optional | `number` | -| `message` | required | [`DeveloperMessage`](#persistence-type-sha256-64ef542b1bfc988382d337f2ea85aab8ad76ea55341cf082163919d315688428) | +| `message` | required | [`DeveloperMessage`](#persistence-type-sha256-01c161379a3c3394a642807843e108ef911a8b7dbffd6d7de43f4484c88b0796) | | `step` | required | `number` | | `turn` | required | `number` | @@ -6425,17 +6479,17 @@ Sources: [`packages/core/session/src/types.ts:435`](../packages/core/session/src |---|---|---| | `inherited` | optional | `true` | - + ### `{ inserted, outcome?, removedCount?, start, … }` -SHA-256: `d4d960185026266ed7bd127be1e252234689e8b4890c7c2677a644ba58f35401` +SHA-256: `e1880c67e735e228f3555df8a55a492db2c0b0c71bc8859e938e184a46c7db51` Sources: [`packages/core/agent/src/types.ts:110`](../packages/core/agent/src/types.ts) | Property | Presence | Type | |---|---|---| -| `inserted` | required | [`UserMessage[]`](#persistence-type-sha256-7c203f3d4dfa6638ff2601247cc7ca5d46731d2c614aa61217475b9eb1fbd0b3) | +| `inserted` | required | [`UserMessage[]`](#persistence-type-sha256-690583582246babf6120f16eefd849dd760c5ec84619014951af403a1b3032c0) | | `outcome` | optional | `"canceled"` | | `removedCount` | optional | `number` | | `start` | required | `number` | @@ -7858,7 +7912,7 @@ Sources: [`packages/web/web-search-deepseek/src/provider.ts:61`](../packages/web SHA-256: `b1ebd5cda47d90d980bb05bffc3f970c51537b9aff15b3bc3d639811727741b1` -Sources: [`packages/experimental/agent-team/src/types.ts:230`](../packages/experimental/agent-team/src/types.ts) +Sources: [`packages/team/agent-team/src/types.ts:173`](../packages/team/agent-team/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -7886,7 +7940,7 @@ Sources: [`packages/core/session/src/types.ts:338`](../packages/core/session/src SHA-256: `ea08ec1df0c33108fd03345b9af199cf04dea450aa045d3c683f216311403403` -Sources: [`packages/experimental/agent-team/src/types.ts:234`](../packages/experimental/agent-team/src/types.ts) +Sources: [`packages/team/agent-team/src/types.ts:177`](../packages/team/agent-team/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -7900,7 +7954,7 @@ Sources: [`packages/experimental/agent-team/src/types.ts:234`](../packages/exper SHA-256: `0bcfaf5566d271028429ac7b562cb59053c9d78b8ae14708c35fac835108d1b1` -Sources: [`packages/experimental/agent-team/src/types.ts:236`](../packages/experimental/agent-team/src/types.ts) +Sources: [`packages/team/agent-team/src/types.ts:179`](../packages/team/agent-team/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -8046,7 +8100,7 @@ Sources: [`packages/compaction/compaction-image-offload/src/projection.ts:25`](. SHA-256: `a40d12070f6f4a124f32fb3cc86e7857554702e32f0eda3080a25c4b9ac9b18c` -Sources: [`packages/experimental/agent-team/src/types.ts:232`](../packages/experimental/agent-team/src/types.ts) +Sources: [`packages/team/agent-team/src/types.ts:175`](../packages/team/agent-team/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -8094,17 +8148,17 @@ SHA-256: `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b` | `time` | required | `number` | | `type` | required | `"agent-preset/selected"` | - + ### `{ type: "agent/inbox/spliced" }` -SHA-256: `cdf988172b589b230f2bce868489def1bb575eb0501076ac757a8f369ac5c64e` +SHA-256: `ddce54a3873dc57113b0cd38ad9b23b3c259186139e8847e5e5f415d86f52482` | Property | Presence | Type | |---|---|---| -| `data` | required | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-d4d960185026266ed7bd127be1e252234689e8b4890c7c2677a644ba58f35401) | +| `data` | required | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-e1880c67e735e228f3555df8a55a492db2c0b0c71bc8859e938e184a46c7db51) | | `ignorable` | optional | `true` | | `seq` | required | `number` | | `time` | required | `number` | @@ -8345,17 +8399,17 @@ SHA-256: `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176` | `time` | required | `number` | | `type` | required | `"deliverables/presented"` | - + ### `{ type: "developer/message" }` -SHA-256: `70d2acdec1ebcfea56d02f7233ebb9ec0f7f494447fb952884c22c3afdc01d6f` +SHA-256: `70ea8e7943c5e2d76905bcc2e694e8c3d5b138aaae3609f3e261b8d369a6e35a` | Property | Presence | Type | |---|---|---| -| `data` | required | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-53d4f8256bc58109468ea358ac54d437760cd765012360cafe72a401e6a41d4a) | +| `data` | required | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-65377a76c6a58f785d95b89f89f2e13d289cb88a70ea74939d0ec08f429f0a6e) | | `ignorable` | optional | `true` | | `seq` | required | `number` | | `sourceEventSeqs` | optional | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | @@ -8755,17 +8809,17 @@ SHA-256: `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | `time` | required | `number` | | `type` | required | `"session/title"` | - + ### `{ type: "session/title-llm-request" }` -SHA-256: `2e505098f9209e52eebd577f1a6f9d4e773ec4d78987aff28a7c786442b9f54c` +SHA-256: `d95c624eddecdacf1ee74356196c09d2f3fcd625714be5e852a6b643fa12a7ca` | Property | Presence | Type | |---|---|---| -| `data` | required | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-7ff62f6599c083ceb5a0d7a8940ff85ba80dc1d7ddd6ba9339903e93f74cba54) | +| `data` | required | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-601327bedca5f98a0fa4c27133bd35749f3af2baaaacf53badf074e59cea5d25) | | `ignorable` | optional | `true` | | `seq` | required | `number` | | `time` | required | `number` | @@ -8917,6 +8971,22 @@ SHA-256: `21fb6a90d5068f6a0003b7ab316ed2f56342477146a65c00db0f13c4d8df667d` | `time` | required | `number` | | `type` | required | `"team/message/queued"` | + + + + +### `{ type: "team/mode" }` + +SHA-256: `920d87e22b7409bcc7224f8b1dc7f5101c33574c75627c65baab12b85cc96e75` + +| Property | Presence | Type | +|---|---|---| +| `data` | required | [`{ enabled }`](#persistence-type-sha256-c2a13242efc3bea4b36a843ddf1524ff4d33c765a71b582fa1fdbfa4fa148434) | +| `ignorable` | optional | `true` | +| `seq` | required | `number` | +| `time` | required | `number` | +| `type` | required | `"team/mode"` | + @@ -9188,17 +9258,17 @@ Sources: [`packages/llm/llm/src/types.ts:502`](../packages/llm/llm/src/types.ts) | `type` | required | `"usage"` | | `usage` | required | [`TokenUsage`](#persistence-type-sha256-fec5442b9ee4122e10514d81d5a43648003fe8bfb7ee2ba50c0cdbdd383ed394) | - + ### `{ type: "user/message" }` -SHA-256: `5bb231bc5afdb532a6898a20604e76bf0c1db438bfcdc17d4847dceaddc02451` +SHA-256: `dcb07e04496586d2664d72dab0e94312917da56927b2a0dc3ee53d6a33e32450` | Property | Presence | Type | |---|---|---| -| `data` | required | [`UserMessage`](#persistence-type-sha256-4035903993dfee62cca2142c0ca51ab57075909ddee2817d0620daa97ac4d4ce) | +| `data` | required | [`UserMessage`](#persistence-type-sha256-613660455704fb82a825b5b766e000aa64b5b4d44b4f10e55cffe968d4ec6ab1) | | `ignorable` | optional | `true` | | `seq` | required | `number` | | `sourceEventSeqs` | optional | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index 46eded7418..5e045309fd 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -25,7 +25,7 @@ | `JsonlHeaderLine` | header | `18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f` | [`HeaderLine`](#persistence-type-sha256-18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f) | | `SessionEventEnvelope` | envelope | `5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc` | [`union (3 variants)`](#persistence-type-sha256-5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc) | | `event:agent-preset/selected` | event | `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b` | [`{ type: "agent-preset/selected" }`](#persistence-type-sha256-a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b) | -| `event:agent/inbox/spliced` | event | `cdf988172b589b230f2bce868489def1bb575eb0501076ac757a8f369ac5c64e` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-cdf988172b589b230f2bce868489def1bb575eb0501076ac757a8f369ac5c64e) | +| `event:agent/inbox/spliced` | event | `ddce54a3873dc57113b0cd38ad9b23b3c259186139e8847e5e5f415d86f52482` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-ddce54a3873dc57113b0cd38ad9b23b3c259186139e8847e5e5f415d86f52482) | | `event:approval/asked` | event | `3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1` | [`{ type: "approval/asked" }`](#persistence-type-sha256-3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1) | | `event:approval/decided` | event | `bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3` | [`{ type: "approval/decided" }`](#persistence-type-sha256-bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3) | | `event:approval/policy` | event | `26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78` | [`{ type: "approval/policy" }`](#persistence-type-sha256-26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78) | @@ -38,7 +38,7 @@ | `event:compaction/start` | event | `db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965` | [`{ type: "compaction/start" }`](#persistence-type-sha256-db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965) | | `event:compaction/summary` | event | `e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602` | [`{ type: "compaction/summary" }`](#persistence-type-sha256-e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602) | | `event:deliverables/presented` | event | `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176` | [`{ type: "deliverables/presented" }`](#persistence-type-sha256-13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176) | -| `event:developer/message` | event | `70d2acdec1ebcfea56d02f7233ebb9ec0f7f494447fb952884c22c3afdc01d6f` | [`{ type: "developer/message" }`](#persistence-type-sha256-70d2acdec1ebcfea56d02f7233ebb9ec0f7f494447fb952884c22c3afdc01d6f) | +| `event:developer/message` | event | `70ea8e7943c5e2d76905bcc2e694e8c3d5b138aaae3609f3e261b8d369a6e35a` | [`{ type: "developer/message" }`](#persistence-type-sha256-70ea8e7943c5e2d76905bcc2e694e8c3d5b138aaae3609f3e261b8d369a6e35a) | | `event:feedback/message-delete` | event | `3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796` | [`{ type: "feedback/message-delete" }`](#persistence-type-sha256-3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796) | | `event:feedback/message-put` | event | `b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf` | [`{ type: "feedback/message-put" }`](#persistence-type-sha256-b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf) | | `event:feedback/record` | event | `b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8` | [`{ type: "feedback/record" }`](#persistence-type-sha256-b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8) | @@ -60,7 +60,7 @@ | `event:session-log-deepseek/delivery-accepted` | event | `d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9` | [`{ type: "session-log-deepseek/delivery-accepted" }`](#persistence-type-sha256-d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9) | | `event:session/end-seed` | event | `5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e` | [`{ type: "session/end-seed" }`](#persistence-type-sha256-5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e) | | `event:session/title` | event | `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | [`{ type: "session/title" }`](#persistence-type-sha256-1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5) | -| `event:session/title-llm-request` | event | `2e505098f9209e52eebd577f1a6f9d4e773ec4d78987aff28a7c786442b9f54c` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-2e505098f9209e52eebd577f1a6f9d4e773ec4d78987aff28a7c786442b9f54c) | +| `event:session/title-llm-request` | event | `d95c624eddecdacf1ee74356196c09d2f3fcd625714be5e852a6b643fa12a7ca` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-d95c624eddecdacf1ee74356196c09d2f3fcd625714be5e852a6b643fa12a7ca) | | `event:step/end` | event | `e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05` | [`{ type: "step/end" }`](#persistence-type-sha256-e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05) | | `event:step/start` | event | `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | [`{ type: "step/start" }`](#persistence-type-sha256-4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986) | | `event:subagent/catalog` | event | `53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac) | @@ -70,6 +70,7 @@ | `event:team/member` | event | `4fb59762612c3e3ac3a3bd4f84c9c148d3c3893bd422ba2b201cc039fabd49bc` | [`{ type: "team/member" }`](#persistence-type-sha256-4fb59762612c3e3ac3a3bd4f84c9c148d3c3893bd422ba2b201cc039fabd49bc) | | `event:team/message/delivered` | event | `48f9c19417a1abbedfa59f4667bba36b93ac2db407adf5e84cb3ba0de30942cb` | [`{ type: "team/message/delivered" }`](#persistence-type-sha256-48f9c19417a1abbedfa59f4667bba36b93ac2db407adf5e84cb3ba0de30942cb) | | `event:team/message/queued` | event | `21fb6a90d5068f6a0003b7ab316ed2f56342477146a65c00db0f13c4d8df667d` | [`{ type: "team/message/queued" }`](#persistence-type-sha256-21fb6a90d5068f6a0003b7ab316ed2f56342477146a65c00db0f13c4d8df667d) | +| `event:team/mode` | event | `920d87e22b7409bcc7224f8b1dc7f5101c33574c75627c65baab12b85cc96e75` | [`{ type: "team/mode" }`](#persistence-type-sha256-920d87e22b7409bcc7224f8b1dc7f5101c33574c75627c65baab12b85cc96e75) | | `event:team/task` | event | `d595ec73b32b016a6055333c67a5d646032b22e672da1d4a09c5b1ae398a093a` | [`{ type: "team/task" }`](#persistence-type-sha256-d595ec73b32b016a6055333c67a5d646032b22e672da1d4a09c5b1ae398a093a) | | `event:todo/write` | event | `b978cff734e62143eb56c9125423ec275405eda969802d42aaf73ecb987d3b26` | [`{ type: "todo/write" }`](#persistence-type-sha256-b978cff734e62143eb56c9125423ec275405eda969802d42aaf73ecb987d3b26) | | `event:tool-workflow/agent-end` | event | `babf9ee4d1af62bf6c3a8103737f7a5e4e78ce179be835ce05a38803e15884b7` | [`{ type: "tool-workflow/agent-end" }`](#persistence-type-sha256-babf9ee4d1af62bf6c3a8103737f7a5e4e78ce179be835ce05a38803e15884b7) | @@ -82,7 +83,7 @@ | `event:tool/result` | event | `7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397` | [`{ type: "tool/result" }`](#persistence-type-sha256-7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397) | | `event:turn/end` | event | `0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce` | [`{ type: "turn/end" }`](#persistence-type-sha256-0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce) | | `event:turn/start` | event | `aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8` | [`{ type: "turn/start" }`](#persistence-type-sha256-aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8) | -| `event:user/message` | event | `5bb231bc5afdb532a6898a20604e76bf0c1db438bfcdc17d4847dceaddc02451` | [`{ type: "user/message" }`](#persistence-type-sha256-5bb231bc5afdb532a6898a20604e76bf0c1db438bfcdc17d4847dceaddc02451) | +| `event:user/message` | event | `dcb07e04496586d2664d72dab0e94312917da56927b2a0dc3ee53d6a33e32450` | [`{ type: "user/message" }`](#persistence-type-sha256-dcb07e04496586d2664d72dab0e94312917da56927b2a0dc3ee53d6a33e32450) | | `event:web/deepseek-search-llm-request` | event | `0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52` | [`{ type: "web/deepseek-search-llm-request" }`](#persistence-type-sha256-0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52) | | `event:working-directory/change` | event | `124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3` | [`{ type: "working-directory/change" }`](#persistence-type-sha256-124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3) | | `event:workspace/changes` | event | `e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72` | [`{ type: "workspace/changes" }`](#persistence-type-sha256-e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72) | @@ -200,15 +201,14 @@ export type SessionEvent = { * The session's agent preset was chosen after creation, while the session * was still blank. Log-only: it records the composition later turns ran * under, so a resumed or forked session rebuilds the same one instead of - * the header's creation-time value. In a child Session, an occurrence - * before its first own `subagent/descriptor` marks an independent spawn - * selection that cold resume restores instead of joining the parent's - * composition (`dsh-subagent` README, "Restoring child composition"). + * the header's creation-time value. Subagent restoration reads this saved + * selection independently of its creator (`dsh-subagent` README, + * "Restoring child composition"). */ 'agent-preset/selected': { agentPreset: string } ``` -来源:[`packages/preset/agent-preset-registry/src/session.ts:31`](../packages/preset/agent-preset-registry/src/session.ts) +来源:[`packages/preset/agent-preset-registry/src/session.ts:30`](../packages/preset/agent-preset-registry/src/session.ts) ### `approval/*` @@ -1011,13 +1011,13 @@ export type SessionEvent = { #### `team/member` — log-only ```ts persistence-catalog -/** Whole teammate lifecycle value, stored only in the Team Lead Session. */ +/** Historical teammate lifecycle value; new teammates are continuable entries in the Lead's subagent catalog. */ 'team/member': { version: 2; teamId: TeamId; member: TeamMemberSnapshot } ``` 类型:[TeamId](subsystems/agent-team.zh.md) · [TeamMemberSnapshot](subsystems/agent-team.zh.md) -来源:[`packages/experimental/agent-team/src/types.ts:230`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/team/agent-team/src/types.ts:173`](../packages/team/agent-team/src/types.ts) @@ -1035,7 +1035,7 @@ export type SessionEvent = { 类型:[TeamId](subsystems/agent-team.zh.md) · [TeamMessageId](subsystems/agent-team.zh.md) -来源:[`packages/experimental/agent-team/src/types.ts:236`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/team/agent-team/src/types.ts:179`](../packages/team/agent-team/src/types.ts) @@ -1048,7 +1048,18 @@ export type SessionEvent = { 类型:[TeamId](subsystems/agent-team.zh.md) · [TeamMessageSnapshot](subsystems/agent-team.zh.md) -来源:[`packages/experimental/agent-team/src/types.ts:234`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/team/agent-team/src/types.ts:177`](../packages/team/agent-team/src/types.ts) + + + +#### `team/mode` — log-only + +```ts persistence-catalog +/** User-selected Team mode; child Agents use the selection of their runtime root. */ +'team/mode': { enabled: boolean } +``` + +来源:[`packages/team/agent-team/src/types.ts:171`](../packages/team/agent-team/src/types.ts) @@ -1061,7 +1072,7 @@ export type SessionEvent = { 类型:[TeamId](subsystems/agent-team.zh.md) · [TeamTaskSnapshot](subsystems/agent-team.zh.md) -来源:[`packages/experimental/agent-team/src/types.ts:232`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/team/agent-team/src/types.ts:175`](../packages/team/agent-team/src/types.ts) ### `todo/*` @@ -2357,6 +2368,14 @@ SHA-256: `383cf73ae4748e5beb281ae74857ba1ffb92ae01075b9fbf51af78b666007a8f` `"recall"` + + +### `"refusal"` + +SHA-256: `4a561c111a78d4520e850c93795369ea169ff86abd082aee86c8fd38121a65aa` + +`"refusal"` + ### `"rejected"` @@ -2709,6 +2728,14 @@ SHA-256: `4e011753f8263f436efaabfbb3b4db53a98fc55342c5ded2daf7208b217be159` `"team/message/queued"` + + +### `"team/mode"` + +SHA-256: `d8a476d2c6fd30df6ec0e799f91716d1518c780d23b4121dce545abfcfb7c5a5` + +`"team/mode"` + ### `"team/task"` @@ -3385,7 +3412,7 @@ SHA-256: `abd6ca62a5ab166553848324a23fef90eed7f59c8021bd63e7ea5bc6fd666b0c` | `body` | 必需 | [`{ max_tokens, messages, model, thinking?, … }`](#persistence-type-sha256-9c001ecb5e1d3c3894410db2a1e383842e5f8454fb52182338686158abc1b3ee) | | `endpoint` | 必需 | `string` | - + @@ -3393,7 +3420,7 @@ SHA-256: `abd6ca62a5ab166553848324a23fef90eed7f59c8021bd63e7ea5bc6fd666b0c` ### `DeveloperMessage` -SHA-256: `64ef542b1bfc988382d337f2ea85aab8ad76ea55341cf082163919d315688428` +SHA-256: `01c161379a3c3394a642807843e108ef911a8b7dbffd6d7de43f4484c88b0796` 来源:[`packages/llm/llm/src/message.ts:157`](../packages/llm/llm/src/message.ts) @@ -3402,7 +3429,7 @@ SHA-256: `64ef542b1bfc988382d337f2ea85aab8ad76ea55341cf082163919d315688428` | `content` | 必需 | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) | | `id` | 必需 | `string` | | `role` | 必需 | `"developer"` | -| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-7e9f1072c17f7aa86547c9c8b02766c1006d866a1859a3d677a336e932f70d1a) | +| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-b120362fd216a16a40fe787f6d3286086aa131266322bab79c026a130ccd0f72) | 消息源兼容策略:`source` — `session-source-attribution` v1; `session.developer-message.source`; `kind`; `preserve`. @@ -4101,7 +4128,7 @@ SHA-256: `6f72ece8f317cd080c4f20a93f1bf709965f5b81ac4669528f5a023191cfcf20` | `step` | 必需 | `number` | | `turn` | 必需 | `number` | - + @@ -4109,15 +4136,15 @@ SHA-256: `6f72ece8f317cd080c4f20a93f1bf709965f5b81ac4669528f5a023191cfcf20` ### `Message` -SHA-256: `c78b9097bfae83cd08f0a98a92730236ba80ac61d712046959e4fc7f28aa1f36` +SHA-256: `e272493e3ea0b33b2e1d64b6a80e9d8bd3d1d466cc115f2facbdd598f8644a39` 来源:[`packages/llm/llm/src/message.ts:196`](../packages/llm/llm/src/message.ts) 以下类型之一: - [`ToolResultMessage`](#persistence-type-sha256-4b259f7f8f0a44ad5e90b70a73553ced1df27012d126921d397ff403b6c86731) -- [`DeveloperMessage`](#persistence-type-sha256-64ef542b1bfc988382d337f2ea85aab8ad76ea55341cf082163919d315688428) -- [`UserMessage`](#persistence-type-sha256-4035903993dfee62cca2142c0ca51ab57075909ddee2817d0620daa97ac4d4ce) +- [`DeveloperMessage`](#persistence-type-sha256-01c161379a3c3394a642807843e108ef911a8b7dbffd6d7de43f4484c88b0796) +- [`UserMessage`](#persistence-type-sha256-613660455704fb82a825b5b766e000aa64b5b4d44b4f10e55cffe968d4ec6ab1) - [`SystemMessage`](#persistence-type-sha256-09f3f8dc417f278257202714132f98ba7b52957d7b558c4a04456603890eb363) - [`AssistantMessage`](#persistence-type-sha256-fa87537f4a9c1e25602990a8bf686b383e9e00352ec8946e16ae24ba7c1625be) @@ -4194,7 +4221,7 @@ SHA-256: `390eed1028aba02774f14408647acec10ec5516e97d4f15ecde2b9989d5a69f2` - `"negative"` - `"positive"` - + @@ -4202,7 +4229,7 @@ SHA-256: `390eed1028aba02774f14408647acec10ec5516e97d4f15ecde2b9989d5a69f2` ### `MessageSource` -SHA-256: `7e9f1072c17f7aa86547c9c8b02766c1006d866a1859a3d677a336e932f70d1a` +SHA-256: `b120362fd216a16a40fe787f6d3286086aa131266322bab79c026a130ccd0f72` 来源:[`packages/llm/llm/src/message.ts:136`](../packages/llm/llm/src/message.ts) @@ -4281,7 +4308,7 @@ SHA-256: `7e9f1072c17f7aa86547c9c8b02766c1006d866a1859a3d677a336e932f70d1a` | `session-reference` | `"recall"` | `references`: [`{ capturedFormatVersion?, capturedThroughSeq, compacted, inputIndex, … }[]`](#persistence-type-sha256-df843efd87c78cf8de98f8e0d0216956cd568d0d0e56a22c853663e274aa1a18); `version`: `1` | [`SessionReferenceSource`](#persistence-type-sha256-f56c5b59ca0161310c338d31dd16cf14287ccd602ff4181058df178c1c286d7f) | | `skill-catalog` | `"catalog"` | `entries`: [`{ description, name }[]`](#persistence-type-sha256-aba175fb20ffc71112c55efe70458ff5d7dbfb83501892b4ba8d077f0ea5d7ba) | [`SkillCatalogSource`](#persistence-type-sha256-65d583e66518c5236015e0846470fb32efcf92375ed306aef14e50a9d2cd2099) | | `skill-invocation` | `"instructions"` | `name`: `string` | [`SkillInvocationSource`](#persistence-type-sha256-672c4aa2a9b736e3d6e5da6346d2e2ed5e3573f2d0baafa18e7407c9f3fee3db) | -| `subagent-settled` | `"notice"` | `senderSessionId`: `string`; `summary`: `string` | [`SubagentSettledMessageSource`](#persistence-type-sha256-4e97c3d85c0fc817ee58873c38c25a2474abaaa0af4839311b50b68db3b8cf1a) | +| `subagent-settled` | `"notice"` | `senderSessionId`: `string`; `summary`: `string` | [`SubagentSettledMessageSource`](#persistence-type-sha256-848639f8787f035b480ec53cb5686850897c5e8006d87ca231cd8a9ee8634936) | | `system-prompt` | 未声明 | 无 | [`SystemPromptMessageSource`](#persistence-type-sha256-65a8400e451e667e5b6e34df72ceb81fca8c12330330fa72083ad40279b189ad) | | `team-message` | 未声明 | `messageId`: `string`; `senderId`: `string`; `senderName`: `string`; `teamId`: `string` | [`TeamMessageSource`](#persistence-type-sha256-2152caf1a71e588e4df2358a806dea24936c1c47131134f01a12939d12cdc0c0) | | `time-context` | `"catalog"` | 无 | [`{ kind: "time-context", form: "catalog" }`](#persistence-type-sha256-85fe96bf15d5c7559eec3bae4d6cf1a76fda9f5be05a29c2830cff560cab0c0d) | @@ -4327,13 +4354,13 @@ SHA-256: `7e9f1072c17f7aa86547c9c8b02766c1006d866a1859a3d677a336e932f70d1a` | `webhook` | `"notice"` | `deliveryId`: `string`; `provider`: `string`; `ruleId`: `string`; `source`: `string`; `summary`: `string` | [`{ kind: "webhook", form: "notice" }`](#persistence-type-sha256-9edc162949abf93bd5ff049191b345454ba104f583937ce5580fb50f067d2b94) | | `working-directory` | 未声明 | 无 | [`{ kind: "working-directory" }`](#persistence-type-sha256-2235d957bf4b7c6cf959231d480f0ba1f3a6d72d82981493fb6c7c963021d3d5) | - + ### `Message[]` -SHA-256: `ff2cbb53582d1827b02622482246979942f59e07e0393b76a161251423549e72` +SHA-256: `95b5cf90ea1f57da3319184d6f5f3811d0f651aa9113591c10e81c3d1e9b9f70` -[`Message`](#persistence-type-sha256-c78b9097bfae83cd08f0a98a92730236ba80ac61d712046959e4fc7f28aa1f36) 的数组。 +[`Message`](#persistence-type-sha256-e272493e3ea0b33b2e1d64b6a80e9d8bd3d1d466cc115f2facbdd598f8644a39) 的数组。 @@ -4701,7 +4728,7 @@ SHA-256: `b56f6a885da3dc394c69a3e3cc5c0cfc4da15601b63fda4989eee8f40efc5be4` | `source` | 必需 | [`SessionTitleSource`](#persistence-type-sha256-30d5a537b296685770b1241c4b4bbde19d5a31ff81aa23df73fab5e90b8fc65f) | | `title` | 必需 | `string` | - + @@ -4709,7 +4736,7 @@ SHA-256: `b56f6a885da3dc394c69a3e3cc5c0cfc4da15601b63fda4989eee8f40efc5be4` ### `SessionTitleLlmRequestEventData` -SHA-256: `7ff62f6599c083ceb5a0d7a8940ff85ba80dc1d7ddd6ba9339903e93f74cba54` +SHA-256: `601327bedca5f98a0fa4c27133bd35749f3af2baaaacf53badf074e59cea5d25` 来源:[`packages/session/session-title-llm/src/index.ts:43`](../packages/session/session-title-llm/src/index.ts) @@ -4717,7 +4744,7 @@ SHA-256: `7ff62f6599c083ceb5a0d7a8940ff85ba80dc1d7ddd6ba9339903e93f74cba54` |---|---|---| | `maxTokens` | 必需 | `number` | | `messageSeqs` | 必需 | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | -| `messages` | 必需 | [`Message[]`](#persistence-type-sha256-ff2cbb53582d1827b02622482246979942f59e07e0393b76a161251423549e72) | +| `messages` | 必需 | [`Message[]`](#persistence-type-sha256-95b5cf90ea1f57da3319184d6f5f3811d0f651aa9113591c10e81c3d1e9b9f70) | | `reasoningEffort` | 可选 | `string` | | `route` | 必需 | [`SessionTitleModelIdentity`](#persistence-type-sha256-07e1a58c58b593f507b8e7f25723bb0aa42baae6e00f5ecfddf28cb1e64d3235) | | `system` | 必需 | `string` | @@ -4886,7 +4913,7 @@ SHA-256: `86d1d0e9cfebb240421187e3dc37fb71cc38a1501885cf32f8f9adef7b0a2bde` - [`ContinuableSubagentDescriptorData`](#persistence-type-sha256-6487b10ba7542f3c447b1e41a1c6a590b7bfafe0b9b66b49f34028da85bb970f) - [`OneShotSubagentDescriptorData`](#persistence-type-sha256-5f758f3de5f00c18968a7c6d347083aac5c9ae13f6a79c78f6611b23c4973564) - + @@ -4894,7 +4921,7 @@ SHA-256: `86d1d0e9cfebb240421187e3dc37fb71cc38a1501885cf32f8f9adef7b0a2bde` ### `SubagentSettledMessageSource` -SHA-256: `4e97c3d85c0fc817ee58873c38c25a2474abaaa0af4839311b50b68db3b8cf1a` +SHA-256: `848639f8787f035b480ec53cb5686850897c5e8006d87ca231cd8a9ee8634936` 来源:[`packages/subagent/subagent/src/continuation-messages.ts:29`](../packages/subagent/subagent/src/continuation-messages.ts) @@ -4903,6 +4930,7 @@ SHA-256: `4e97c3d85c0fc817ee58873c38c25a2474abaaa0af4839311b50b68db3b8cf1a` | `form` | 必需 | `"notice"` | | `kind` | 必需 | `"subagent-settled"` | | `senderSessionId` | 必需 | `string` | +| `stopReason` | 可选 | [`union (5 variants)`](#persistence-type-sha256-866f7b2dbdc5b421eab3e629312d1c839b75c259633b4b43b50d23bac44d92a0) | | `summary` | 必需 | `string` | @@ -4959,7 +4987,7 @@ SHA-256: `65a8400e451e667e5b6e34df72ceb81fca8c12330330fa72083ad40279b189ad` - + @@ -4967,7 +4995,7 @@ SHA-256: `65a8400e451e667e5b6e34df72ceb81fca8c12330330fa72083ad40279b189ad` SHA-256: `dfce5db4aa42b7ebe0bb79eca92553587287e18c06b9eba85cbfb9d332e26acd` -来源:[`packages/experimental/agent-team/src/types.ts:45`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/team/agent-team/src/types.ts:47`](../packages/team/agent-team/src/types.ts) 以下类型之一: @@ -4977,7 +5005,7 @@ SHA-256: `dfce5db4aa42b7ebe0bb79eca92553587287e18c06b9eba85cbfb9d332e26acd` - + @@ -4985,7 +5013,7 @@ SHA-256: `dfce5db4aa42b7ebe0bb79eca92553587287e18c06b9eba85cbfb9d332e26acd` SHA-256: `6598cd6da253948d6b207c6374a45335804c79b576866d6215649c2d1bc85e25` -来源:[`packages/experimental/agent-team/src/types.ts:48`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/team/agent-team/src/types.ts:50`](../packages/team/agent-team/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -4999,7 +5027,7 @@ SHA-256: `6598cd6da253948d6b207c6374a45335804c79b576866d6215649c2d1bc85e25` - + @@ -5007,7 +5035,7 @@ SHA-256: `6598cd6da253948d6b207c6374a45335804c79b576866d6215649c2d1bc85e25` SHA-256: `2eafefdc6f79a4922d68a787a09eaf4dd2b2369c3777fba4c05e800b5678ac8e` -来源:[`packages/experimental/agent-team/src/types.ts:129`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/team/agent-team/src/types.ts:108`](../packages/team/agent-team/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -5019,7 +5047,7 @@ SHA-256: `2eafefdc6f79a4922d68a787a09eaf4dd2b2369c3777fba4c05e800b5678ac8e` - + @@ -5027,7 +5055,7 @@ SHA-256: `2eafefdc6f79a4922d68a787a09eaf4dd2b2369c3777fba4c05e800b5678ac8e` SHA-256: `2152caf1a71e588e4df2358a806dea24936c1c47131134f01a12939d12cdc0c0` -来源:[`packages/experimental/agent-team/src/types.ts:138`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/team/agent-team/src/types.ts:117`](../packages/team/agent-team/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -5039,7 +5067,7 @@ SHA-256: `2152caf1a71e588e4df2358a806dea24936c1c47131134f01a12939d12cdc0c0` - + @@ -5047,7 +5075,7 @@ SHA-256: `2152caf1a71e588e4df2358a806dea24936c1c47131134f01a12939d12cdc0c0` SHA-256: `51edec100659da3a9bd9c3f6ba1180f5e64ec9a1585e5057080df45c180bd357` -来源:[`packages/experimental/agent-team/src/types.ts:75`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/team/agent-team/src/types.ts:64`](../packages/team/agent-team/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -5062,7 +5090,7 @@ SHA-256: `51edec100659da3a9bd9c3f6ba1180f5e64ec9a1585e5057080df45c180bd357` - + @@ -5070,7 +5098,7 @@ SHA-256: `51edec100659da3a9bd9c3f6ba1180f5e64ec9a1585e5057080df45c180bd357` SHA-256: `b2d2a01e51341a3bfec809198952c8c830a7899f4cb6ecd3c1dc0b5d9a2af943` -来源:[`packages/experimental/agent-team/src/types.ts:72`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/team/agent-team/src/types.ts:61`](../packages/team/agent-team/src/types.ts) 以下类型之一: @@ -5392,7 +5420,7 @@ SHA-256: `f1687f982fa3d41ef29e159feed154ecd13af4dd209acaee16038f223f2e9402` | `interrupted` | 未声明 | 无 | [`{ kind: "interrupted" }`](#persistence-type-sha256-56b7c047694875a843ba020dfea3c8380d54ae4ff88ab349e72004dc21e07c23) | | `max-tokens` | 未声明 | 无 | [`{ kind: "max-tokens" }`](#persistence-type-sha256-ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7) | - + @@ -5400,7 +5428,7 @@ SHA-256: `f1687f982fa3d41ef29e159feed154ecd13af4dd209acaee16038f223f2e9402` ### `UserMessage` -SHA-256: `4035903993dfee62cca2142c0ca51ab57075909ddee2817d0620daa97ac4d4ce` +SHA-256: `613660455704fb82a825b5b766e000aa64b5b4d44b4f10e55cffe968d4ec6ab1` 来源:[`packages/llm/llm/src/message.ts:162`](../packages/llm/llm/src/message.ts) @@ -5409,19 +5437,19 @@ SHA-256: `4035903993dfee62cca2142c0ca51ab57075909ddee2817d0620daa97ac4d4ce` | `content` | 必需 | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) | | `id` | 必需 | `string` | | `role` | 必需 | `"user"` | -| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-7e9f1072c17f7aa86547c9c8b02766c1006d866a1859a3d677a336e932f70d1a) | +| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-b120362fd216a16a40fe787f6d3286086aa131266322bab79c026a130ccd0f72) | 消息源兼容策略:`source` — `session-source-attribution` v1; `session.user-message.source`; `kind`; `preserve`. 仅表示归属的新增 kind:`action-cards`, `tmux-context`, `user-question-reply`, `working-directory`. - + ### `UserMessage[]` -SHA-256: `7c203f3d4dfa6638ff2601247cc7ca5d46731d2c614aa61217475b9eb1fbd0b3` +SHA-256: `690583582246babf6120f16eefd849dd760c5ec84619014951af403a1b3032c0` -[`UserMessage`](#persistence-type-sha256-4035903993dfee62cca2142c0ca51ab57075909ddee2817d0620daa97ac4d4ce) 的数组。 +[`UserMessage`](#persistence-type-sha256-613660455704fb82a825b5b766e000aa64b5b4d44b4f10e55cffe968d4ec6ab1) 的数组。 @@ -5539,12 +5567,6 @@ SHA-256: `5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78` - - - - - - @@ -5577,6 +5599,12 @@ SHA-256: `5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78` + + + + + + @@ -5613,7 +5641,7 @@ SHA-256: `5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78` SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b` -来源:[`packages/api/session-controller/src/types.ts:388`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:21`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:33`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:9`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts) +来源:[`packages/api/session-controller/src/types.ts:388`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/team/agent-team/src/types.ts:20`](../packages/team/agent-team/src/types.ts) · [`packages/team/agent-team/src/types.ts:32`](../packages/team/agent-team/src/types.ts) · [`packages/team/agent-team/src/types.ts:8`](../packages/team/agent-team/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts) `string` @@ -5757,6 +5785,20 @@ SHA-256: `fd0d91a1f9dd2efb8959bdf5e07f8f0a98b1cdfa38ce8d4732ebde0b44c3abd4` - `"in_progress"` - `"pending"` + + +### `union (5 variants)` + +SHA-256: `866f7b2dbdc5b421eab3e629312d1c839b75c259633b4b43b50d23bac44d92a0` + +以下类型之一: + +- `"aborted"` +- `"completed"` +- `"error"` +- `"max-tokens"` +- `"refusal"` + ### `union (6 variants)` @@ -5822,7 +5864,7 @@ SHA-256: `20a57544bbf204dacd1ae6beb2ab29444365c3c869f2afdf489dcdcbc3d9c8c5` SHA-256: `6e52dd55a1d48954ed5464ec46fe34d9eb65f709baabc9561e7a279910b0c406` -来源:[`packages/preset/agent-preset-registry/src/session.ts:31`](../packages/preset/agent-preset-registry/src/session.ts) +来源:[`packages/preset/agent-preset-registry/src/session.ts:30`](../packages/preset/agent-preset-registry/src/session.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -6236,6 +6278,18 @@ SHA-256: `0a92c5bdfa06c2e2615b777728f3aa95f408d43b9012e7b754568311251db623` | `subjectId` | 必需 | `string` | | `tenantId` | 必需 | `string` | + + +### `{ enabled }` + +SHA-256: `c2a13242efc3bea4b36a843ddf1524ff4d33c765a71b582fa1fdbfa4fa148434` + +来源:[`packages/team/agent-team/src/types.ts:171`](../packages/team/agent-team/src/types.ts) + +| 属性 | 存在性 | 类型 | +|---|---|---| +| `enabled` | 必需 | `boolean` | + ### `{ end, start }` @@ -6332,18 +6386,18 @@ SHA-256: `ad0c38a0adb82a94c925456df41586d9b5f9202fca18593eae2213d4445b4585` | `reason` | 必需 | [`RequestHeaderReason`](#persistence-type-sha256-530da6c0267813bb4511af72883cc518834e624e3b3220a7062955fd57a788a6) | | `startsSeries` | 可选 | `true` | - + ### `{ headerSeq?, message, step, turn }` -SHA-256: `53d4f8256bc58109468ea358ac54d437760cd765012360cafe72a401e6a41d4a` +SHA-256: `65377a76c6a58f785d95b89f89f2e13d289cb88a70ea74939d0ec08f429f0a6e` 来源:[`packages/core/session/src/types.ts:317`](../packages/core/session/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| | `headerSeq` | 可选 | `number` | -| `message` | 必需 | [`DeveloperMessage`](#persistence-type-sha256-64ef542b1bfc988382d337f2ea85aab8ad76ea55341cf082163919d315688428) | +| `message` | 必需 | [`DeveloperMessage`](#persistence-type-sha256-01c161379a3c3394a642807843e108ef911a8b7dbffd6d7de43f4484c88b0796) | | `step` | 必需 | `number` | | `turn` | 必需 | `number` | @@ -6427,17 +6481,17 @@ SHA-256: `17d1afb770d9941936130996da00dc86782cfef731d8d6526162c301256a4ac3` |---|---|---| | `inherited` | 可选 | `true` | - + ### `{ inserted, outcome?, removedCount?, start, … }` -SHA-256: `d4d960185026266ed7bd127be1e252234689e8b4890c7c2677a644ba58f35401` +SHA-256: `e1880c67e735e228f3555df8a55a492db2c0b0c71bc8859e938e184a46c7db51` 来源:[`packages/core/agent/src/types.ts:110`](../packages/core/agent/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| -| `inserted` | 必需 | [`UserMessage[]`](#persistence-type-sha256-7c203f3d4dfa6638ff2601247cc7ca5d46731d2c614aa61217475b9eb1fbd0b3) | +| `inserted` | 必需 | [`UserMessage[]`](#persistence-type-sha256-690583582246babf6120f16eefd849dd760c5ec84619014951af403a1b3032c0) | | `outcome` | 可选 | `"canceled"` | | `removedCount` | 可选 | `number` | | `start` | 必需 | `number` | @@ -7860,7 +7914,7 @@ SHA-256: `9c001ecb5e1d3c3894410db2a1e383842e5f8454fb52182338686158abc1b3ee` SHA-256: `b1ebd5cda47d90d980bb05bffc3f970c51537b9aff15b3bc3d639811727741b1` -来源:[`packages/experimental/agent-team/src/types.ts:230`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/team/agent-team/src/types.ts:173`](../packages/team/agent-team/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -7888,7 +7942,7 @@ SHA-256: `6bc8e51124d0874bc89e1d0ceff381a9c3cd0fc240667d69f841ce4f60f65fe5` SHA-256: `ea08ec1df0c33108fd03345b9af199cf04dea450aa045d3c683f216311403403` -来源:[`packages/experimental/agent-team/src/types.ts:234`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/team/agent-team/src/types.ts:177`](../packages/team/agent-team/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -7902,7 +7956,7 @@ SHA-256: `ea08ec1df0c33108fd03345b9af199cf04dea450aa045d3c683f216311403403` SHA-256: `0bcfaf5566d271028429ac7b562cb59053c9d78b8ae14708c35fac835108d1b1` -来源:[`packages/experimental/agent-team/src/types.ts:236`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/team/agent-team/src/types.ts:179`](../packages/team/agent-team/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -8048,7 +8102,7 @@ SHA-256: `477614bca595a9e7adb5a1870be446b9a8684f3ba8950259d6289f2d3b74f028` SHA-256: `a40d12070f6f4a124f32fb3cc86e7857554702e32f0eda3080a25c4b9ac9b18c` -来源:[`packages/experimental/agent-team/src/types.ts:232`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/team/agent-team/src/types.ts:175`](../packages/team/agent-team/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -8096,17 +8150,17 @@ SHA-256: `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b` | `time` | 必需 | `number` | | `type` | 必需 | `"agent-preset/selected"` | - + ### `{ type: "agent/inbox/spliced" }` -SHA-256: `cdf988172b589b230f2bce868489def1bb575eb0501076ac757a8f369ac5c64e` +SHA-256: `ddce54a3873dc57113b0cd38ad9b23b3c259186139e8847e5e5f415d86f52482` | 属性 | 存在性 | 类型 | |---|---|---| -| `data` | 必需 | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-d4d960185026266ed7bd127be1e252234689e8b4890c7c2677a644ba58f35401) | +| `data` | 必需 | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-e1880c67e735e228f3555df8a55a492db2c0b0c71bc8859e938e184a46c7db51) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | | `time` | 必需 | `number` | @@ -8347,17 +8401,17 @@ SHA-256: `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176` | `time` | 必需 | `number` | | `type` | 必需 | `"deliverables/presented"` | - + ### `{ type: "developer/message" }` -SHA-256: `70d2acdec1ebcfea56d02f7233ebb9ec0f7f494447fb952884c22c3afdc01d6f` +SHA-256: `70ea8e7943c5e2d76905bcc2e694e8c3d5b138aaae3609f3e261b8d369a6e35a` | 属性 | 存在性 | 类型 | |---|---|---| -| `data` | 必需 | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-53d4f8256bc58109468ea358ac54d437760cd765012360cafe72a401e6a41d4a) | +| `data` | 必需 | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-65377a76c6a58f785d95b89f89f2e13d289cb88a70ea74939d0ec08f429f0a6e) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | | `sourceEventSeqs` | 可选 | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | @@ -8757,17 +8811,17 @@ SHA-256: `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | `time` | 必需 | `number` | | `type` | 必需 | `"session/title"` | - + ### `{ type: "session/title-llm-request" }` -SHA-256: `2e505098f9209e52eebd577f1a6f9d4e773ec4d78987aff28a7c786442b9f54c` +SHA-256: `d95c624eddecdacf1ee74356196c09d2f3fcd625714be5e852a6b643fa12a7ca` | 属性 | 存在性 | 类型 | |---|---|---| -| `data` | 必需 | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-7ff62f6599c083ceb5a0d7a8940ff85ba80dc1d7ddd6ba9339903e93f74cba54) | +| `data` | 必需 | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-601327bedca5f98a0fa4c27133bd35749f3af2baaaacf53badf074e59cea5d25) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | | `time` | 必需 | `number` | @@ -8919,6 +8973,22 @@ SHA-256: `21fb6a90d5068f6a0003b7ab316ed2f56342477146a65c00db0f13c4d8df667d` | `time` | 必需 | `number` | | `type` | 必需 | `"team/message/queued"` | + + + + +### `{ type: "team/mode" }` + +SHA-256: `920d87e22b7409bcc7224f8b1dc7f5101c33574c75627c65baab12b85cc96e75` + +| 属性 | 存在性 | 类型 | +|---|---|---| +| `data` | 必需 | [`{ enabled }`](#persistence-type-sha256-c2a13242efc3bea4b36a843ddf1524ff4d33c765a71b582fa1fdbfa4fa148434) | +| `ignorable` | 可选 | `true` | +| `seq` | 必需 | `number` | +| `time` | 必需 | `number` | +| `type` | 必需 | `"team/mode"` | + @@ -9190,17 +9260,17 @@ SHA-256: `f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515` | `type` | 必需 | `"usage"` | | `usage` | 必需 | [`TokenUsage`](#persistence-type-sha256-fec5442b9ee4122e10514d81d5a43648003fe8bfb7ee2ba50c0cdbdd383ed394) | - + ### `{ type: "user/message" }` -SHA-256: `5bb231bc5afdb532a6898a20604e76bf0c1db438bfcdc17d4847dceaddc02451` +SHA-256: `dcb07e04496586d2664d72dab0e94312917da56927b2a0dc3ee53d6a33e32450` | 属性 | 存在性 | 类型 | |---|---|---| -| `data` | 必需 | [`UserMessage`](#persistence-type-sha256-4035903993dfee62cca2142c0ca51ab57075909ddee2817d0620daa97ac4d4ce) | +| `data` | 必需 | [`UserMessage`](#persistence-type-sha256-613660455704fb82a825b5b766e000aa64b5b4d44b4f10e55cffe968d4ec6ab1) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | | `sourceEventSeqs` | 可选 | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | diff --git a/docs/persistence-changes/2026-10-19-session-team-mode.i18n.yaml b/docs/persistence-changes/2026-10-19-session-team-mode.i18n.yaml new file mode 100644 index 0000000000..d69a435040 --- /dev/null +++ b/docs/persistence-changes/2026-10-19-session-team-mode.i18n.yaml @@ -0,0 +1,28 @@ +# Bilingual-pair consistency record for 2026-10-19-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 docs/persistence-changes/2026-10-19-session-team-mode.md +/: + en: 3d5a424ae7d7e46b + zh: c22a8e4a62d8cc1a +/2026-10-19-session-team-mode: + en: a882cc1bc383bf80 + zh: 33f40913b70ad97b +/2026-10-19-session-team-mode/summary: + en: 665465829d68c487 + zh: 2d5bd37335c79aad +/2026-10-19-session-team-mode/table-of-contents: + en: 6cf314a069ceb451 + zh: 9adff678e9ecab3b +/2026-10-19-session-team-mode/declaration: + en: 6907136f28e48c90 + zh: 02f8298485f3d8e7 +/2026-10-19-session-team-mode/compatibility: + en: c0ca565e19ad32a7 + zh: 548c334b7e15b464 +/2026-10-19-session-team-mode/verification: + en: 7f3a3e4b1ffb9f85 + zh: dd9258af01f40937 +/2026-10-19-session-team-mode/dev-note: + en: b75c8458928b72b9 + zh: 003ba4e17b4b5269 diff --git a/docs/persistence-changes/2026-10-19-session-team-mode.md b/docs/persistence-changes/2026-10-19-session-team-mode.md new file mode 100644 index 0000000000..ca350836a8 --- /dev/null +++ b/docs/persistence-changes/2026-10-19-session-team-mode.md @@ -0,0 +1,48 @@ +--- +description: "Records a persistence type transition and its compatibility acknowledgement." +kind: persistence-change +--- + +# 2026-10-19-session-team-mode + +English | [中文](2026-10-19-session-team-mode.zh.md) + +## Summary + +Records the root Session’s Agent Teams selection with the required team/mode event. + +## Table of Contents + +- [Declaration](#declaration) +- [Compatibility](#compatibility) +- [Verification](#verification) +- [Dev Note](#dev-note) + + +## Declaration + +```yaml persistence-change +schemaVersion: 1 +id: 2026-10-19-session-team-mode +baseline: false +changes: + - root: "event:team/mode" + previous: null + after: "920d87e22b7409bcc7224f8b1dc7f5101c33574c75627c65baab12b85cc96e75" + decision: same-version +``` + + +## Compatibility + +Existing V4 logs remain readable. Without a selection, historical tasks enable Team; without historical tasks it remains disabled. Readers predating team/mode refuse the unknown required discriminator, so they cannot silently restore the wrong mode. Ordinary forks ignore inherited mode selections and start disabled. No existing payload or Session header version changes. + + +## Verification + +197 focused Team, tool, model-menu and client tests passed; the Web mode and task-board scenario passed 8 tests. The authored team-session-mode headless and TypeScript SDK snapshots exercise on/off commands; Python SDK raw-event projection retains both selections. + + +## Dev Note + +None. diff --git a/docs/persistence-changes/2026-10-19-session-team-mode.schema.json b/docs/persistence-changes/2026-10-19-session-team-mode.schema.json new file mode 100644 index 0000000000..4e854f51d3 --- /dev/null +++ b/docs/persistence-changes/2026-10-19-session-team-mode.schema.json @@ -0,0 +1,15 @@ +{ + "formatVersion": 2, + "roots": [ + {"key":"event:team/mode","kind":"event","event":"team/mode","surface":false,"digest":"920d87e22b7409bcc7224f8b1dc7f5101c33574c75627c65baab12b85cc96e75"} + ], + "types": [], + "nodes": { + "0657d3ca5ac4e7bf": {"kind":"primitive","type":"number"}, + "920d87e22b7409bc": {"kind":"object","properties":[{"name":"data","type":"c2a13242efc3bea4","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"d8a476d2c6fd30df","optional":false}],"indices":[]}, + "aa93010d0e521ad7": {"kind":"primitive","type":"boolean"}, + "c2a13242efc3bea4": {"kind":"object","properties":[{"name":"enabled","type":"aa93010d0e521ad7","optional":false}],"indices":[]}, + "d8a476d2c6fd30df": {"kind":"literal","value":"team/mode"}, + "dacf3d5f3e4ff619": {"kind":"literal","value":true} + } +} diff --git a/docs/persistence-changes/2026-10-19-session-team-mode.zh.md b/docs/persistence-changes/2026-10-19-session-team-mode.zh.md new file mode 100644 index 0000000000..6ea99093a6 --- /dev/null +++ b/docs/persistence-changes/2026-10-19-session-team-mode.zh.md @@ -0,0 +1,48 @@ +--- +description: "记录持久化类型更改及其兼容性确认。" +kind: persistence-change +--- + +# 2026-10-19-session-team-mode + +[English](2026-10-19-session-team-mode.md) | 中文 + +## 概述 + +通过必读的 team/mode 事件记录根 Session 的智能体团队选择。 + +## 目录 + +- [声明](#declaration) +- [兼容性](#compatibility) +- [验证](#verification) +- [开发备注](#dev-note) + + +## 声明 + +```yaml persistence-change +schemaVersion: 1 +id: 2026-10-19-session-team-mode +baseline: false +changes: + - root: "event:team/mode" + previous: null + after: "920d87e22b7409bcc7224f8b1dc7f5101c33574c75627c65baab12b85cc96e75" + decision: same-version +``` + + +## 兼容性 + +已有 V4 日志仍可读取。没有显式选择时,历史任务使 Team 开启;没有历史任务时保持关闭。旧读取器拒绝未知的必读 team/mode 判别值,避免静默恢复错误模式。普通分叉忽略继承的模式选择,初始保持关闭。不修改已有载荷或 Session 头部版本。 + + +## 验证 + +197 个 Team、工具、模型菜单和客户端定向测试通过;Web 开关与任务板场景的 8 个测试通过。team-session-mode 的 headless 和 TypeScript SDK 人工编写快照覆盖开关命令;Python SDK 原始事件投影保留两次选择。 + + +## 开发备注 + +无。 diff --git a/docs/persistence-changes/2026-10-27-subagent-settlement-attribution.i18n.yaml b/docs/persistence-changes/2026-10-27-subagent-settlement-attribution.i18n.yaml new file mode 100644 index 0000000000..e1d1916155 --- /dev/null +++ b/docs/persistence-changes/2026-10-27-subagent-settlement-attribution.i18n.yaml @@ -0,0 +1,28 @@ +# Bilingual-pair consistency record for 2026-10-27-subagent-settlement-attribution.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 docs/persistence-changes/2026-10-27-subagent-settlement-attribution.md +/: + en: 3d5a424ae7d7e46b + zh: c22a8e4a62d8cc1a +/2026-10-27-subagent-settlement-attribution: + en: d2a2da526a11227c + zh: 047e8cc6da20118f +/2026-10-27-subagent-settlement-attribution/summary: + en: fb422315a6c3e9e7 + zh: bce866c511613c73 +/2026-10-27-subagent-settlement-attribution/table-of-contents: + en: 6cf314a069ceb451 + zh: 9adff678e9ecab3b +/2026-10-27-subagent-settlement-attribution/declaration: + en: 6907136f28e48c90 + zh: 02f8298485f3d8e7 +/2026-10-27-subagent-settlement-attribution/compatibility: + en: e18e26f07dcc315e + zh: 7120bc3f3e9b90b5 +/2026-10-27-subagent-settlement-attribution/verification: + en: 42d56d2378276e50 + zh: 16c96c5c3d018964 +/2026-10-27-subagent-settlement-attribution/dev-note: + en: b75c8458928b72b9 + zh: 003ba4e17b4b5269 diff --git a/docs/persistence-changes/2026-10-27-subagent-settlement-attribution.md b/docs/persistence-changes/2026-10-27-subagent-settlement-attribution.md new file mode 100644 index 0000000000..0f5994a749 --- /dev/null +++ b/docs/persistence-changes/2026-10-27-subagent-settlement-attribution.md @@ -0,0 +1,60 @@ +--- +description: "Records a persistence type transition and its compatibility acknowledgement." +kind: persistence-change +--- + +# 2026-10-27-subagent-settlement-attribution + +English | [中文](2026-10-27-subagent-settlement-attribution.zh.md) + +## Summary + +Adds optional stopReason attribution to persisted subagent settlement notices while keeping the Session format version unchanged. + +## Table of Contents + +- [Declaration](#declaration) +- [Compatibility](#compatibility) +- [Verification](#verification) +- [Dev Note](#dev-note) + + +## Declaration + +```yaml persistence-change +schemaVersion: 1 +id: 2026-10-27-subagent-settlement-attribution +baseline: false +changes: + - root: "event:agent/inbox/spliced" + previous: "2026-10-13-liveshare-share-viewer-principal" + after: "ddce54a3873dc57113b0cd38ad9b23b3c259186139e8847e5e5f415d86f52482" + decision: same-version + - root: "event:developer/message" + previous: "2026-10-13-liveshare-share-viewer-principal" + after: "70ea8e7943c5e2d76905bcc2e694e8c3d5b138aaae3609f3e261b8d369a6e35a" + decision: same-version + - root: "event:session/title-llm-request" + previous: "2026-10-13-liveshare-share-viewer-principal" + after: "d95c624eddecdacf1ee74356196c09d2f3fcd625714be5e852a6b643fa12a7ca" + decision: same-version + - root: "event:user/message" + previous: "2026-10-13-liveshare-share-viewer-principal" + after: "dcb07e04496586d2664d72dab0e94312917da56927b2a0dc3ee53d6a33e32450" + decision: same-version +``` + + +## Compatibility + +New readers accept historical notices without stopReason and must not infer it from the English summary. Writers always record the exact captured execution stopReason. Older readers preserve additional source metadata and continue using the existing kind, notice form, summary, senderSessionId, and unchanged content; native V4 source admission validates the producer kind rather than rejecting additional attribution fields. The new metadata affects presentation only, not replay, authority, or recovery. No migration or Session version bump is required. + + +## Verification + +Focused Vitest runs passed: continuation-messages.spec.ts (14 tests), external-activation.spec.ts (19 tests), continuation-messages-adapter.spec.ts (1 test), and continuation.spec.ts (166 tests), all under packages/subagent/subagent/tests. Command: node node_modules/vitest/vitest.mjs run followed by the listed owner paths. Coverage includes all five terminal reasons, historical attribution without inference, external cleanup failure retaining the captured execution reason, cold resume, and unchanged DeepSeek Messages wire content. Historical Session generations remain unchanged; affected current-format snapshot expectations include stopReason. + + +## Dev Note + +None. diff --git a/docs/persistence-changes/2026-10-27-subagent-settlement-attribution.schema.json b/docs/persistence-changes/2026-10-27-subagent-settlement-attribution.schema.json new file mode 100644 index 0000000000..3299e5d126 --- /dev/null +++ b/docs/persistence-changes/2026-10-27-subagent-settlement-attribution.schema.json @@ -0,0 +1,259 @@ +{ + "formatVersion": 2, + "roots": [ + {"key":"event:agent/inbox/spliced","kind":"event","event":"agent/inbox/spliced","surface":false,"digest":"ddce54a3873dc57113b0cd38ad9b23b3c259186139e8847e5e5f415d86f52482"}, + {"key":"event:developer/message","kind":"event","event":"developer/message","surface":true,"digest":"70ea8e7943c5e2d76905bcc2e694e8c3d5b138aaae3609f3e261b8d369a6e35a"}, + {"key":"event:session/title-llm-request","kind":"event","event":"session/title-llm-request","surface":false,"digest":"d95c624eddecdacf1ee74356196c09d2f3fcd625714be5e852a6b643fa12a7ca"}, + {"key":"event:user/message","kind":"event","event":"user/message","surface":true,"digest":"dcb07e04496586d2664d72dab0e94312917da56927b2a0dc3ee53d6a33e32450"} + ], + "types": [], + "nodes": { + "01c161379a3c3394": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"589d0d91239681f8","optional":false},{"name":"source","type":"b120362fd216a16a","optional":false,"compatibility":{"version":1,"policy":"session-source-attribution","binding":"session.developer-message.source","discriminator":"kind","unknownKinds":"preserve","attributionKinds":["action-cards","tmux-context","user-question-reply","working-directory"]}}],"indices":[]}, + "0203243b72fc867a": {"kind":"object","properties":[{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, + "022623b9ef98ec47": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false}],"indices":[]}, + "02b2c23f2d94b028": {"kind":"object","properties":[{"name":"kind","type":"c5687b1313206a6f","optional":false}],"indices":[]}, + "05205074b24d8340": {"kind":"literal","value":"action-cards"}, + "0657d3ca5ac4e7bf": {"kind":"primitive","type":"number"}, + "07e1a58c58b593f5": {"kind":"object","properties":[{"name":"model","type":"4cd48f3e5108bd6e","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "09655b34d7f3cf1e": {"kind":"literal","value":"system-prompt"}, + "09f3f8dc417f2782": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"8af841fc9b265ade","optional":false},{"name":"source","type":"65a8400e451e667e","optional":false}],"indices":[]}, + "0a92c5bdfa06c2e2": {"kind":"object","properties":[{"name":"displayName","type":"4cd48f3e5108bd6e","optional":true},{"name":"providerId","type":"4cd48f3e5108bd6e","optional":false},{"name":"subjectId","type":"4cd48f3e5108bd6e","optional":false},{"name":"tenantId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "0ed8d1108fe7a5e1": {"kind":"object","properties":[{"name":"kind","type":"a9059a81430b3207","optional":false}],"indices":[]}, + "0eeb17e1df89b1eb": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, + "0f46153645c29784": {"kind":"object","properties":[{"name":"height","type":"0657d3ca5ac4e7bf","optional":false},{"name":"width","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "0fe52387f6505270": {"kind":"literal","value":"plan-mode"}, + "11638dc58a31496f": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "12d5808fac1996b9": {"kind":"object","properties":[{"name":"attachment","type":"b0b0eb1b16490f6b","optional":false},{"name":"offloaded","type":"dacf3d5f3e4ff619","optional":true},{"name":"type","type":"fb8b6a2e53cb9cf4","optional":false}],"indices":[]}, + "1379f0b72275994d": {"kind":"literal","value":"developer/message"}, + "16d0507cb94b4cfc": {"kind":"literal","value":"relay"}, + "1922cd79a70806c1": {"kind":"literal","value":"skill-invocation"}, + "1938c8c20f1cba7c": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false}],"indices":[]}, + "1e0b0b66b6f0c53f": {"kind":"literal","value":"aborted"}, + "1e709346005826eb": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false}],"indices":[]}, + "1ebc6f9ff3aa9e3a": {"kind":"literal","value":"completed"}, + "1f0133217eeeffd8": {"kind":"literal","value":"tool-call"}, + "1f1385dbc3e875c0": {"kind":"literal","value":"repeat-tool-reminder"}, + "1fdad624334d3548": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"bb94f3217aa56c88","optional":false}],"indices":[]}, + "2152caf1a71e588e": {"kind":"object","properties":[{"name":"kind","type":"2e4debca12f2c449","optional":false},{"name":"messageId","type":"4cd48f3e5108bd6e","optional":false},{"name":"senderId","type":"4cd48f3e5108bd6e","optional":false},{"name":"senderName","type":"4cd48f3e5108bd6e","optional":false},{"name":"teamId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "2235d957bf4b7c6c": {"kind":"object","properties":[{"name":"kind","type":"7b1ce60193fa8b69","optional":false}],"indices":[]}, + "224aa11d4d5f88c4": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false}],"indices":[]}, + "23af3759678a30de": {"kind":"literal","value":"dsh-session-title-llm"}, + "24b10f7cdae64928": {"kind":"literal","value":"runtime-context"}, + "24ce31aa528272e5": {"kind":"object","properties":[{"name":"kind","type":"99e7e7d7fed55ae6","optional":false}],"indices":[]}, + "260523b412c19ad8": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false}],"indices":[]}, + "2765e9119edf5816": {"kind":"literal","value":"model"}, + "277b61f9c6f81704": {"kind":"literal","value":"file"}, + "28936c614bd4e7e2": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "290f32bfd81ff85a": {"kind":"object","properties":[{"name":"kind","type":"23af3759678a30de","optional":false}],"indices":[]}, + "297fbd8067d7815a": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"a9059a81430b3207","optional":false}],"indices":[]}, + "2aad1179f9576314": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "2ab12a0e345658f9": {"kind":"array","element":"83da72c6857601c7"}, + "2e4debca12f2c449": {"kind":"literal","value":"team-message"}, + "2f1e6a7ebace43d7": {"kind":"union","types":["3f74ba74acd10644","58f94390b982f8a5","de30b1fa12c75f81","ebb768d85ec87fb6"]}, + "2f3f201c8846e19c": {"kind":"literal","value":"tool-goal"}, + "312aee2f0c42aabb": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "31907de956375182": {"kind":"object","properties":[{"name":"compactionId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"6b23ed9ce1b397e6","optional":false},{"name":"sourceCommandId","type":"4cd48f3e5108bd6e","optional":true}],"indices":[]}, + "3247e566f4340029": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "335e242de1fcc17b": {"kind":"union","types":["bcf0caf62d964b2f","e438a9d4fb883b3e"]}, + "383cf73ae4748e5b": {"kind":"literal","value":"recall"}, + "3b04790e08b35241": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, + "3b4f2c9ef2c1217e": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"bb94f3217aa56c88","optional":false}],"indices":[]}, + "3bd652ebfa8726b3": {"kind":"union","types":["0657d3ca5ac4e7bf","938f7c7357bc403e"]}, + "3bdb8dc6989655b0": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"a9059a81430b3207","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "3c2535ca0a2062f1": {"kind":"literal","value":"reasoning"}, + "3e2ece4967cfebe9": {"kind":"literal","value":"next-step"}, + "3f74ba74acd10644": {"kind":"literal","value":"image/jpeg"}, + "408927267422a7b2": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, + "43e311235aa537df": {"kind":"object","properties":[{"name":"toolName","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"83d6916021eede8f","optional":false}],"indices":[]}, + "45f005e6bbb76452": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "4991dfc415c9efeb": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false}],"indices":[]}, + "49dab019d00e8c26": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "4a561c111a78d452": {"kind":"literal","value":"refusal"}, + "4a9bef9f27a94252": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"82470020b5bfcb1a","optional":false},{"name":"senderSessionId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "4b259f7f8f0a44ad": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"isError","type":"aa93010d0e521ad7","optional":true},{"name":"role","type":"a13d8f379222ece8","optional":false},{"name":"source","type":"d9c462b9f62fdd3e","optional":false},{"name":"toolCallId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "4cd48f3e5108bd6e": {"kind":"primitive","type":"string"}, + "4d60dddaa65d3da8": {"kind":"object","properties":[{"name":"actor","type":"0a92c5bdfa06c2e2","optional":false},{"name":"kind","type":"6ba7edb6447b6a3e","optional":false},{"name":"shareId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "4e44c9118152f682": {"kind":"literal","value":"text"}, + "4e88776858652856": {"kind":"object","properties":[{"name":"text","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"4e44c9118152f682","optional":false}],"indices":[]}, + "4edec1b770e8ec4c": {"kind":"literal","value":"ptc-mode"}, + "4f91f187baeabef5": {"kind":"object","properties":[{"name":"capturedFormatVersion","type":"0657d3ca5ac4e7bf","optional":true},{"name":"capturedThroughSeq","type":"3bd652ebfa8726b3","optional":false},{"name":"compacted","type":"aa93010d0e521ad7","optional":false},{"name":"inputIndex","type":"0657d3ca5ac4e7bf","optional":false},{"name":"label","type":"4cd48f3e5108bd6e","optional":false},{"name":"omittedBytes","type":"0657d3ca5ac4e7bf","optional":false},{"name":"omittedMessages","type":"0657d3ca5ac4e7bf","optional":false},{"name":"originalMessages","type":"0657d3ca5ac4e7bf","optional":false},{"name":"retainedMessages","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sessionId","type":"4cd48f3e5108bd6e","optional":false},{"name":"truncated","type":"aa93010d0e521ad7","optional":false}],"indices":[]}, + "52c6113cb0e53f34": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "53cf1704ae0709c7": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false}],"indices":[]}, + "5654685d7557ec74": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "58015da1c1950a66": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, + "587bb11da730e06a": {"kind":"literal","value":"session/title-llm-request"}, + "589d0d91239681f8": {"kind":"literal","value":"developer"}, + "58f94390b982f8a5": {"kind":"literal","value":"image/png"}, + "5bb69b88517f6214": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "5bc66269cfaf614c": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false}],"indices":[]}, + "5cdbf342b7748d15": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false}],"indices":[]}, + "5d03ba38734809bc": {"kind":"array","element":"0657d3ca5ac4e7bf"}, + "5d7a64ed6aac5551": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"8067b0d232c350f1","optional":false}],"indices":[]}, + "601327bedca5f98a": {"kind":"object","properties":[{"name":"maxTokens","type":"0657d3ca5ac4e7bf","optional":false},{"name":"messageSeqs","type":"5d03ba38734809bc","optional":false},{"name":"messages","type":"95b5cf90ea1f57da","optional":false},{"name":"reasoningEffort","type":"4cd48f3e5108bd6e","optional":true},{"name":"route","type":"07e1a58c58b593f5","optional":false},{"name":"system","type":"4cd48f3e5108bd6e","optional":false},{"name":"titleProvider","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "6085488d223aeffd": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false}],"indices":[]}, + "613660455704fb82": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"68f44f59e91c4139","optional":false},{"name":"source","type":"b120362fd216a16a","optional":false,"compatibility":{"version":1,"policy":"session-source-attribution","binding":"session.user-message.source","discriminator":"kind","unknownKinds":"preserve","attributionKinds":["action-cards","tmux-context","user-question-reply","working-directory"]}}],"indices":[]}, + "6292bcbedaa97fea": {"kind":"object","properties":[{"name":"kind","type":"f68c335306535a00","optional":false}],"indices":[]}, + "62ecb62dc0fe1b5e": {"kind":"literal","value":"next-turn"}, + "65377a76c6a58f78": {"kind":"object","properties":[{"name":"headerSeq","type":"0657d3ca5ac4e7bf","optional":true},{"name":"message","type":"01c161379a3c3394","optional":false},{"name":"step","type":"0657d3ca5ac4e7bf","optional":false},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "65a8400e451e667e": {"kind":"object","properties":[{"name":"kind","type":"09655b34d7f3cf1e","optional":false}],"indices":[]}, + "65d583e66518c523": {"kind":"object","properties":[{"name":"entries","type":"aba175fb20ffc711","optional":false},{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"incomplete","type":"dacf3d5f3e4ff619","optional":true},{"name":"kind","type":"6ebcc2dbec9f2dab","optional":false},{"name":"update","type":"dacf3d5f3e4ff619","optional":true}],"indices":[]}, + "672c4aa2a9b736e3": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"1922cd79a70806c1","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "68141cbe3994a5ff": {"kind":"literal","value":"max-tokens"}, + "68f44f59e91c4139": {"kind":"literal","value":"user"}, + "690583582246babf": {"kind":"array","element":"613660455704fb82"}, + "6b23ed9ce1b397e6": {"kind":"literal","value":"compact-checkpoint"}, + "6ba7edb6447b6a3e": {"kind":"literal","value":"share-viewer"}, + "6ccb6bc423fb3952": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "6ea608bdc8a7a63d": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"a9059a81430b3207","optional":false}],"indices":[]}, + "6ebcc2dbec9f2dab": {"kind":"literal","value":"skill-catalog"}, + "6f22a9d02b9a1f11": {"kind":"object","properties":[{"name":"baseline","type":"dacf3d5f3e4ff619","optional":true},{"name":"baselineIdentity","type":"4cd48f3e5108bd6e","optional":true},{"name":"changes","type":"2ab12a0e345658f9","optional":false},{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"fbd2c74b346a1595","optional":false}],"indices":[]}, + "6f44f32be9bcd9a8": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false}],"indices":[]}, + "6f9e00383836275e": {"kind":"literal","value":"snapshot"}, + "70ea8e7943c5e2d7": {"kind":"object","properties":[{"name":"data","type":"65377a76c6a58f78","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"1379f0b72275994d","optional":false}],"indices":[]}, + "719c51f67074f970": {"kind":"object","properties":[{"name":"kind","type":"2765e9119edf5816","optional":false},{"name":"model","type":"4cd48f3e5108bd6e","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false},{"name":"replayState","type":"bbc5696e3f13c758","optional":true}],"indices":[]}, + "7382c4fc95fe8c5d": {"kind":"literal","value":"remove"}, + "7554565be416b034": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false}],"indices":[]}, + "756b5e23a5cb2c7f": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"8067b0d232c350f1","optional":false}],"indices":[]}, + "78a9be340090b735": {"kind":"literal","value":"tmux-context"}, + "7b1ce60193fa8b69": {"kind":"literal","value":"working-directory"}, + "7df62978a2b7de85": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false}],"indices":[]}, + "7eaef7a763ba5b94": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false}],"indices":[]}, + "7f13860ee89a2d35": {"kind":"literal","value":"session-reference"}, + "7f959241c24128b4": {"kind":"object","properties":[{"name":"goalId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"b815d7b91cdd9fc6","optional":false},{"name":"revision","type":"0657d3ca5ac4e7bf","optional":false},{"name":"round","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "8064c03c87c357fd": {"kind":"union","types":["12d5808fac1996b9","43e311235aa537df","4e88776858652856","814d9434e1ddf907","8e611bf46821e500","c1f0f17b6493b85d","f2da0f27c1e6add5"]}, + "80677c8c8b804189": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false}],"indices":[]}, + "8067b0d232c350f1": {"kind":"literal","value":"time-context"}, + "814d9434e1ddf907": {"kind":"object","properties":[{"name":"text","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"3c2535ca0a2062f1","optional":false}],"indices":[]}, + "82470020b5bfcb1a": {"kind":"literal","value":"agent-message"}, + "824a4aad44a54e24": {"kind":"union","types":["4d60dddaa65d3da8","deebfc7c62177ea8"]}, + "825ca23416b1ea9d": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false}],"indices":[]}, + "829a2fb3be6efd4b": {"kind":"literal","value":"agent/inbox/spliced"}, + "83079c8a3f733ac3": {"kind":"object","properties":[{"name":"description","type":"4cd48f3e5108bd6e","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "8338ee303e2bd448": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "83d6916021eede8f": {"kind":"literal","value":"tool-removal"}, + "83da72c6857601c7": {"kind":"object","properties":[{"name":"action","type":"904bf499d98be4a3","optional":false},{"name":"digest","type":"4cd48f3e5108bd6e","optional":true},{"name":"path","type":"4cd48f3e5108bd6e","optional":false},{"name":"scope","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "848639f8787f035b": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"bf16d2e98a7f1389","optional":false},{"name":"senderSessionId","type":"4cd48f3e5108bd6e","optional":false},{"name":"stopReason","type":"866f7b2dbdc5b421","optional":true},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "854db3efdfb0cd39": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"bb94f3217aa56c88","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "85d6d96ff8842974": {"kind":"literal","value":"answered"}, + "85f9b505b7bdff1f": {"kind":"literal","value":"tool-addition"}, + "85fe96bf15d5c755": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"8067b0d232c350f1","optional":false}],"indices":[]}, + "8620189c8e335827": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false}],"indices":[]}, + "866f7b2dbdc5b421": {"kind":"union","types":["1e0b0b66b6f0c53f","1ebc6f9ff3aa9e3a","4a561c111a78d452","68141cbe3994a5ff","a2be0f0cb59f0d4a"]}, + "86e1e384495ee09a": {"kind":"object","properties":[{"name":"name","type":"4cd48f3e5108bd6e","optional":false},{"name":"text","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "87b1a33564b9a4d6": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"a9059a81430b3207","optional":false}],"indices":[]}, + "88841202942c7683": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"bb94f3217aa56c88","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "8a046b52331959aa": {"kind":"object","properties":[{"name":"kind","type":"78a9be340090b735","optional":false}],"indices":[]}, + "8af841fc9b265ade": {"kind":"literal","value":"system"}, + "8c1c4f72755a78ea": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "8c627d7a45fa406c": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "8e611bf46821e500": {"kind":"object","properties":[{"name":"tool","type":"fcf427290c6213c2","optional":true},{"name":"toolName","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"85f9b505b7bdff1f","optional":false}],"indices":[]}, + "8f324cea3916c051": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false}],"indices":[]}, + "8fcb0b44a575e998": {"kind":"object","properties":[{"name":"kind","type":"a720757f704a2512","optional":false}],"indices":[]}, + "904bf499d98be4a3": {"kind":"union","types":["7382c4fc95fe8c5d","b765ee31033cea46","f6cb462f808309e3"]}, + "938f7c7357bc403e": {"kind":"primitive","type":"null"}, + "95a3ffccbf3c8afc": {"kind":"literal","value":"owner"}, + "95b5cf90ea1f57da": {"kind":"array","element":"e272493e3ea0b33b"}, + "95c0e88fcf0bafb2": {"kind":"literal","value":"assistant"}, + "9645cf93769a6dfb": {"kind":"literal","value":"user-question-reply"}, + "96bfb916a727cd44": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, + "96f4a9c81fc0f940": {"kind":"union","types":["3e2ece4967cfebe9","62ecb62dc0fe1b5e"]}, + "99135d8a39379779": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false}],"indices":[]}, + "99e7e7d7fed55ae6": {"kind":"literal","value":"tool-jobs"}, + "99f0e98184709ad1": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"bb94f3217aa56c88","optional":false}],"indices":[]}, + "9ac52f0f84fec2eb": {"kind":"literal","value":"model-selection"}, + "9edc162949abf93b": {"kind":"object","properties":[{"name":"deliveryId","type":"4cd48f3e5108bd6e","optional":false},{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"db6c5246911d47a7","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false},{"name":"ruleId","type":"4cd48f3e5108bd6e","optional":false},{"name":"source","type":"4cd48f3e5108bd6e","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "9f65c43e340bba31": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false}],"indices":[]}, + "a13d8f379222ece8": {"kind":"literal","value":"tool"}, + "a15a19ef63cf5d1b": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false}],"indices":[]}, + "a16e567c8cd2c55c": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false}],"indices":[]}, + "a2066bd52f61218b": {"kind":"object","properties":[{"name":"kind","type":"2f3f201c8846e19c","optional":false}],"indices":[]}, + "a2be0f0cb59f0d4a": {"kind":"literal","value":"error"}, + "a3c6f1ea38e9a61d": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "a4afec59b75aad5d": {"kind":"object","properties":[{"name":"clientTimeZone","type":"4cd48f3e5108bd6e","optional":true},{"name":"kind","type":"68f44f59e91c4139","optional":false},{"name":"principal","type":"824a4aad44a54e24","optional":true},{"name":"rpcId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "a720757f704a2512": {"kind":"literal","value":"tool-registry"}, + "a9059a81430b3207": {"kind":"literal","value":"hooks-claude-code"}, + "aa2bf9e82b9fdeb9": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false}],"indices":[]}, + "aa93010d0e521ad7": {"kind":"primitive","type":"boolean"}, + "aae343bc3d663c83": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false}],"indices":[]}, + "aba175fb20ffc711": {"kind":"array","element":"83079c8a3f733ac3"}, + "ada310bf0bdb8fed": {"kind":"object","properties":[{"name":"kind","type":"68f44f59e91c4139","optional":false}],"indices":[]}, + "ade057f4fdc387ae": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false}],"indices":[]}, + "aec150797b655c5f": {"kind":"literal","value":"catalog"}, + "b062078e899958f9": {"kind":"object","properties":[{"name":"kind","type":"1f1385dbc3e875c0","optional":false}],"indices":[]}, + "b0b0eb1b16490f6b": {"kind":"object","properties":[{"name":"attachmentId","type":"4cd48f3e5108bd6e","optional":false},{"name":"bytes","type":"0657d3ca5ac4e7bf","optional":false},{"name":"height","type":"0657d3ca5ac4e7bf","optional":false},{"name":"mediaType","type":"2f1e6a7ebace43d7","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":true},{"name":"originalDimensions","type":"0f46153645c29784","optional":true},{"name":"width","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "b120362fd216a16a": {"kind":"union","types":["0203243b72fc867a","022623b9ef98ec47","02b2c23f2d94b028","0ed8d1108fe7a5e1","0eeb17e1df89b1eb","11638dc58a31496f","1938c8c20f1cba7c","1e709346005826eb","1fdad624334d3548","2152caf1a71e588e","2235d957bf4b7c6c","224aa11d4d5f88c4","24ce31aa528272e5","260523b412c19ad8","28936c614bd4e7e2","290f32bfd81ff85a","297fbd8067d7815a","2aad1179f9576314","312aee2f0c42aabb","31907de956375182","3247e566f4340029","3b04790e08b35241","3b4f2c9ef2c1217e","3bdb8dc6989655b0","408927267422a7b2","45f005e6bbb76452","4991dfc415c9efeb","49dab019d00e8c26","4a9bef9f27a94252","52c6113cb0e53f34","53cf1704ae0709c7","5654685d7557ec74","58015da1c1950a66","5bb69b88517f6214","5bc66269cfaf614c","5cdbf342b7748d15","5d7a64ed6aac5551","6085488d223aeffd","6292bcbedaa97fea","65a8400e451e667e","65d583e66518c523","672c4aa2a9b736e3","6ccb6bc423fb3952","6ea608bdc8a7a63d","6f22a9d02b9a1f11","6f44f32be9bcd9a8","719c51f67074f970","7554565be416b034","756b5e23a5cb2c7f","7df62978a2b7de85","7eaef7a763ba5b94","7f959241c24128b4","80677c8c8b804189","825ca23416b1ea9d","8338ee303e2bd448","848639f8787f035b","854db3efdfb0cd39","85fe96bf15d5c755","8620189c8e335827","87b1a33564b9a4d6","88841202942c7683","8a046b52331959aa","8c1c4f72755a78ea","8c627d7a45fa406c","8f324cea3916c051","8fcb0b44a575e998","96bfb916a727cd44","99135d8a39379779","99f0e98184709ad1","9edc162949abf93b","9f65c43e340bba31","a15a19ef63cf5d1b","a16e567c8cd2c55c","a2066bd52f61218b","a3c6f1ea38e9a61d","a4afec59b75aad5d","aa2bf9e82b9fdeb9","aae343bc3d663c83","ada310bf0bdb8fed","ade057f4fdc387ae","b062078e899958f9","b2a5c8c008a31e3d","b3681c026a0b98e9","b4ec0af39fd52ed9","b613f83572fe2fbf","b6f94c17dd9bbd7c","b70f3e81fe11b7a5","b79641a9d25e7566","ba2a1a8f194b1bb2","ba4c40b4381e0dcd","baccc97ec34be6c4","c5228b7a55498d84","c55827fba7018198","cb1c8959e1726f82","cda2345a9be9d139","d85590a5675c6e2f","d9c462b9f62fdd3e","db06ef35b27a3842","dba54c09c6e7aafc","dd00cdccd0ba487b","dd5ee8762bd8a14b","dd6cc2bb2a8f2824","dde0cdf9d426768b","deaae5cd30987d4f","e7f4cbb3a57f4cbd","edbbfff1128099fd","ee577a0cc18664c9","f230c0b583e23b25","f29d63336e8266d8","f4a66f654996c093","f56c5b59ca016131","f6329fb30c7ea801","f6605d6c57933aea","f6afe41e3926f422","f7283223aacdc39b","f97f47b0eb87905e","fcf81d3c58c13593","fe1a7acdeed4d0f5"]}, + "b2a5c8c008a31e3d": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false}],"indices":[]}, + "b3681c026a0b98e9": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "b4ec0af39fd52ed9": {"kind":"object","properties":[{"name":"kind","type":"bb94f3217aa56c88","optional":false}],"indices":[]}, + "b52f215a6cd69105": {"kind":"array","element":"8064c03c87c357fd"}, + "b613f83572fe2fbf": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "b6f94c17dd9bbd7c": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"bb94f3217aa56c88","optional":false}],"indices":[]}, + "b70f3e81fe11b7a5": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"a9059a81430b3207","optional":false}],"indices":[]}, + "b765ee31033cea46": {"kind":"literal","value":"set"}, + "b79641a9d25e7566": {"kind":"object","properties":[{"name":"kind","type":"8067b0d232c350f1","optional":false}],"indices":[]}, + "b815d7b91cdd9fc6": {"kind":"literal","value":"goal"}, + "ba2a1a8f194b1bb2": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "ba4c40b4381e0dcd": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "baccc97ec34be6c4": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "bb94f3217aa56c88": {"kind":"literal","value":"hooks-codex"}, + "bbc5696e3f13c758": {"kind":"opaque","reason":"unknown"}, + "bcf0caf62d964b2f": {"kind":"object","properties":[{"name":"endSeq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"op","type":"f6cb462f808309e3","optional":false},{"name":"startSeq","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "bf16d2e98a7f1389": {"kind":"literal","value":"subagent-settled"}, + "c1f0f17b6493b85d": {"kind":"object","properties":[{"name":"arguments","type":"4cd48f3e5108bd6e","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"1f0133217eeeffd8","optional":false}],"indices":[]}, + "c5228b7a55498d84": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "c55827fba7018198": {"kind":"object","properties":[{"name":"callId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"9645cf93769a6dfb","optional":false},{"name":"outcome","type":"85d6d96ff8842974","optional":false}],"indices":[]}, + "c5687b1313206a6f": {"kind":"literal","value":"cordis-host-runner"}, + "c748fb42c80a1200": {"kind":"array","element":"86e1e384495ee09a"}, + "cb1c8959e1726f82": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, + "cda2345a9be9d139": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "d4e4d575abeb4b72": {"kind":"literal","value":"canceled"}, + "d85590a5675c6e2f": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false}],"indices":[]}, + "d95c624eddecdacf": {"kind":"object","properties":[{"name":"data","type":"601327bedca5f98a","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"587bb11da730e06a","optional":false}],"indices":[]}, + "d9c462b9f62fdd3e": {"kind":"object","properties":[{"name":"callId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"a13d8f379222ece8","optional":false}],"indices":[]}, + "d9edf1862e4471d6": {"kind":"literal","value":"schedule"}, + "dacf3d5f3e4ff619": {"kind":"literal","value":true}, + "db06ef35b27a3842": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "db6c5246911d47a7": {"kind":"literal","value":"webhook"}, + "dba54c09c6e7aafc": {"kind":"object","properties":[{"name":"kind","type":"4edec1b770e8ec4c","optional":false}],"indices":[]}, + "dcb07e04496586d2": {"kind":"object","properties":[{"name":"data","type":"613660455704fb82","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"de25352d2dc41963","optional":false}],"indices":[]}, + "dd00cdccd0ba487b": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "dd5ee8762bd8a14b": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false}],"indices":[]}, + "dd6cc2bb2a8f2824": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false}],"indices":[]}, + "ddce54a3873dc571": {"kind":"object","properties":[{"name":"data","type":"e1880c67e735e228","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"829a2fb3be6efd4b","optional":false}],"indices":[]}, + "dde0cdf9d426768b": {"kind":"object","properties":[{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "de25352d2dc41963": {"kind":"literal","value":"user/message"}, + "de30b1fa12c75f81": {"kind":"literal","value":"image/gif"}, + "deaae5cd30987d4f": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, + "deebfc7c62177ea8": {"kind":"object","properties":[{"name":"kind","type":"95a3ffccbf3c8afc","optional":false}],"indices":[]}, + "df843efd87c78cf8": {"kind":"array","element":"4f91f187baeabef5"}, + "e1880c67e735e228": {"kind":"object","properties":[{"name":"inserted","type":"690583582246babf","optional":false},{"name":"outcome","type":"d4e4d575abeb4b72","optional":true},{"name":"removedCount","type":"0657d3ca5ac4e7bf","optional":true},{"name":"start","type":"0657d3ca5ac4e7bf","optional":false},{"name":"target","type":"96f4a9c81fc0f940","optional":false}],"indices":[]}, + "e272493e3ea0b33b": {"kind":"union","types":["01c161379a3c3394","09f3f8dc417f2782","4b259f7f8f0a44ad","613660455704fb82","fa87537f4a9c1e25"]}, + "e438a9d4fb883b3e": {"kind":"literal","value":"append"}, + "e7f4cbb3a57f4cbd": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "e8de0f457fed5b9f": {"kind":"literal","value":"notice"}, + "ebb768d85ec87fb6": {"kind":"literal","value":"image/webp"}, + "edbbfff1128099fd": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"a9059a81430b3207","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "ee577a0cc18664c9": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"8067b0d232c350f1","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "f19bee84bf3d2090": {"kind":"object","properties":[{"name":"attachmentId","type":"4cd48f3e5108bd6e","optional":false},{"name":"bytes","type":"0657d3ca5ac4e7bf","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "f230c0b583e23b25": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"8067b0d232c350f1","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "f29d63336e8266d8": {"kind":"object","properties":[{"name":"kind","type":"d9edf1862e4471d6","optional":false}],"indices":[]}, + "f2da0f27c1e6add5": {"kind":"object","properties":[{"name":"attachment","type":"f19bee84bf3d2090","optional":false},{"name":"type","type":"277b61f9c6f81704","optional":false}],"indices":[]}, + "f4a66f654996c093": {"kind":"object","properties":[{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, + "f56c5b59ca016131": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"7f13860ee89a2d35","optional":false},{"name":"references","type":"df843efd87c78cf8","optional":false},{"name":"version","type":"f88f1b7b6bb733f6","optional":false}],"indices":[]}, + "f6329fb30c7ea801": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, + "f6605d6c57933aea": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false}],"indices":[]}, + "f68c335306535a00": {"kind":"literal","value":"user-approval"}, + "f6afe41e3926f422": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"8067b0d232c350f1","optional":false}],"indices":[]}, + "f6cb462f808309e3": {"kind":"literal","value":"replace"}, + "f7283223aacdc39b": {"kind":"object","properties":[{"name":"kind","type":"9ac52f0f84fec2eb","optional":false}],"indices":[]}, + "f88f1b7b6bb733f6": {"kind":"literal","value":1}, + "f97f47b0eb87905e": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false}],"indices":[]}, + "fa87537f4a9c1e25": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"95c0e88fcf0bafb2","optional":false},{"name":"source","type":"719c51f67074f970","optional":false}],"indices":[]}, + "fb8b6a2e53cb9cf4": {"kind":"literal","value":"image"}, + "fbd2c74b346a1595": {"kind":"literal","value":"agent-instructions"}, + "fcf427290c6213c2": {"kind":"primitive","type":"never"}, + "fcf81d3c58c13593": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false}],"indices":[]}, + "fe1a7acdeed4d0f5": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "fee1caf8ab6a65c1": {"kind":"literal","value":"instructions"} + } +} diff --git a/docs/persistence-changes/2026-10-27-subagent-settlement-attribution.zh.md b/docs/persistence-changes/2026-10-27-subagent-settlement-attribution.zh.md new file mode 100644 index 0000000000..4ea60a4c94 --- /dev/null +++ b/docs/persistence-changes/2026-10-27-subagent-settlement-attribution.zh.md @@ -0,0 +1,60 @@ +--- +description: "记录持久化类型更改及其兼容性确认。" +kind: persistence-change +--- + +# 2026-10-27-subagent-settlement-attribution + +[English](2026-10-27-subagent-settlement-attribution.md) | 中文 + +## 概述 + +为持久化的子代理结算通知增加可选 stopReason 归属信息,保持 Session 格式版本不变。 + +## 目录 + +- [声明](#declaration) +- [兼容性](#compatibility) +- [验证](#verification) +- [开发备注](#dev-note) + + +## 声明 + +```yaml persistence-change +schemaVersion: 1 +id: 2026-10-27-subagent-settlement-attribution +baseline: false +changes: + - root: "event:agent/inbox/spliced" + previous: "2026-10-13-liveshare-share-viewer-principal" + after: "ddce54a3873dc57113b0cd38ad9b23b3c259186139e8847e5e5f415d86f52482" + decision: same-version + - root: "event:developer/message" + previous: "2026-10-13-liveshare-share-viewer-principal" + after: "70ea8e7943c5e2d76905bcc2e694e8c3d5b138aaae3609f3e261b8d369a6e35a" + decision: same-version + - root: "event:session/title-llm-request" + previous: "2026-10-13-liveshare-share-viewer-principal" + after: "d95c624eddecdacf1ee74356196c09d2f3fcd625714be5e852a6b643fa12a7ca" + decision: same-version + - root: "event:user/message" + previous: "2026-10-13-liveshare-share-viewer-principal" + after: "dcb07e04496586d2664d72dab0e94312917da56927b2a0dc3ee53d6a33e32450" + decision: same-version +``` + + +## 兼容性 + +新读取方接受缺少 stopReason 的历史通知,不得从英文摘要推断该字段。写入方始终记录已捕获执行的确切 stopReason。旧读取方保留额外的 source 元数据,继续使用已有 kind、notice form、summary、senderSessionId 和未改变的内容;原生 V4 source 准入校验生产方 kind,不拒绝额外归属字段。新元数据只影响展示,不影响回放、权限或恢复。无需迁移或提升 Session 版本。 + + +## 验证 + +定向 Vitest 运行通过:continuation-messages.spec.ts(14 个测试)、external-activation.spec.ts(19 个测试)、continuation-messages-adapter.spec.ts(1 个测试)和 continuation.spec.ts(166 个测试),均位于 packages/subagent/subagent/tests。命令为 node node_modules/vitest/vitest.mjs run,后接上述测试文件路径。覆盖全部五种终止原因、历史归属信息不作推断、外部清理失败保留已捕获的执行原因、冷恢复,以及不变的 DeepSeek Messages 线协议内容。历史 Session 代际保持不变;受影响的当前格式快照预期包含 stopReason。 + + +## 开发备注 + +无。 diff --git a/docs/persistence-changes/historical-formats/README.i18n.yaml b/docs/persistence-changes/historical-formats/README.i18n.yaml index e9e8287e17..fcd95ef300 100644 --- a/docs/persistence-changes/historical-formats/README.i18n.yaml +++ b/docs/persistence-changes/historical-formats/README.i18n.yaml @@ -15,8 +15,8 @@ en: 14c57f30e676f6e3 zh: b1a0ccf26fab3727 /session-persistence-formats/format-references: - en: 5912b107ada0bfc4 - zh: b997083d6fed3f07 + en: a660985fd2941e51 + zh: 0d4aed6be93be922 /session-persistence-formats/scope-and-evidence: en: b220062139ddac1e zh: 7c4c0efe05dafa9c diff --git a/docs/persistence-changes/historical-formats/README.md b/docs/persistence-changes/historical-formats/README.md index e0a594594d..24b42751fa 100644 --- a/docs/persistence-changes/historical-formats/README.md +++ b/docs/persistence-changes/historical-formats/README.md @@ -32,7 +32,7 @@ The index is generated from validated snapshots and the current writer constant. | 1 | PR #3349 | [V1](v1.md) | [JSON](v1.schema.json) | 54 / 415 | | 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.md) | [JSON](v2.schema.json) | 56 / 435 | | 3 | PR #4320 | [V3](v3.md) | [JSON](v3.schema.json) | 60 / 467 | -| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 65 / 626 | +| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 66 / 631 | diff --git a/docs/persistence-changes/historical-formats/README.zh.md b/docs/persistence-changes/historical-formats/README.zh.md index 4202dfcc4e..ef48b880a4 100644 --- a/docs/persistence-changes/historical-formats/README.zh.md +++ b/docs/persistence-changes/historical-formats/README.zh.md @@ -32,7 +32,7 @@ description: "查找从 V0 到工作区写入器版本的每个 Session 格式 | 1 | PR #3349 | [V1](v1.zh.md) | [JSON](v1.schema.json) | 54 / 415 | | 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.zh.md) | [JSON](v2.schema.json) | 56 / 435 | | 3 | PR #4320 | [V3](v3.zh.md) | [JSON](v3.schema.json) | 60 / 467 | -| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 65 / 626 | +| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 66 / 631 | diff --git a/docs/persistence-schema.json b/docs/persistence-schema.json index 75aa6950eb..3a30b6f025 100644 --- a/docs/persistence-schema.json +++ b/docs/persistence-schema.json @@ -5,7 +5,7 @@ {"key":"JsonlHeaderLine","kind":"header","digest":"18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f"}, {"key":"SessionEventEnvelope","kind":"envelope","digest":"5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc"}, {"key":"event:agent-preset/selected","kind":"event","event":"agent-preset/selected","surface":false,"digest":"a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b"}, - {"key":"event:agent/inbox/spliced","kind":"event","event":"agent/inbox/spliced","surface":false,"digest":"cdf988172b589b230f2bce868489def1bb575eb0501076ac757a8f369ac5c64e"}, + {"key":"event:agent/inbox/spliced","kind":"event","event":"agent/inbox/spliced","surface":false,"digest":"ddce54a3873dc57113b0cd38ad9b23b3c259186139e8847e5e5f415d86f52482"}, {"key":"event:approval/asked","kind":"event","event":"approval/asked","surface":false,"digest":"3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1"}, {"key":"event:approval/decided","kind":"event","event":"approval/decided","surface":false,"digest":"bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3"}, {"key":"event:approval/policy","kind":"event","event":"approval/policy","surface":false,"digest":"26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78"}, @@ -18,7 +18,7 @@ {"key":"event:compaction/start","kind":"event","event":"compaction/start","surface":false,"digest":"db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965"}, {"key":"event:compaction/summary","kind":"event","event":"compaction/summary","surface":false,"digest":"e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602"}, {"key":"event:deliverables/presented","kind":"event","event":"deliverables/presented","surface":false,"digest":"13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176"}, - {"key":"event:developer/message","kind":"event","event":"developer/message","surface":true,"digest":"70d2acdec1ebcfea56d02f7233ebb9ec0f7f494447fb952884c22c3afdc01d6f"}, + {"key":"event:developer/message","kind":"event","event":"developer/message","surface":true,"digest":"70ea8e7943c5e2d76905bcc2e694e8c3d5b138aaae3609f3e261b8d369a6e35a"}, {"key":"event:feedback/message-delete","kind":"event","event":"feedback/message-delete","surface":false,"digest":"3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796"}, {"key":"event:feedback/message-put","kind":"event","event":"feedback/message-put","surface":false,"digest":"b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf"}, {"key":"event:feedback/record","kind":"event","event":"feedback/record","surface":false,"digest":"b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8"}, @@ -40,7 +40,7 @@ {"key":"event:session-log-deepseek/delivery-accepted","kind":"event","event":"session-log-deepseek/delivery-accepted","surface":false,"digest":"d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9"}, {"key":"event:session/end-seed","kind":"event","event":"session/end-seed","surface":false,"digest":"5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e"}, {"key":"event:session/title","kind":"event","event":"session/title","surface":false,"digest":"1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5"}, - {"key":"event:session/title-llm-request","kind":"event","event":"session/title-llm-request","surface":false,"digest":"2e505098f9209e52eebd577f1a6f9d4e773ec4d78987aff28a7c786442b9f54c"}, + {"key":"event:session/title-llm-request","kind":"event","event":"session/title-llm-request","surface":false,"digest":"d95c624eddecdacf1ee74356196c09d2f3fcd625714be5e852a6b643fa12a7ca"}, {"key":"event:step/end","kind":"event","event":"step/end","surface":false,"digest":"e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05"}, {"key":"event:step/start","kind":"event","event":"step/start","surface":false,"digest":"4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986"}, {"key":"event:subagent/catalog","kind":"event","event":"subagent/catalog","surface":false,"digest":"53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac"}, @@ -50,6 +50,7 @@ {"key":"event:team/member","kind":"event","event":"team/member","surface":false,"digest":"4fb59762612c3e3ac3a3bd4f84c9c148d3c3893bd422ba2b201cc039fabd49bc"}, {"key":"event:team/message/delivered","kind":"event","event":"team/message/delivered","surface":false,"digest":"48f9c19417a1abbedfa59f4667bba36b93ac2db407adf5e84cb3ba0de30942cb"}, {"key":"event:team/message/queued","kind":"event","event":"team/message/queued","surface":false,"digest":"21fb6a90d5068f6a0003b7ab316ed2f56342477146a65c00db0f13c4d8df667d"}, + {"key":"event:team/mode","kind":"event","event":"team/mode","surface":false,"digest":"920d87e22b7409bcc7224f8b1dc7f5101c33574c75627c65baab12b85cc96e75"}, {"key":"event:team/task","kind":"event","event":"team/task","surface":false,"digest":"d595ec73b32b016a6055333c67a5d646032b22e672da1d4a09c5b1ae398a093a"}, {"key":"event:todo/write","kind":"event","event":"todo/write","surface":false,"digest":"b978cff734e62143eb56c9125423ec275405eda969802d42aaf73ecb987d3b26"}, {"key":"event:tool-workflow/agent-end","kind":"event","event":"tool-workflow/agent-end","surface":false,"digest":"babf9ee4d1af62bf6c3a8103737f7a5e4e78ce179be835ce05a38803e15884b7"}, @@ -62,13 +63,14 @@ {"key":"event:tool/result","kind":"event","event":"tool/result","surface":true,"digest":"7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397"}, {"key":"event:turn/end","kind":"event","event":"turn/end","surface":false,"digest":"0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce"}, {"key":"event:turn/start","kind":"event","event":"turn/start","surface":false,"digest":"aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8"}, - {"key":"event:user/message","kind":"event","event":"user/message","surface":true,"digest":"5bb231bc5afdb532a6898a20604e76bf0c1db438bfcdc17d4847dceaddc02451"}, + {"key":"event:user/message","kind":"event","event":"user/message","surface":true,"digest":"dcb07e04496586d2664d72dab0e94312917da56927b2a0dc3ee53d6a33e32450"}, {"key":"event:web/deepseek-search-llm-request","kind":"event","event":"web/deepseek-search-llm-request","surface":false,"digest":"0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52"}, {"key":"event:working-directory/change","kind":"event","event":"working-directory/change","surface":false,"digest":"124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3"}, {"key":"event:workspace/changes","kind":"event","event":"workspace/changes","surface":false,"digest":"e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72"} ], "types": [ {"digest":"0116b1f89c05221d56b0ba35c83d302bf4857bc7277d247e9bbaf74b27d2c66e","names":[],"sources":[]}, + {"digest":"01c161379a3c3394a642807843e108ef911a8b7dbffd6d7de43f4484c88b0796","names":["packages/llm/llm/src/message.ts#DeveloperMessage"],"sources":["packages/llm/llm/src/message.ts:157"]}, {"digest":"0203243b72fc867afc81aff5f4cf2167e564ed342534eb612fc92f295cf07789","names":[],"sources":[]}, {"digest":"022623b9ef98ec47ec3c3bae0c51027c70d2601141c5e92e26a0d83d1b4cef06","names":[],"sources":[]}, {"digest":"023a355b2be0d688fb6e4df8388dadb942761d46cfe96d0f499aae9631a159d4","names":[],"sources":["packages/core/session/src/types.ts:211"]}, @@ -90,7 +92,7 @@ {"digest":"0af3ecfb8c60d416fc40a7cc4fb2c4851a8e46608b2b38ab9e510670c971aa1f","names":[],"sources":[]}, {"digest":"0b6d718c693fdf7f06dcccb0bcbbabe78cc63514eafc62d6b2a3899735279d6f","names":["packages/goal/goal/src/types.ts#GoalRef"],"sources":["packages/goal/goal/src/types.ts:20"]}, {"digest":"0b9b8facbe2c19773b3fb0ecc824ccbcc603d41b8f18069faa3807d449572fce","names":[],"sources":[]}, - {"digest":"0bcfaf5566d271028429ac7b562cb59053c9d78b8ae14708c35fac835108d1b1","names":[],"sources":["packages/experimental/agent-team/src/types.ts:236"]}, + {"digest":"0bcfaf5566d271028429ac7b562cb59053c9d78b8ae14708c35fac835108d1b1","names":[],"sources":["packages/team/agent-team/src/types.ts:179"]}, {"digest":"0bdaf9cb63594be4ef034b68901fffe792fe09d6d5f6bac42d7f56f6e7834fb9","names":[],"sources":[]}, {"digest":"0c4205180f75b9ee1e0d878bd9a9851628b5dc18448486389d9c5fdc3f1ccda3","names":[],"sources":[]}, {"digest":"0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52","names":[],"sources":[]}, @@ -153,7 +155,7 @@ {"digest":"1fdad624334d354820dfb3a26e28d0bc9fc1b62a33f87bab63f715b3f4391bfd","names":[],"sources":[]}, {"digest":"20a57544bbf204dacd1ae6beb2ab29444365c3c869f2afdf489dcdcbc3d9c8c5","names":[],"sources":["packages/plan/plan-mode/src/index.ts:53"]}, {"digest":"2142e391b9ccb60d487afcf373ce74db98d952f69e8289eddb352a7e16e996c5","names":[],"sources":[]}, - {"digest":"2152caf1a71e588e4df2358a806dea24936c1c47131134f01a12939d12cdc0c0","names":["packages/experimental/agent-team/src/types.ts#TeamMessageSource"],"sources":["packages/experimental/agent-team/src/types.ts:138"]}, + {"digest":"2152caf1a71e588e4df2358a806dea24936c1c47131134f01a12939d12cdc0c0","names":["packages/team/agent-team/src/types.ts#TeamMessageSource"],"sources":["packages/team/agent-team/src/types.ts:117"]}, {"digest":"21fb6a90d5068f6a0003b7ab316ed2f56342477146a65c00db0f13c4d8df667d","names":[],"sources":[]}, {"digest":"2235d957bf4b7c6cf959231d480f0ba1f3a6d72d82981493fb6c7c963021d3d5","names":[],"sources":["packages/session/working-directory/src/index.ts:29"]}, {"digest":"224aa11d4d5f88c450822d01b99e6ca5e3a9c7f26a5ede10bae33384a09ef4dc","names":[],"sources":[]}, @@ -183,8 +185,7 @@ {"digest":"2da725e554cd89301d829ed9dc45a572c3cd1b93b5e91d0d71f007976d8a0863","names":[],"sources":[]}, {"digest":"2e3523541e942ef6cc9fbc5b91f242e0ca4c6ada479b79b96025b797f6e41f3f","names":["packages/schedule/schedule/src/types.ts#ScheduleChange"],"sources":["packages/schedule/schedule/src/types.ts:232"]}, {"digest":"2e4debca12f2c449f96dc6574f3fbf2fb346193443114a241d01a236362ec48c","names":[],"sources":[]}, - {"digest":"2e505098f9209e52eebd577f1a6f9d4e773ec4d78987aff28a7c786442b9f54c","names":[],"sources":[]}, - {"digest":"2eafefdc6f79a4922d68a787a09eaf4dd2b2369c3777fba4c05e800b5678ac8e","names":["packages/experimental/agent-team/src/types.ts#TeamMessageSnapshot"],"sources":["packages/experimental/agent-team/src/types.ts:129"]}, + {"digest":"2eafefdc6f79a4922d68a787a09eaf4dd2b2369c3777fba4c05e800b5678ac8e","names":["packages/team/agent-team/src/types.ts#TeamMessageSnapshot"],"sources":["packages/team/agent-team/src/types.ts:108"]}, {"digest":"2f1e6a7ebace43d727ae138ddcd352eb3813960009e5805b8c5ac84300c97716","names":["packages/attachment/attachment/src/types.ts#ImageMediaType"],"sources":["packages/attachment/attachment/src/types.ts:8"]}, {"digest":"2f3f201c8846e19c20bb75c02280e3d4eb3c57509e4715f0c0a4d1d4f91a7a06","names":[],"sources":[]}, {"digest":"2fe5026daf14bd4ff2b7bfb7cf9791c302d9cbba06802178b6dfec00b892698f","names":["packages/core/tool-executor-ptc/src/types.ts#PtcDispatchStartEventData"],"sources":["packages/core/tool-executor-ptc/src/types.ts:12"]}, @@ -226,7 +227,6 @@ {"digest":"3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796","names":[],"sources":[]}, {"digest":"3f74ba74acd10644c1ed6947a411d8088bd2cfac74f61bb8540f96d68e31f004","names":[],"sources":[]}, {"digest":"3fdf9582c3b5597df104d32d97dab6c6ee3a93794f3034ec6ada6272404ea27f","names":["packages/schedule/schedule/src/types.ts#LegacyAfterScheduleRecord"],"sources":["packages/schedule/schedule/src/types.ts:161"]}, - {"digest":"4035903993dfee62cca2142c0ca51ab57075909ddee2817d0620daa97ac4d4ce","names":["packages/llm/llm/src/message.ts#UserMessage"],"sources":["packages/llm/llm/src/message.ts:162"]}, {"digest":"408927267422a7b2beb6df354ff7c1406d929a5646554f48adf85eb8372f811c","names":[],"sources":[]}, {"digest":"4208123b50df5006b181481ab45fcf1cde807b88d3fd4d340090bc2e202fac41","names":[],"sources":[]}, {"digest":"42e0916e0dda5f6d1e7bb05d8514717147c036a79c9f36085683469516c1fd3f","names":[],"sources":[]}, @@ -245,6 +245,7 @@ {"digest":"48f9c19417a1abbedfa59f4667bba36b93ac2db407adf5e84cb3ba0de30942cb","names":[],"sources":[]}, {"digest":"4991dfc415c9efebc36fa6969e1798c7d2e42396cc1e6e0ac7d7c89c5c5c4aea","names":[],"sources":[]}, {"digest":"49dab019d00e8c26f27ad2ccff7b604d69e323ae4d17e0d2a7b6b8a830b1ade2","names":[],"sources":[]}, + {"digest":"4a561c111a78d4520e850c93795369ea169ff86abd082aee86c8fd38121a65aa","names":[],"sources":[]}, {"digest":"4a9bef9f27a942520cc2279ca7dd3082556e4bff543f66b5c0333e1bbed1cd86","names":["packages/subagent/subagent/src/continuation-messages.ts#AgentMessageSource"],"sources":["packages/subagent/subagent/src/continuation-messages.ts:14"]}, {"digest":"4ae007ed190a72c92138b22d059b83208079c7551ca13fb59ae8cf5e23693964","names":[],"sources":[]}, {"digest":"4aed17ec726b5397c29f6cb4bcd7905dc1a12bb6140f1657f073cd24380f2af8","names":[],"sources":[]}, @@ -252,12 +253,11 @@ {"digest":"4b259f7f8f0a44ad5e90b70a73553ced1df27012d126921d397ff403b6c86731","names":["packages/llm/llm/src/message.ts#ToolResultMessage"],"sources":["packages/llm/llm/src/message.ts:173"]}, {"digest":"4b7ddaf3710b88ff9b8df66304f5f97bf03688fd19730eedc2e9535203c820ea","names":[],"sources":[]}, {"digest":"4bb451330ff699f36b45539ceb0eb2606ead72084e2c5bb637c36cc80e2252db","names":[],"sources":["packages/core/session/src/types.ts:198"]}, - {"digest":"4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b","names":["packages/api/session-controller/src/types.ts#SessionRequestId","packages/attachment/attachment/src/brand.ts#AttachmentId","packages/compaction/compaction/src/brand.ts#CompactionId","packages/core/session/src/types.ts#SessionId","packages/experimental/agent-team/src/types.ts#TeamId","packages/experimental/agent-team/src/types.ts#TeamMessageId","packages/experimental/agent-team/src/types.ts#TeamTaskId","packages/experimental/webhook/src/brand.ts#WebhookDeliveryId","packages/experimental/webhook/src/brand.ts#WebhookRuleId","packages/experimental/webhook/src/brand.ts#WebhookSourceId","packages/feedback/message-feedback/src/types.ts#MessageFeedbackVersion","packages/goal/goal/src/types.ts#GoalId","packages/interaction/commands/src/brand.ts#CommandId","packages/interaction/user-approval/src/types.ts#ApprovalRequestId","packages/jobs/jobs/src/brand.ts#JobId","packages/llm/llm-retry/src/brand.ts#RetryId","packages/llm/llm/src/brand.ts#MessageId","packages/llm/llm/src/brand.ts#ProviderRequestId","packages/llm/llm/src/brand.ts#ReasoningEffortId","packages/llm/llm/src/brand.ts#ToolCallId","packages/schedule/schedule/src/types.ts#ScheduleId","packages/session/session-title/src/index.ts#SessionTitleProviderId","packages/session/session-title/src/types.ts#SessionTitleProviderId","packages/workflow/workflow/src/types.ts#WorkflowRunId"],"sources":["packages/api/session-controller/src/types.ts:388","packages/attachment/attachment/src/brand.ts:6","packages/compaction/compaction/src/brand.ts:4","packages/core/session/src/types.ts:21","packages/experimental/agent-team/src/types.ts:21","packages/experimental/agent-team/src/types.ts:33","packages/experimental/agent-team/src/types.ts:9","packages/experimental/webhook/src/brand.ts:12","packages/experimental/webhook/src/brand.ts:6","packages/experimental/webhook/src/brand.ts:9","packages/feedback/message-feedback/src/types.ts:14","packages/goal/goal/src/types.ts:17","packages/interaction/commands/src/brand.ts:31","packages/interaction/user-approval/src/types.ts:17","packages/jobs/jobs/src/brand.ts:19","packages/llm/llm-retry/src/brand.ts:4","packages/llm/llm/src/brand.ts:16","packages/llm/llm/src/brand.ts:31","packages/llm/llm/src/brand.ts:43","packages/llm/llm/src/brand.ts:67","packages/schedule/schedule/src/types.ts:14","packages/session/session-title/src/index.ts:42","packages/session/session-title/src/types.ts:17","packages/workflow/workflow/src/types.ts:13"]}, + {"digest":"4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b","names":["packages/api/session-controller/src/types.ts#SessionRequestId","packages/attachment/attachment/src/brand.ts#AttachmentId","packages/compaction/compaction/src/brand.ts#CompactionId","packages/core/session/src/types.ts#SessionId","packages/experimental/webhook/src/brand.ts#WebhookDeliveryId","packages/experimental/webhook/src/brand.ts#WebhookRuleId","packages/experimental/webhook/src/brand.ts#WebhookSourceId","packages/feedback/message-feedback/src/types.ts#MessageFeedbackVersion","packages/goal/goal/src/types.ts#GoalId","packages/interaction/commands/src/brand.ts#CommandId","packages/interaction/user-approval/src/types.ts#ApprovalRequestId","packages/jobs/jobs/src/brand.ts#JobId","packages/llm/llm-retry/src/brand.ts#RetryId","packages/llm/llm/src/brand.ts#MessageId","packages/llm/llm/src/brand.ts#ProviderRequestId","packages/llm/llm/src/brand.ts#ReasoningEffortId","packages/llm/llm/src/brand.ts#ToolCallId","packages/schedule/schedule/src/types.ts#ScheduleId","packages/session/session-title/src/index.ts#SessionTitleProviderId","packages/session/session-title/src/types.ts#SessionTitleProviderId","packages/team/agent-team/src/types.ts#TeamId","packages/team/agent-team/src/types.ts#TeamMessageId","packages/team/agent-team/src/types.ts#TeamTaskId","packages/workflow/workflow/src/types.ts#WorkflowRunId"],"sources":["packages/api/session-controller/src/types.ts:388","packages/attachment/attachment/src/brand.ts:6","packages/compaction/compaction/src/brand.ts:4","packages/core/session/src/types.ts:21","packages/experimental/webhook/src/brand.ts:12","packages/experimental/webhook/src/brand.ts:6","packages/experimental/webhook/src/brand.ts:9","packages/feedback/message-feedback/src/types.ts:14","packages/goal/goal/src/types.ts:17","packages/interaction/commands/src/brand.ts:31","packages/interaction/user-approval/src/types.ts:17","packages/jobs/jobs/src/brand.ts:19","packages/llm/llm-retry/src/brand.ts:4","packages/llm/llm/src/brand.ts:16","packages/llm/llm/src/brand.ts:31","packages/llm/llm/src/brand.ts:43","packages/llm/llm/src/brand.ts:67","packages/schedule/schedule/src/types.ts:14","packages/session/session-title/src/index.ts:42","packages/session/session-title/src/types.ts:17","packages/team/agent-team/src/types.ts:20","packages/team/agent-team/src/types.ts:32","packages/team/agent-team/src/types.ts:8","packages/workflow/workflow/src/types.ts:13"]}, {"digest":"4d60dddaa65d3da8d4cd014d85ce3d2c2875664dd0433e331d8e9d395289686b","names":["packages/experimental/liveshare/src/host/viewer-principal.ts#ShareViewerPrincipal"],"sources":["packages/experimental/liveshare/src/host/viewer-principal.ts:24"]}, {"digest":"4e011753f8263f436efaabfbb3b4db53a98fc55342c5ded2daf7208b217be159","names":[],"sources":[]}, {"digest":"4e44c9118152f6829634bfd508e47669ab0497caffa380c94b4be91f9390d35d","names":[],"sources":[]}, {"digest":"4e887768586528565381dadbddee6cef555874148089f4579e0b4b1ad096fc9c","names":["packages/llm/llm/src/types.ts#TextBlock"],"sources":["packages/llm/llm/src/types.ts:69","packages/web/web-search-deepseek/src/provider.ts:66"]}, - {"digest":"4e97c3d85c0fc817ee58873c38c25a2474abaaa0af4839311b50b68db3b8cf1a","names":["packages/subagent/subagent/src/continuation-messages.ts#SubagentSettledMessageSource"],"sources":["packages/subagent/subagent/src/continuation-messages.ts:29"]}, {"digest":"4edec1b770e8ec4ce1220bf42edc5ac46713a1b48d4caa778dcfc98bad1f28a3","names":[],"sources":[]}, {"digest":"4f507598cf476214aa7646ce7841a7064a9dcb05d0a1eaaefaf79c2b738f160f","names":[],"sources":[]}, {"digest":"4f91f187baeabef5c7950c4780d31bbe39f3345dff591a6cf26ac7e135ac1cf4","names":[],"sources":["packages/context/session-reference/src/types.ts:18"]}, @@ -265,7 +265,7 @@ {"digest":"50cf02d875f8a1f1411f4a6fbcffab9120ccef5f74a47331fbf29480e5f68f48","names":[],"sources":[]}, {"digest":"511801b0a6470d98566e8385d3dffddb2b67f79fcf73ed6056f46c3cc0a245a7","names":[],"sources":[]}, {"digest":"516da4cdd6d2f1e5ce488e648578ca51f40e458f707b803e5b24de870e799415","names":[],"sources":[]}, - {"digest":"51edec100659da3a9bd9c3f6ba1180f5e64ec9a1585e5057080df45c180bd357","names":["packages/experimental/agent-team/src/types.ts#TeamTaskSnapshot"],"sources":["packages/experimental/agent-team/src/types.ts:75"]}, + {"digest":"51edec100659da3a9bd9c3f6ba1180f5e64ec9a1585e5057080df45c180bd357","names":["packages/team/agent-team/src/types.ts#TeamTaskSnapshot"],"sources":["packages/team/agent-team/src/types.ts:64"]}, {"digest":"525254db03b1d1e6b74cf55aced52817568331ac1f96c0818728910b6692e336","names":[],"sources":[]}, {"digest":"529e0ccd0e34079ab4ff21ed87f810cee0388246263bf22e8aba5986b56fae05","names":[],"sources":["packages/util/values/src/index.ts:4"]}, {"digest":"52c6113cb0e53f345324bf84f01928bea5d5df57ee80bc0dd65407b8bc5a3734","names":[],"sources":[]}, @@ -273,7 +273,6 @@ {"digest":"530da6c0267813bb4511af72883cc518834e624e3b3220a7062955fd57a788a6","names":["packages/core/session/src/types.ts#RequestHeaderReason"],"sources":["packages/core/session/src/types.ts:279"]}, {"digest":"53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac","names":[],"sources":[]}, {"digest":"53cf1704ae0709c7678bcf070c83c619fc48ca1b5c173b74cfa8702f22a8e492","names":[],"sources":[]}, - {"digest":"53d4f8256bc58109468ea358ac54d437760cd765012360cafe72a401e6a41d4a","names":[],"sources":["packages/core/session/src/types.ts:317"]}, {"digest":"54f9276a1617d7c12594722c89cfe864bcbe4517e9a6f9c98bab14f199a135e0","names":[],"sources":["packages/core/session/src/types.ts:363"]}, {"digest":"557e148d30b79ee48645850832b30c717d7c3b64641deb5d6133b8b4e91ee0b8","names":[],"sources":[]}, {"digest":"5654685d7557ec747e74887237065e626a5bdfa426ca43f7fbb0b6fee9b0916c","names":[],"sources":[]}, @@ -290,7 +289,6 @@ {"digest":"5abab3a27fd2736660ecddd94b28d6bbf114986dd894880153bd1be3ccacb563","names":[],"sources":[]}, {"digest":"5b232a9b9bfcc0fc6794dab7865bcf203a65785c5de5aa647a51228bff628413","names":[],"sources":[]}, {"digest":"5b6fb1f226ff56402db08a6aae82af76100b14056271875f69ec526a1ae05d51","names":[],"sources":["packages/interaction/commands/src/types.ts:112"]}, - {"digest":"5bb231bc5afdb532a6898a20604e76bf0c1db438bfcdc17d4847dceaddc02451","names":[],"sources":[]}, {"digest":"5bb69b88517f6214f2af4d1a6d2b771e271ad3ad695cc9c2c272069ae698c9bc","names":[],"sources":[]}, {"digest":"5bc66269cfaf614c0ecda8ecb469f23101a7609793fc2b693ac23c4aecdb575e","names":[],"sources":[]}, {"digest":"5bd310104cfe448e530beb11422d2f791e2e32e8e95fdc528a15f9ca693dae75","names":[],"sources":[]}, @@ -307,16 +305,18 @@ {"digest":"5f2117d1856a5ab43c28371faa978ebfaa48f8c7d734ea3fc94842210df83dca","names":["packages/llm/llm/src/types.ts#LlmFailure"],"sources":["packages/llm/llm/src/types.ts:48"]}, {"digest":"5f26a6c20b37632f8f57729d171c671def4683d994ac6257a8dffcd855101627","names":[],"sources":[]}, {"digest":"5f758f3de5f00c18968a7c6d347083aac5c9ae13f6a79c78f6611b23c4973564","names":["packages/subagent/subagent/src/descriptor.ts#OneShotSubagentDescriptorData"],"sources":["packages/subagent/subagent/src/descriptor.ts:61"]}, + {"digest":"601327bedca5f98a0fa4c27133bd35749f3af2baaaacf53badf074e59cea5d25","names":["packages/session/session-title-llm/src/index.ts#SessionTitleLlmRequestEventData"],"sources":["packages/session/session-title-llm/src/index.ts:43"]}, {"digest":"6085488d223aeffdef51d7ae17655fc26ab22ee13cbed82a50e62e2b6fb5b918","names":[],"sources":[]}, {"digest":"611cae9a3539733aa0c42f66035a7d90f19dcbbdfb5d9e3b628b13705253942d","names":[],"sources":[]}, {"digest":"611d82d2dc50df706cf08a0fc04bc8eaf78083391fe6f10313de943e615dd2da","names":["packages/jobs/jobs/src/types.ts#JobSettledData"],"sources":["packages/jobs/jobs/src/types.ts:40"]}, + {"digest":"613660455704fb82a825b5b766e000aa64b5b4d44b4f10e55cffe968d4ec6ab1","names":["packages/llm/llm/src/message.ts#UserMessage"],"sources":["packages/llm/llm/src/message.ts:162"]}, {"digest":"616f93a5047de668e1c30d57b1786abd76beeb4123599637ec1dc5290320ede5","names":[],"sources":[]}, {"digest":"6292bcbedaa97feaec5b48a03bb8144b581b93309d9d6c905f26da7a6a7a0a4f","names":[],"sources":[]}, {"digest":"62e6a429ff3f390a46ac39fb468692032f42936ca0db19b53454184189401379","names":[],"sources":[]}, {"digest":"62ecb62dc0fe1b5e70217f8a80c39256a40ee3a1917be14e5ca899d20c35f7fd","names":[],"sources":[]}, {"digest":"6487b10ba7542f3c447b1e41a1c6a590b7bfafe0b9b66b49f34028da85bb970f","names":["packages/subagent/subagent/src/descriptor.ts#ContinuableSubagentDescriptorData"],"sources":["packages/subagent/subagent/src/descriptor.ts:72"]}, - {"digest":"64ef542b1bfc988382d337f2ea85aab8ad76ea55341cf082163919d315688428","names":["packages/llm/llm/src/message.ts#DeveloperMessage"],"sources":["packages/llm/llm/src/message.ts:157"]}, - {"digest":"6598cd6da253948d6b207c6374a45335804c79b576866d6215649c2d1bc85e25","names":["packages/experimental/agent-team/src/types.ts#TeamMemberSnapshot"],"sources":["packages/experimental/agent-team/src/types.ts:48"]}, + {"digest":"65377a76c6a58f785d95b89f89f2e13d289cb88a70ea74939d0ec08f429f0a6e","names":[],"sources":["packages/core/session/src/types.ts:317"]}, + {"digest":"6598cd6da253948d6b207c6374a45335804c79b576866d6215649c2d1bc85e25","names":["packages/team/agent-team/src/types.ts#TeamMemberSnapshot"],"sources":["packages/team/agent-team/src/types.ts:50"]}, {"digest":"65a8400e451e667e5b6e34df72ceb81fca8c12330330fa72083ad40279b189ad","names":["packages/llm/llm/src/message.ts#SystemPromptMessageSource"],"sources":["packages/llm/llm/src/message.ts:35"]}, {"digest":"65d583e66518c5236015e0846470fb32efcf92375ed306aef14e50a9d2cd2099","names":["packages/skill/tool-skill/src/index.ts#SkillCatalogSource"],"sources":["packages/skill/tool-skill/src/index.ts:35"]}, {"digest":"66ba70f6f068da443f969a0b947a013c3addd776b9b60a0a393759c19825e9a2","names":["packages/llm/llm/src/types.ts#ToolSchema"],"sources":["packages/llm/llm/src/types.ts:517"]}, @@ -325,6 +325,7 @@ {"digest":"68141cbe3994a5ffcd064c07c75996538437fd9dbf77ce8a2927e89ec577fabe","names":[],"sources":[]}, {"digest":"688e853dc149f0d30b31765c12ec7e3be74169bdf1a4c6463a10b945d50cc1b8","names":[],"sources":[]}, {"digest":"68f44f59e91c41395076cb77265981a601680ec8a381bbbeb6ab754ffdc94573","names":[],"sources":[]}, + {"digest":"690583582246babf6120f16eefd849dd760c5ec84619014951af403a1b3032c0","names":[],"sources":[]}, {"digest":"69081694be231d56fd9580ba14645fd5e35373202605d5c5c841a9435b5fa3b1","names":[],"sources":[]}, {"digest":"693f4952fb5565da96c06bace8924b098ac2fbc915fde0ff8019701183abc057","names":[],"sources":["packages/llm/llm/src/types.ts:168"]}, {"digest":"69d238a6e9b08d67f601b1825962963d8d3523cb69ebf6208c697dc5d058c199","names":[],"sources":[]}, @@ -339,7 +340,7 @@ {"digest":"6ccb6bc423fb3952458abf9c1b1c3284208b7e660344b612cc55da773d61afe4","names":[],"sources":[]}, {"digest":"6cda0d6d3b4336d83998fe580628426df81c2afbab5d768b8813b56d569120e8","names":[],"sources":[]}, {"digest":"6cf082d0fb6034dea61636787ff0ddf43b6e59aad71e644435a479bc9b83ad65","names":[],"sources":[]}, - {"digest":"6e52dd55a1d48954ed5464ec46fe34d9eb65f709baabc9561e7a279910b0c406","names":[],"sources":["packages/preset/agent-preset-registry/src/session.ts:31"]}, + {"digest":"6e52dd55a1d48954ed5464ec46fe34d9eb65f709baabc9561e7a279910b0c406","names":[],"sources":["packages/preset/agent-preset-registry/src/session.ts:30"]}, {"digest":"6ea608bdc8a7a63d3d23b84c49721325094db88d136482e43a820945295eee3e","names":[],"sources":[]}, {"digest":"6ebcc2dbec9f2dabca9f668ae6d39e877899b180740570f453409db339221df3","names":[],"sources":[]}, {"digest":"6f091fe32ab24407236090ca9d710bbaf59bb57466e9f3b8f1f6e4862bc863c6","names":[],"sources":[]}, @@ -349,7 +350,7 @@ {"digest":"6f72ece8f317cd080c4f20a93f1bf709965f5b81ac4669528f5a023191cfcf20","names":["packages/llm/llm-retry/src/types.ts#LlmRetryStartedEventData"],"sources":["packages/llm/llm-retry/src/types.ts:43"]}, {"digest":"6f9e00383836275e46c0a9915767f9bb561be5fc30aae490174908a93e99a912","names":[],"sources":[]}, {"digest":"70b082fe78991edf13cf66e1b4d06cd2dcff9cacfd76f8cfb996bfb61939e7b2","names":[],"sources":[]}, - {"digest":"70d2acdec1ebcfea56d02f7233ebb9ec0f7f494447fb952884c22c3afdc01d6f","names":[],"sources":[]}, + {"digest":"70ea8e7943c5e2d76905bcc2e694e8c3d5b138aaae3609f3e261b8d369a6e35a","names":[],"sources":[]}, {"digest":"7180844e1066230c5e891f1b9ccd942423b0fe48aac6ded7eef2a285b288a732","names":[],"sources":[]}, {"digest":"719c51f67074f97034a6c94297fe1f4b5d3c8d392c57586dab732aa5398c7a91","names":["packages/llm/llm/src/message.ts#ModelMessageSource"],"sources":["packages/llm/llm/src/message.ts:24"]}, {"digest":"71bb3254ec8f1e491ce63b762a7b5f18709de23aa4e8abcc8b886f4b6ce051dd","names":[],"sources":["packages/llm/llm/src/assistant-stream.ts:28"]}, @@ -367,20 +368,17 @@ {"digest":"7a651f3b672c52fb615f22c0ce9cb3cc5b22aad92e6bbeefb0808fd7b9233a50","names":[],"sources":[]}, {"digest":"7af85bf70d4eafce63e739adae38ea85501e68df3a0e22f97f3a0be90fd9e4cb","names":[],"sources":[]}, {"digest":"7b1ce60193fa8b69292e5843858d9cee7dc565dcb40d33c136e5d7e8b8d4296a","names":[],"sources":[]}, - {"digest":"7c203f3d4dfa6638ff2601247cc7ca5d46731d2c614aa61217475b9eb1fbd0b3","names":[],"sources":[]}, {"digest":"7c6129b6961cf4b2a030f36c78d2b0a5be38617651a9b81322e054ee512bee06","names":["packages/feedback/message-feedback/src/types.ts#MessageFeedbackDelete"],"sources":["packages/feedback/message-feedback/src/types.ts:46"]}, {"digest":"7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397","names":[],"sources":[]}, {"digest":"7d226b9fd95b4ba213d4237c22e930a91c3c769c95ca7f92977b9d625ba13438","names":["packages/goal/goal/src/domain.ts#GoalChangeMeta"],"sources":["packages/goal/goal/src/domain.ts:44"]}, {"digest":"7d54a5a5e629e2e7783bfac72683bcc5e969e9f43426b9289826aaf294ff6344","names":["packages/feedback/command-feedback/src/types.ts#FeedbackRecord"],"sources":["packages/feedback/command-feedback/src/types.ts:27"]}, {"digest":"7d65acf0a3a43a8f445ce5d3320b42eca492739fc931b2a7516bfb8368dc2c68","names":["packages/llm/llm/src/types.ts#ContentBlockType"],"sources":["packages/llm/llm/src/types.ts:155"]}, {"digest":"7df62978a2b7de8540e714152297743e0a622209a46a5ac9513fc0fad73ca23b","names":[],"sources":[]}, - {"digest":"7e9f1072c17f7aa86547c9c8b02766c1006d866a1859a3d677a336e932f70d1a","names":["packages/llm/llm/src/message.ts#MessageSource"],"sources":["packages/llm/llm/src/message.ts:136"]}, {"digest":"7eaef7a763ba5b949b8464bc8add8980a3a5ab566014b69787aeba71e5dfe07b","names":[],"sources":[]}, {"digest":"7f13860ee89a2d35ce09f958e85a04566cd59cb1de1d73a9619a9b631961dc71","names":[],"sources":[]}, {"digest":"7f7fd5a6b0064f597534b29ff62ef26e786dffccf5e14f654a7d4fcea2c35f04","names":[],"sources":[]}, {"digest":"7f9365bae9fcb73e1e34061a1acfe0422c1a5fd351848cace9bebbe806f28462","names":["packages/workflow/tool-workflow/src/types.ts#ToolWorkflowRunEndData"],"sources":["packages/workflow/tool-workflow/src/types.ts:36"]}, {"digest":"7f959241c24128b4983b559984811861dc59774b85905c8fa57c6cb0148a3dbc","names":["packages/goal/goal/src/domain.ts#GoalMessageSource"],"sources":["packages/goal/goal/src/domain.ts:47"]}, - {"digest":"7ff62f6599c083ceb5a0d7a8940ff85ba80dc1d7ddd6ba9339903e93f74cba54","names":["packages/session/session-title-llm/src/index.ts#SessionTitleLlmRequestEventData"],"sources":["packages/session/session-title-llm/src/index.ts:43"]}, {"digest":"8064c03c87c357fd1575cb9395ce86e6dcbe2d275bf7717864a02cddf01827d5","names":["packages/llm/llm/src/types.ts#ContentBlock"],"sources":["packages/llm/llm/src/types.ts:157"]}, {"digest":"80677c8c8b804189a0fde8aa70dd4ad7af8bb12dfd03027e8e023c8592c838bc","names":[],"sources":[]}, {"digest":"8067b0d232c350f189738e60ee3c9939d46ca3b32b45db5525237b077893f649","names":[],"sources":[]}, @@ -395,6 +393,7 @@ {"digest":"83d6916021eede8f2d83a1872716caa1efdec39a3681890996c55147187fdd12","names":[],"sources":[]}, {"digest":"83da72c6857601c75da11212ef847f57d457b9c99f0a82246dcdff3e26700239","names":["packages/context/agent-instructions/src/render.ts#AgentInstructionChange"],"sources":["packages/context/agent-instructions/src/render.ts:62"]}, {"digest":"8465ece5e9d9007c038b357015d849c15262cb0bc25abc59e47692d87ee01a15","names":["packages/workflow/workflow/src/types.ts#WorkflowStopReason"],"sources":["packages/workflow/workflow/src/types.ts:63"]}, + {"digest":"848639f8787f035b480ec53cb5686850897c5e8006d87ca231cd8a9ee8634936","names":["packages/subagent/subagent/src/continuation-messages.ts#SubagentSettledMessageSource"],"sources":["packages/subagent/subagent/src/continuation-messages.ts:29"]}, {"digest":"84ac8c1a66ffc697262e181f42e851259eae14038c113ca329fb4700ccdb1f7c","names":["packages/goal/goal/src/types.ts#GoalBlockReason"],"sources":["packages/goal/goal/src/types.ts:52"]}, {"digest":"84bbfdce7d2eab5b0c53e72ff7406db856c8bfd621b965b72fd86598ba493bf7","names":[],"sources":[]}, {"digest":"854db3efdfb0cd39d30953b28703e519c121f1115cebfffc40369fe86c406dd1","names":[],"sources":[]}, @@ -402,6 +401,7 @@ {"digest":"85f9b505b7bdff1f61845c4cea8f36fd0fde1b62facbe87d8924d1d27e616f3a","names":[],"sources":[]}, {"digest":"85fe96bf15d5c7559eec3bae4d6cf1a76fda9f5be05a29c2830cff560cab0c0d","names":[],"sources":[]}, {"digest":"8620189c8e33582708ece48fd2d20dc4d57912486a17bdbb9aea4c44f7018062","names":[],"sources":[]}, + {"digest":"866f7b2dbdc5b421eab3e629312d1c839b75c259633b4b43b50d23bac44d92a0","names":[],"sources":[]}, {"digest":"86d1d0e9cfebb240421187e3dc37fb71cc38a1501885cf32f8f9adef7b0a2bde","names":["packages/subagent/subagent/src/descriptor.ts#SubagentDescriptorData"],"sources":["packages/subagent/subagent/src/descriptor.ts:89"]}, {"digest":"86e1e384495ee09ac2d60c9db3610f1f1fddac2d248f6f1d6000e78054680219","names":["packages/llm/llm/src/message.ts#ContextSnapshotSection"],"sources":["packages/llm/llm/src/message.ts:70"]}, {"digest":"871bfd6f7bb91061246768d3377f3fd58d6b8ea7c31f44c714ae7ab215650d62","names":[],"sources":[]}, @@ -425,6 +425,7 @@ {"digest":"8f6d609794bf5afc01d7ccf3e03811d32f4b8047f58a2c9a98db0154c2ad7cb9","names":[],"sources":[]}, {"digest":"8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543","names":[],"sources":["packages/core/tools/src/index.ts:28"]}, {"digest":"904bf499d98be4a3a5f836fcac533b6c4899569b018071e6604e1d32657d00fd","names":[],"sources":[]}, + {"digest":"920d87e22b7409bcc7224f8b1dc7f5101c33574c75627c65baab12b85cc96e75","names":[],"sources":[]}, {"digest":"930c1cd289def119d443e19dd90008916bf035f0a4d23e07241e7f050f2b89b7","names":["packages/interaction/user-approval/src/types.ts#ApprovalOutcome"],"sources":["packages/interaction/user-approval/src/types.ts:32"]}, {"digest":"935c8d486292b13c87b57ca280eb8c58b5b2f5239be86fcd4334c33fdeb87682","names":[],"sources":[]}, {"digest":"938f7c7357bc403ea3b281a9dd016939df7a0880fc8db12612585be574d0d615","names":[],"sources":[]}, @@ -434,6 +435,7 @@ {"digest":"94c56aeb1564ad735eb773d88636b40761d063efa1610f1308004d32923f5378","names":[],"sources":[]}, {"digest":"95080295115fbd83544520ba247b68a03790131f6a2eac323d388758cb542f65","names":[],"sources":["packages/llm/llm/src/types.ts:498"]}, {"digest":"95a3ffccbf3c8afcf9a2f3b355f724da8e07e98dc47ee741166dde7f9ea058bd","names":[],"sources":[]}, + {"digest":"95b5cf90ea1f57da3319184d6f5f3811d0f651aa9113591c10e81c3d1e9b9f70","names":[],"sources":[]}, {"digest":"95c0e88fcf0bafb2087f912ad9e2369de6ae6530cc0c4c2772c96205b57f7f60","names":[],"sources":[]}, {"digest":"9645cf93769a6dfb227bd400bd6cb9fa7bab9140f4a851e4ae92514ade6e3114","names":[],"sources":[]}, {"digest":"96bfb916a727cd4445a594df264c523fc039749a31bfed924b8cc7bb308a5bb0","names":[],"sources":[]}, @@ -471,7 +473,7 @@ {"digest":"a3a259a1aa5401af786ce08c4a8419681d3000d73467991b5fc563389bc4ebd2","names":[],"sources":[]}, {"digest":"a3c6f1ea38e9a61d7a9706057ba65aabe02dfe6d9dd90d4736aa62325d874531","names":[],"sources":[]}, {"digest":"a3edd1efd1fef21b72c55e213c8a8b24196b091abde2b89db52be0b2aaae3cfa","names":[],"sources":[]}, - {"digest":"a40d12070f6f4a124f32fb3cc86e7857554702e32f0eda3080a25c4b9ac9b18c","names":[],"sources":["packages/experimental/agent-team/src/types.ts:232"]}, + {"digest":"a40d12070f6f4a124f32fb3cc86e7857554702e32f0eda3080a25c4b9ac9b18c","names":[],"sources":["packages/team/agent-team/src/types.ts:175"]}, {"digest":"a4afec59b75aad5d13ef82d6931f35cc2d98918503916cf069355b77c5e42302","names":[],"sources":["packages/api/session-controller/src/types.ts:397"]}, {"digest":"a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc","names":[],"sources":[]}, {"digest":"a6b41f5bede8094d7bf635be5d69b1bd7a25e006ca9cf9415bceb9322f1a7feb","names":["packages/experimental/hook-protocol/src/types.ts#HookDialect"],"sources":["packages/experimental/hook-protocol/src/types.ts:48"]}, @@ -508,14 +510,15 @@ {"digest":"b0127044ab31a702bddfd785d345f5abd7a70876746e895ce443afa3e60ddf2d","names":[],"sources":[]}, {"digest":"b062078e899958f99ae464514578b8c093924dfe34bd9a835932b420ff2150b1","names":[],"sources":[]}, {"digest":"b0b0eb1b16490f6bee135b2377e8f8c39227974c799ad7d49ff418af23ecbc35","names":["packages/attachment/attachment/src/types.ts#ImageAttachmentRef"],"sources":["packages/attachment/attachment/src/types.ts:11"]}, + {"digest":"b120362fd216a16a40fe787f6d3286086aa131266322bab79c026a130ccd0f72","names":["packages/llm/llm/src/message.ts#MessageSource"],"sources":["packages/llm/llm/src/message.ts:136"]}, {"digest":"b158f3ff6923714807c0d73882f0b05dc87f8fa85c9e90814852d48bbc866081","names":["packages/core/session/src/types.ts#RequestContext"],"sources":["packages/core/session/src/types.ts:260"]}, {"digest":"b1664fa89f8eab49a14e7e3168d8bc4da4aa9fd76332f6c4a053d839e5276ae7","names":[],"sources":[]}, - {"digest":"b1ebd5cda47d90d980bb05bffc3f970c51537b9aff15b3bc3d639811727741b1","names":[],"sources":["packages/experimental/agent-team/src/types.ts:230"]}, + {"digest":"b1ebd5cda47d90d980bb05bffc3f970c51537b9aff15b3bc3d639811727741b1","names":[],"sources":["packages/team/agent-team/src/types.ts:173"]}, {"digest":"b1f62380f6746fedd629e71065e3583cef83334875b499ac9665c2eb87acfbf5","names":[],"sources":[]}, {"digest":"b222069eea2d768065161c1147b1f2c78c1b54328c84b3586ae5c8f91b8ed35e","names":[],"sources":[]}, {"digest":"b2245a906e13aa94f2d3cc6cf7b04cfa0267f7b7f4d468de1b00b1a314e54626","names":[],"sources":[]}, {"digest":"b2a5c8c008a31e3dc6c3013502f2356aca13894b31c8e70601fa92f2d40d1dba","names":[],"sources":[]}, - {"digest":"b2d2a01e51341a3bfec809198952c8c830a7899f4cb6ecd3c1dc0b5d9a2af943","names":["packages/experimental/agent-team/src/types.ts#TeamTaskStatus"],"sources":["packages/experimental/agent-team/src/types.ts:72"]}, + {"digest":"b2d2a01e51341a3bfec809198952c8c830a7899f4cb6ecd3c1dc0b5d9a2af943","names":["packages/team/agent-team/src/types.ts#TeamTaskStatus"],"sources":["packages/team/agent-team/src/types.ts:61"]}, {"digest":"b3681c026a0b98e98b103f057dfd217fbe904129c090efd668253ec9aea7fece","names":[],"sources":[]}, {"digest":"b38f206d59bad891fa58c89f97a0036987b3defe7685acbd318fa55eb85f9751","names":[],"sources":[]}, {"digest":"b4ec0af39fd52ed971990e42f04e5be3204882e555c409eb330f8238eec21569","names":[],"sources":[]}, @@ -558,6 +561,7 @@ {"digest":"c1f0f17b6493b85db6453bb30e01b208d703731b25bc1e68e5d4fc72eea9cdea","names":["packages/llm/llm/src/types.ts#ToolCallBlock"],"sources":["packages/llm/llm/src/types.ts:112"]}, {"digest":"c1f9e0405de6d18cabb9ee70782a027f9bbdc57e5abec9dcccdd56119e2e9058","names":[],"sources":[]}, {"digest":"c219efbdfd53b836786202ab4532913d555798f0a8615a4e9629736e3799fb70","names":[],"sources":[]}, + {"digest":"c2a13242efc3bea4b36a843ddf1524ff4d33c765a71b582fa1fdbfa4fa148434","names":[],"sources":["packages/team/agent-team/src/types.ts:171"]}, {"digest":"c2deb7c9183735d4e0fffddf5c623f4e34daa1c8e96b111982e957dfcbbcf896","names":["packages/workflow/workflow/src/types.ts#WorkflowAgentOutcome"],"sources":["packages/workflow/workflow/src/types.ts:110"]}, {"digest":"c2f49004d9ae38a964e80850c87bc6849d6a872091a1d43e99eedf00c641e2a3","names":[],"sources":[]}, {"digest":"c43b119ce579b3e9983a0d3adc4ba402ebb9ba773d89738a2dab01ecf5813943","names":[],"sources":[]}, @@ -567,7 +571,6 @@ {"digest":"c589e938cc0e4989df8b82abef6339de621f0dc4fe62c230d145b94996af7f92","names":[],"sources":[]}, {"digest":"c592ce75aab73fcab19c1d7845684c72cf402b78d2e1f2833a58ecf9f3598ed6","names":["packages/util/values/src/index.ts#JsonValue"],"sources":["packages/util/values/src/index.ts:4"]}, {"digest":"c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262","names":[],"sources":[]}, - {"digest":"c78b9097bfae83cd08f0a98a92730236ba80ac61d712046959e4fc7f28aa1f36","names":["packages/llm/llm/src/message.ts#Message"],"sources":["packages/llm/llm/src/message.ts:196"]}, {"digest":"c8e6f48878cc2a2f0dc59682280d81f407943fb503f32c64bf9ff3d9393c3eff","names":[],"sources":[]}, {"digest":"c99e20441bf15341e9db54385e558e5c03d21dff2eb1ac08ccbf47c2d8747954","names":[],"sources":[]}, {"digest":"ca085dba2c877feceebf3c62f9d39c6c2a4d2314d97b94b3927a780fe8fe2bb2","names":[],"sources":[]}, @@ -579,21 +582,21 @@ {"digest":"cda2345a9be9d139df49ccbd16dcba716f3440178bf20ff8b801fda1f5eb77dc","names":[],"sources":[]}, {"digest":"cdca80b8dad24d4e7b1d5a288a8475ba8d8d46be385bd2da85f122ee836f1670","names":[],"sources":[]}, {"digest":"cdee10e87dbed8440d8b6935888624884878efc7af95402e94171b29f173dced","names":[],"sources":["packages/llm/llm-retry/src/types.ts:17"]}, - {"digest":"cdf988172b589b230f2bce868489def1bb575eb0501076ac757a8f369ac5c64e","names":[],"sources":[]}, {"digest":"cef171adf021a1867d244618a3dbbaafc22beb17c7f3df3ee203393271c8c6f6","names":["packages/core/tool-executor-ptc/src/types.ts#PtcDispatchEventData"],"sources":["packages/core/tool-executor-ptc/src/types.ts:21"]}, {"digest":"cffcdc19881ab61ec0a9ce18743996f7ac44ac8889f34dfac36ae745574aa22f","names":["packages/goal/goal/src/domain.ts#GoalClearChangeMeta"],"sources":["packages/goal/goal/src/domain.ts:35"]}, {"digest":"d0fbb0ca1bbba059830826f33f801da1aced35a2504376d61e020bdfec8d50c0","names":[],"sources":[]}, {"digest":"d28765f87d6b6ab408e0b2f3e7e297791b004b806d8e302c49903ebae815feb2","names":[],"sources":[]}, {"digest":"d4b739491689cc6bb13d07ffac1a32239875abda364c0d5edd8f74faf5e9c00d","names":[],"sources":["packages/llm/llm/src/types.ts:501"]}, {"digest":"d4c799e3d51cae1040362ba4968efc484739e14416c9c1df20c2f108b4ef2eb5","names":[],"sources":[]}, - {"digest":"d4d960185026266ed7bd127be1e252234689e8b4890c7c2677a644ba58f35401","names":[],"sources":["packages/core/agent/src/types.ts:110"]}, {"digest":"d4e4d575abeb4b72d616dc65c17ac0eaa943cc135609395044c38d5afcb10e31","names":[],"sources":[]}, {"digest":"d595ec73b32b016a6055333c67a5d646032b22e672da1d4a09c5b1ae398a093a","names":[],"sources":[]}, {"digest":"d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9","names":[],"sources":[]}, {"digest":"d66e11dddf5dfa3c71547e2cbd7b0608824cd8d6243fc16f532017ff28e8fba5","names":[],"sources":[]}, {"digest":"d6e654b88b584fcdf290a56802b1730bc7b9847ae86baa4265ab7769eb48990c","names":[],"sources":[]}, {"digest":"d85590a5675c6e2fd8164e5792a6cceac714605b8ac228adae5ee668facb1490","names":[],"sources":[]}, + {"digest":"d8a476d2c6fd30df6ec0e799f91716d1518c780d23b4121dce545abfcfb7c5a5","names":[],"sources":[]}, {"digest":"d9196086e41e4dd5c2ce87f5c25529c8112a1731d258cf0728747d41a9ee3241","names":[],"sources":[]}, + {"digest":"d95c624eddecdacf1ee74356196c09d2f3fcd625714be5e852a6b643fa12a7ca","names":[],"sources":[]}, {"digest":"d9c462b9f62fdd3efc76a44b9524dc8781f6b91cd9c7e584b17d971defafebab","names":["packages/llm/llm/src/message.ts#ToolMessageSource"],"sources":["packages/llm/llm/src/message.ts:29"]}, {"digest":"d9edf1862e4471d6ad8a106a3f4160e0643874368707d50ecd8e27e5ee7a2567","names":[],"sources":[]}, {"digest":"da60ed58a8fa23d7aa4920ab5c1f4e3d28109cb0797ef9e5c8b125721b847582","names":[],"sources":["packages/session/working-directory/src/index.ts:36"]}, @@ -603,10 +606,12 @@ {"digest":"db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965","names":[],"sources":[]}, {"digest":"dba54c09c6e7aafc9acbed7792ecc920d99f92d0784aeeed10af55f7fe0bc8cd","names":[],"sources":["packages/core/tool-executor-ptc/src/types.ts:68"]}, {"digest":"dbdafceb9a900183cccc80596498d386f3bf7bddead2b8cfde68453aa4739edc","names":[],"sources":[]}, + {"digest":"dcb07e04496586d2664d72dab0e94312917da56927b2a0dc3ee53d6a33e32450","names":[],"sources":[]}, {"digest":"dd00cdccd0ba487b92362ef67db58cd264f48c1bfd368291424a1da9a899489a","names":[],"sources":[]}, {"digest":"dd2780e7e07684332f7a9ab047237798d260e7eaffb1321cb38eca695d184c3d","names":[],"sources":[]}, {"digest":"dd5ee8762bd8a14b6c6bffedd1753e4e78505bb0f9ea1621018d03a54eef59d5","names":[],"sources":[]}, {"digest":"dd6cc2bb2a8f28248347021ad93565360b196a042360b4cd16b569d3a2a91f2d","names":[],"sources":[]}, + {"digest":"ddce54a3873dc57113b0cd38ad9b23b3c259186139e8847e5e5f415d86f52482","names":[],"sources":[]}, {"digest":"dde0cdf9d426768b649b5a9783d2144e7f900c35ef65229cd89036dbf411d1b0","names":[],"sources":[]}, {"digest":"de25352d2dc4196309bd84b5bac6319f6d5c898ea0e82bb5ae66c5738d0d426f","names":[],"sources":[]}, {"digest":"de30b1fa12c75f81df6aa4096e1595296d2d04e2be14a7c29849cf8028b79048","names":[],"sources":[]}, @@ -614,8 +619,10 @@ {"digest":"deebfc7c62177ea840f23c6362b8615bc78ffa93377dd5d243c04e541c413af5","names":[],"sources":["packages/typert/protocol/src/types.ts:457"]}, {"digest":"df5d9709b7db2a72510dc52052a23f0c38f39730af1504bab8bccabc14fae528","names":[],"sources":[]}, {"digest":"df843efd87c78cf8de98f8e0d0216956cd568d0d0e56a22c853663e274aa1a18","names":[],"sources":[]}, - {"digest":"dfce5db4aa42b7ebe0bb79eca92553587287e18c06b9eba85cbfb9d332e26acd","names":["packages/experimental/agent-team/src/types.ts#TeamMemberPhase"],"sources":["packages/experimental/agent-team/src/types.ts:45"]}, + {"digest":"dfce5db4aa42b7ebe0bb79eca92553587287e18c06b9eba85cbfb9d332e26acd","names":["packages/team/agent-team/src/types.ts#TeamMemberPhase"],"sources":["packages/team/agent-team/src/types.ts:47"]}, {"digest":"e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05","names":[],"sources":[]}, + {"digest":"e1880c67e735e228f3555df8a55a492db2c0b0c71bc8859e938e184a46c7db51","names":[],"sources":["packages/core/agent/src/types.ts:110"]}, + {"digest":"e272493e3ea0b33b2e1d64b6a80e9d8bd3d1d466cc115f2facbdd598f8644a39","names":["packages/llm/llm/src/message.ts#Message"],"sources":["packages/llm/llm/src/message.ts:196"]}, {"digest":"e2ad8f5bcd813f268ffef1b10a12d7ddd2b8ced8b9b6d0275b3f6b7ed094a187","names":[],"sources":[]}, {"digest":"e2d0367e5716bc188435e51718ebff8ce873190625fa93b1c5331102d98758fa","names":[],"sources":[]}, {"digest":"e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602","names":[],"sources":[]}, @@ -640,7 +647,7 @@ {"digest":"e7f4cbb3a57f4cbdb61ea41e95bcba626ad1fff6bb9b1e063b1d50a32da6e864","names":[],"sources":[]}, {"digest":"e8de0f457fed5b9ff94dc4950b59ef75fd8ba8af665cc7899485924a62653367","names":[],"sources":[]}, {"digest":"e95f957154b2b1b58692af53d4131180b6dd19e2264869f876dbaa959991494e","names":[],"sources":[]}, - {"digest":"ea08ec1df0c33108fd03345b9af199cf04dea450aa045d3c683f216311403403","names":[],"sources":["packages/experimental/agent-team/src/types.ts:234"]}, + {"digest":"ea08ec1df0c33108fd03345b9af199cf04dea450aa045d3c683f216311403403","names":[],"sources":["packages/team/agent-team/src/types.ts:177"]}, {"digest":"eaa71be0fbf0f702df8ea6c11d0aa2f9782263d45a10d0fb14eda662f4bfe04c","names":[],"sources":["packages/core/session/src/types.ts:201"]}, {"digest":"ebb768d85ec87fb68e42fc14fd7d8535b5ac11791b14d04c6770180f0a719bf8","names":[],"sources":[]}, {"digest":"ec38b5949af8eacaf00df002f4acbe344f934f8a061e9cdc65a52a48e5f6dd93","names":[],"sources":[]}, @@ -692,11 +699,11 @@ {"digest":"fe1a7acdeed4d0f5ab483396eaa5b3dc97263b7c10eb4da2f0028cb01728bfc9","names":[],"sources":[]}, {"digest":"fe792a6cd430494503837e21e79f91677704ccdd9e870c679aabd7d43ed47ddd","names":[],"sources":[]}, {"digest":"fec5442b9ee4122e10514d81d5a43648003fe8bfb7ee2ba50c0cdbdd383ed394","names":["packages/llm/llm/src/types.ts#TokenUsage"],"sources":["packages/llm/llm/src/types.ts:182"]}, - {"digest":"fee1caf8ab6a65c1bc981f5a1730164490f22a2a89ffa3d0b76f1f432c44c8b5","names":[],"sources":[]}, - {"digest":"ff2cbb53582d1827b02622482246979942f59e07e0393b76a161251423549e72","names":[],"sources":[]} + {"digest":"fee1caf8ab6a65c1bc981f5a1730164490f22a2a89ffa3d0b76f1f432c44c8b5","names":[],"sources":[]} ], "nodes": { "0116b1f89c05221d": {"kind":"literal","value":"web_search_20250305"}, + "01c161379a3c3394": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"589d0d91239681f8","optional":false},{"name":"source","type":"b120362fd216a16a","optional":false,"compatibility":{"version":1,"policy":"session-source-attribution","binding":"session.developer-message.source","discriminator":"kind","unknownKinds":"preserve","attributionKinds":["action-cards","tmux-context","user-question-reply","working-directory"]}}],"indices":[]}, "0203243b72fc867a": {"kind":"object","properties":[{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, "022623b9ef98ec47": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false}],"indices":[]}, "023a355b2be0d688": {"kind":"object","properties":[{"name":"kind","type":"254dce209e735f9c","optional":false}],"indices":[]}, @@ -811,7 +818,6 @@ "2da725e554cd8930": {"kind":"literal","value":"fork"}, "2e3523541e942ef6": {"kind":"union","types":["18dc21f12bbac8ad","8e8e91646a5cae3f","e6063d0451dc0202","eea687d437880bd5"]}, "2e4debca12f2c449": {"kind":"literal","value":"team-message"}, - "2e505098f9209e52": {"kind":"object","properties":[{"name":"data","type":"7ff62f6599c083ce","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"587bb11da730e06a","optional":false}],"indices":[]}, "2eafefdc6f79a492": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"senderId","type":"4cd48f3e5108bd6e","optional":false},{"name":"senderName","type":"4cd48f3e5108bd6e","optional":false},{"name":"targetId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "2f1e6a7ebace43d7": {"kind":"union","types":["3f74ba74acd10644","58f94390b982f8a5","de30b1fa12c75f81","ebb768d85ec87fb6"]}, "2f3f201c8846e19c": {"kind":"literal","value":"tool-goal"}, @@ -854,7 +860,6 @@ "3ee93b06f3a12585": {"kind":"object","properties":[{"name":"data","type":"7c6129b6961cf4b2","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"44818000e56acb97","optional":false}],"indices":[]}, "3f74ba74acd10644": {"kind":"literal","value":"image/jpeg"}, "3fdf9582c3b5597d": {"kind":"object","properties":[{"name":"afterSeconds","type":"0657d3ca5ac4e7bf","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"5b232a9b9bfcc0fc","optional":false},{"name":"prompt","type":"4cd48f3e5108bd6e","optional":false},{"name":"scheduledAt","type":"4cd48f3e5108bd6e","optional":false},{"name":"title","type":"4cd48f3e5108bd6e","optional":true}],"indices":[]}, - "4035903993dfee62": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"68f44f59e91c4139","optional":false},{"name":"source","type":"7e9f1072c17f7aa8","optional":false,"compatibility":{"version":1,"policy":"session-source-attribution","binding":"session.user-message.source","discriminator":"kind","unknownKinds":"preserve","attributionKinds":["action-cards","tmux-context","user-question-reply","working-directory"]}}],"indices":[]}, "408927267422a7b2": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, "4208123b50df5006": {"kind":"object","properties":[{"name":"data","type":"ad0c38a0adb82a94","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"3e0eff5946255ff0","optional":false}],"indices":[]}, "42e0916e0dda5f6d": {"kind":"object","properties":[{"name":"data","type":"7f9365bae9fcb73e","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"fe792a6cd4304945","optional":false}],"indices":[]}, @@ -873,6 +878,7 @@ "48f9c19417a1abbe": {"kind":"object","properties":[{"name":"data","type":"0bcfaf5566d27102","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"a8a2b9011fc06786","optional":false}],"indices":[]}, "4991dfc415c9efeb": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false}],"indices":[]}, "49dab019d00e8c26": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "4a561c111a78d452": {"kind":"literal","value":"refusal"}, "4a9bef9f27a94252": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"82470020b5bfcb1a","optional":false},{"name":"senderSessionId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "4ae007ed190a72c9": {"kind":"literal","value":"llm/retry-started"}, "4aed17ec726b5397": {"kind":"tuple","elements":[{"type":"2d11ca7b0d4493e2","optional":false,"rest":false}]}, @@ -885,7 +891,6 @@ "4e011753f8263f43": {"kind":"literal","value":"team/message/queued"}, "4e44c9118152f682": {"kind":"literal","value":"text"}, "4e88776858652856": {"kind":"object","properties":[{"name":"text","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"4e44c9118152f682","optional":false}],"indices":[]}, - "4e97c3d85c0fc817": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"bf16d2e98a7f1389","optional":false},{"name":"senderSessionId","type":"4cd48f3e5108bd6e","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "4edec1b770e8ec4c": {"kind":"literal","value":"ptc-mode"}, "4f507598cf476214": {"kind":"literal","value":"series"}, "4f91f187baeabef5": {"kind":"object","properties":[{"name":"capturedFormatVersion","type":"0657d3ca5ac4e7bf","optional":true},{"name":"capturedThroughSeq","type":"3bd652ebfa8726b3","optional":false},{"name":"compacted","type":"aa93010d0e521ad7","optional":false},{"name":"inputIndex","type":"0657d3ca5ac4e7bf","optional":false},{"name":"label","type":"4cd48f3e5108bd6e","optional":false},{"name":"omittedBytes","type":"0657d3ca5ac4e7bf","optional":false},{"name":"omittedMessages","type":"0657d3ca5ac4e7bf","optional":false},{"name":"originalMessages","type":"0657d3ca5ac4e7bf","optional":false},{"name":"retainedMessages","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sessionId","type":"4cd48f3e5108bd6e","optional":false},{"name":"truncated","type":"aa93010d0e521ad7","optional":false}],"indices":[]}, @@ -901,7 +906,6 @@ "530da6c0267813bb": {"kind":"union","types":["34d1ab5c5df37818","4f507598cf476214","e7ce49f12953583f","f789af5ab844827e"]}, "53750b9abf61efb7": {"kind":"object","properties":[{"name":"data","type":"f731ffe80810b028","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"44c8c77aa9aa100b","optional":false}],"indices":[]}, "53cf1704ae0709c7": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false}],"indices":[]}, - "53d4f8256bc58109": {"kind":"object","properties":[{"name":"headerSeq","type":"0657d3ca5ac4e7bf","optional":true},{"name":"message","type":"64ef542b1bfc9883","optional":false},{"name":"step","type":"0657d3ca5ac4e7bf","optional":false},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "54f9276a1617d7c1": {"kind":"object","properties":[{"name":"step","type":"0657d3ca5ac4e7bf","optional":false},{"name":"stream","type":"84bbfdce7d2eab5b","optional":false},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "557e148d30b79ee4": {"kind":"literal","value":"agent-preset/selected"}, "5654685d7557ec74": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, @@ -918,7 +922,6 @@ "5abab3a27fd27366": {"kind":"literal","value":"text-delta"}, "5b232a9b9bfcc0fc": {"kind":"literal","value":"after"}, "5b6fb1f226ff5640": {"kind":"object","properties":[{"name":"commandId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"1ab2ef1296f157ba","optional":false},{"name":"sourceEventSeq","type":"0657d3ca5ac4e7bf","optional":true},{"name":"text","type":"4cd48f3e5108bd6e","optional":true}],"indices":[]}, - "5bb231bc5afdb532": {"kind":"object","properties":[{"name":"data","type":"4035903993dfee62","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"de25352d2dc41963","optional":false}],"indices":[]}, "5bb69b88517f6214": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "5bc66269cfaf614c": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false}],"indices":[]}, "5bd310104cfe448e": {"kind":"literal","value":"resource-cost"}, @@ -935,15 +938,17 @@ "5f2117d1856a5ab4": {"kind":"object","properties":[{"name":"code","type":"4cd48f3e5108bd6e","optional":false},{"name":"message","type":"4cd48f3e5108bd6e","optional":false},{"name":"offloadImages","type":"0657d3ca5ac4e7bf","optional":true},{"name":"providerRetryAfterMs","type":"0657d3ca5ac4e7bf","optional":true},{"name":"requestId","type":"4cd48f3e5108bd6e","optional":true},{"name":"status","type":"0657d3ca5ac4e7bf","optional":true}],"indices":[]}, "5f26a6c20b37632f": {"kind":"object","properties":[{"name":"data","type":"9a410ddf56792b2b","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"ad92f473a03ae725","optional":false}],"indices":[]}, "5f758f3de5f00c18": {"kind":"object","properties":[{"name":"label","type":"4cd48f3e5108bd6e","optional":true},{"name":"mode","type":"d28765f87d6b6ab4","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false},{"name":"version","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "601327bedca5f98a": {"kind":"object","properties":[{"name":"maxTokens","type":"0657d3ca5ac4e7bf","optional":false},{"name":"messageSeqs","type":"5d03ba38734809bc","optional":false},{"name":"messages","type":"95b5cf90ea1f57da","optional":false},{"name":"reasoningEffort","type":"4cd48f3e5108bd6e","optional":true},{"name":"route","type":"07e1a58c58b593f5","optional":false},{"name":"system","type":"4cd48f3e5108bd6e","optional":false},{"name":"titleProvider","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "6085488d223aeffd": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false}],"indices":[]}, "611cae9a3539733a": {"kind":"literal","value":"llm/retry"}, "611d82d2dc50df70": {"kind":"object","properties":[{"name":"cause","type":"750a2b129d67704d","optional":false},{"name":"detail","type":"4cd48f3e5108bd6e","optional":true},{"name":"finishedAt","type":"0657d3ca5ac4e7bf","optional":false},{"name":"jobId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"4cd48f3e5108bd6e","optional":false},{"name":"label","type":"4cd48f3e5108bd6e","optional":false},{"name":"owner","type":"4cd48f3e5108bd6e","optional":false},{"name":"producerReported","type":"aa93010d0e521ad7","optional":false},{"name":"startedAt","type":"0657d3ca5ac4e7bf","optional":false},{"name":"status","type":"c2f49004d9ae38a9","optional":false},{"name":"toolCall","type":"39cf2d3ff8bac81c","optional":true}],"indices":[]}, + "613660455704fb82": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"68f44f59e91c4139","optional":false},{"name":"source","type":"b120362fd216a16a","optional":false,"compatibility":{"version":1,"policy":"session-source-attribution","binding":"session.user-message.source","discriminator":"kind","unknownKinds":"preserve","attributionKinds":["action-cards","tmux-context","user-question-reply","working-directory"]}}],"indices":[]}, "616f93a5047de668": {"kind":"literal","value":"tool-call-chunks"}, "6292bcbedaa97fea": {"kind":"object","properties":[{"name":"kind","type":"f68c335306535a00","optional":false}],"indices":[]}, "62e6a429ff3f390a": {"kind":"union","types":["1d8798b1a5e8c545","1e346446e183ee76","34d1ab5c5df37818","7624ca28c61ca0ea","ad22a5ca5bbaab2e","d9196086e41e4dd5"]}, "62ecb62dc0fe1b5e": {"kind":"literal","value":"next-turn"}, "6487b10ba7542f3c": {"kind":"object","properties":[{"name":"agentModel","type":"4cd48f3e5108bd6e","optional":true},{"name":"agentProvider","type":"4cd48f3e5108bd6e","optional":true},{"name":"agentReasoningEffort","type":"4cd48f3e5108bd6e","optional":true},{"name":"label","type":"4cd48f3e5108bd6e","optional":false},{"name":"mode","type":"e5d3828df1ec3e2a","optional":false},{"name":"persona","type":"4cd48f3e5108bd6e","optional":true},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false},{"name":"toolFilter","type":"3cfc3a56502da1f8","optional":true},{"name":"version","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, - "64ef542b1bfc9883": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"589d0d91239681f8","optional":false},{"name":"source","type":"7e9f1072c17f7aa8","optional":false,"compatibility":{"version":1,"policy":"session-source-attribution","binding":"session.developer-message.source","discriminator":"kind","unknownKinds":"preserve","attributionKinds":["action-cards","tmux-context","user-question-reply","working-directory"]}}],"indices":[]}, + "65377a76c6a58f78": {"kind":"object","properties":[{"name":"headerSeq","type":"0657d3ca5ac4e7bf","optional":true},{"name":"message","type":"01c161379a3c3394","optional":false},{"name":"step","type":"0657d3ca5ac4e7bf","optional":false},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "6598cd6da253948d": {"kind":"object","properties":[{"name":"context","type":"50cf02d875f8a1f1","optional":false},{"name":"description","type":"4cd48f3e5108bd6e","optional":false},{"name":"error","type":"4cd48f3e5108bd6e","optional":true},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false},{"name":"phase","type":"dfce5db4aa42b7eb","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "65a8400e451e667e": {"kind":"object","properties":[{"name":"kind","type":"09655b34d7f3cf1e","optional":false}],"indices":[]}, "65d583e66518c523": {"kind":"object","properties":[{"name":"entries","type":"aba175fb20ffc711","optional":false},{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"incomplete","type":"dacf3d5f3e4ff619","optional":true},{"name":"kind","type":"6ebcc2dbec9f2dab","optional":false},{"name":"update","type":"dacf3d5f3e4ff619","optional":true}],"indices":[]}, @@ -953,6 +958,7 @@ "68141cbe3994a5ff": {"kind":"literal","value":"max-tokens"}, "688e853dc149f0d3": {"kind":"literal","value":"reasoning-chunks"}, "68f44f59e91c4139": {"kind":"literal","value":"user"}, + "690583582246babf": {"kind":"array","element":"613660455704fb82"}, "69081694be231d56": {"kind":"object","properties":[{"name":"data","type":"6bc8e51124d0874b","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"1c5a58f4599bd4a9","optional":false}],"indices":[]}, "693f4952fb5565da": {"kind":"object","properties":[{"name":"failure","type":"5f2117d1856a5ab4","optional":false},{"name":"kind","type":"a2be0f0cb59f0d4a","optional":false}],"indices":[]}, "69d238a6e9b08d67": {"kind":"literal","value":"interrupted"}, @@ -977,7 +983,7 @@ "6f72ece8f317cd08": {"kind":"object","properties":[{"name":"retry","type":"0657d3ca5ac4e7bf","optional":false},{"name":"retryId","type":"4cd48f3e5108bd6e","optional":false},{"name":"step","type":"0657d3ca5ac4e7bf","optional":false},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "6f9e00383836275e": {"kind":"literal","value":"snapshot"}, "70b082fe78991edf": {"kind":"literal","value":"request/context"}, - "70d2acdec1ebcfea": {"kind":"object","properties":[{"name":"data","type":"53d4f8256bc58109","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"1379f0b72275994d","optional":false}],"indices":[]}, + "70ea8e7943c5e2d7": {"kind":"object","properties":[{"name":"data","type":"65377a76c6a58f78","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"1379f0b72275994d","optional":false}],"indices":[]}, "7180844e1066230c": {"kind":"literal","value":"service-stability"}, "719c51f67074f970": {"kind":"object","properties":[{"name":"kind","type":"2765e9119edf5816","optional":false},{"name":"model","type":"4cd48f3e5108bd6e","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false},{"name":"replayState","type":"bbc5696e3f13c758","optional":true}],"indices":[]}, "71bb3254ec8f1e49": {"kind":"object","properties":[{"name":"dt","type":"5d03ba38734809bc","optional":false},{"name":"index","type":"0657d3ca5ac4e7bf","optional":false},{"name":"texts","type":"93c33d9687613293","optional":false},{"name":"time0","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"688e853dc149f0d3","optional":false}],"indices":[]}, @@ -995,20 +1001,17 @@ "7a651f3b672c52fb": {"kind":"literal","value":"session/title"}, "7af85bf70d4eafce": {"kind":"object","properties":[{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "7b1ce60193fa8b69": {"kind":"literal","value":"working-directory"}, - "7c203f3d4dfa6638": {"kind":"array","element":"4035903993dfee62"}, "7c6129b6961cf4b2": {"kind":"object","properties":[{"name":"messageId","type":"4cd48f3e5108bd6e","optional":false},{"name":"sessionId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "7c9f44e90a0058f4": {"kind":"object","properties":[{"name":"data","type":"5302e76315127761","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"570e850224346133","optional":false}],"indices":[]}, "7d226b9fd95b4ba2": {"kind":"union","types":["1df34a6c086866ff","cffcdc19881ab61e"]}, "7d54a5a5e629e2e7": {"kind":"object","properties":[{"name":"category","type":"3305a4bb6b6c29b4","optional":true},{"name":"text","type":"4cd48f3e5108bd6e","optional":true}],"indices":[]}, "7d65acf0a3a43a8f": {"kind":"union","types":["1f0133217eeeffd8","277b61f9c6f81704","3c2535ca0a2062f1","4e44c9118152f682","83d6916021eede8f","85f9b505b7bdff1f","fb8b6a2e53cb9cf4"]}, "7df62978a2b7de85": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false}],"indices":[]}, - "7e9f1072c17f7aa8": {"kind":"union","types":["0203243b72fc867a","022623b9ef98ec47","02b2c23f2d94b028","0ed8d1108fe7a5e1","0eeb17e1df89b1eb","11638dc58a31496f","1938c8c20f1cba7c","1e709346005826eb","1fdad624334d3548","2152caf1a71e588e","2235d957bf4b7c6c","224aa11d4d5f88c4","24ce31aa528272e5","260523b412c19ad8","28936c614bd4e7e2","290f32bfd81ff85a","297fbd8067d7815a","2aad1179f9576314","312aee2f0c42aabb","31907de956375182","3247e566f4340029","3b04790e08b35241","3b4f2c9ef2c1217e","3bdb8dc6989655b0","408927267422a7b2","45f005e6bbb76452","4991dfc415c9efeb","49dab019d00e8c26","4a9bef9f27a94252","4e97c3d85c0fc817","52c6113cb0e53f34","53cf1704ae0709c7","5654685d7557ec74","58015da1c1950a66","5bb69b88517f6214","5bc66269cfaf614c","5cdbf342b7748d15","5d7a64ed6aac5551","6085488d223aeffd","6292bcbedaa97fea","65a8400e451e667e","65d583e66518c523","672c4aa2a9b736e3","6ccb6bc423fb3952","6ea608bdc8a7a63d","6f22a9d02b9a1f11","6f44f32be9bcd9a8","719c51f67074f970","7554565be416b034","756b5e23a5cb2c7f","7df62978a2b7de85","7eaef7a763ba5b94","7f959241c24128b4","80677c8c8b804189","825ca23416b1ea9d","8338ee303e2bd448","854db3efdfb0cd39","85fe96bf15d5c755","8620189c8e335827","87b1a33564b9a4d6","88841202942c7683","8a046b52331959aa","8c1c4f72755a78ea","8c627d7a45fa406c","8f324cea3916c051","8fcb0b44a575e998","96bfb916a727cd44","99135d8a39379779","99f0e98184709ad1","9edc162949abf93b","9f65c43e340bba31","a15a19ef63cf5d1b","a16e567c8cd2c55c","a2066bd52f61218b","a3c6f1ea38e9a61d","a4afec59b75aad5d","aa2bf9e82b9fdeb9","aae343bc3d663c83","ada310bf0bdb8fed","ade057f4fdc387ae","b062078e899958f9","b2a5c8c008a31e3d","b3681c026a0b98e9","b4ec0af39fd52ed9","b613f83572fe2fbf","b6f94c17dd9bbd7c","b70f3e81fe11b7a5","b79641a9d25e7566","ba2a1a8f194b1bb2","ba4c40b4381e0dcd","baccc97ec34be6c4","c5228b7a55498d84","c55827fba7018198","cb1c8959e1726f82","cda2345a9be9d139","d85590a5675c6e2f","d9c462b9f62fdd3e","db06ef35b27a3842","dba54c09c6e7aafc","dd00cdccd0ba487b","dd5ee8762bd8a14b","dd6cc2bb2a8f2824","dde0cdf9d426768b","deaae5cd30987d4f","e7f4cbb3a57f4cbd","edbbfff1128099fd","ee577a0cc18664c9","f230c0b583e23b25","f29d63336e8266d8","f4a66f654996c093","f56c5b59ca016131","f6329fb30c7ea801","f6605d6c57933aea","f6afe41e3926f422","f7283223aacdc39b","f97f47b0eb87905e","fcf81d3c58c13593","fe1a7acdeed4d0f5"]}, "7eaef7a763ba5b94": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false}],"indices":[]}, "7f13860ee89a2d35": {"kind":"literal","value":"session-reference"}, "7f7fd5a6b0064f59": {"kind":"object","properties":[{"name":"data","type":"5dd766ccd1702a11","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"10e386801903f9d4","optional":false}],"indices":[]}, "7f9365bae9fcb73e": {"kind":"object","properties":[{"name":"runId","type":"4cd48f3e5108bd6e","optional":false},{"name":"stopReason","type":"8465ece5e9d9007c","optional":false}],"indices":[]}, "7f959241c24128b4": {"kind":"object","properties":[{"name":"goalId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"b815d7b91cdd9fc6","optional":false},{"name":"revision","type":"0657d3ca5ac4e7bf","optional":false},{"name":"round","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, - "7ff62f6599c083ce": {"kind":"object","properties":[{"name":"maxTokens","type":"0657d3ca5ac4e7bf","optional":false},{"name":"messageSeqs","type":"5d03ba38734809bc","optional":false},{"name":"messages","type":"ff2cbb53582d1827","optional":false},{"name":"reasoningEffort","type":"4cd48f3e5108bd6e","optional":true},{"name":"route","type":"07e1a58c58b593f5","optional":false},{"name":"system","type":"4cd48f3e5108bd6e","optional":false},{"name":"titleProvider","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "8064c03c87c357fd": {"kind":"union","types":["12d5808fac1996b9","43e311235aa537df","4e88776858652856","814d9434e1ddf907","8e611bf46821e500","c1f0f17b6493b85d","f2da0f27c1e6add5"]}, "80677c8c8b804189": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false}],"indices":[]}, "8067b0d232c350f1": {"kind":"literal","value":"time-context"}, @@ -1023,6 +1026,7 @@ "83d6916021eede8f": {"kind":"literal","value":"tool-removal"}, "83da72c6857601c7": {"kind":"object","properties":[{"name":"action","type":"904bf499d98be4a3","optional":false},{"name":"digest","type":"4cd48f3e5108bd6e","optional":true},{"name":"path","type":"4cd48f3e5108bd6e","optional":false},{"name":"scope","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "8465ece5e9d9007c": {"kind":"union","types":["1ebc6f9ff3aa9e3a","2142e391b9ccb60d","a2be0f0cb59f0d4a"]}, + "848639f8787f035b": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"bf16d2e98a7f1389","optional":false},{"name":"senderSessionId","type":"4cd48f3e5108bd6e","optional":false},{"name":"stopReason","type":"866f7b2dbdc5b421","optional":true},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "84ac8c1a66ffc697": {"kind":"object","properties":[{"name":"code","type":"4cd48f3e5108bd6e","optional":false},{"name":"message","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "84bbfdce7d2eab5b": {"kind":"array","element":"33341aac09312300"}, "854db3efdfb0cd39": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"bb94f3217aa56c88","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, @@ -1030,6 +1034,7 @@ "85f9b505b7bdff1f": {"kind":"literal","value":"tool-addition"}, "85fe96bf15d5c755": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"8067b0d232c350f1","optional":false}],"indices":[]}, "8620189c8e335827": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false}],"indices":[]}, + "866f7b2dbdc5b421": {"kind":"union","types":["1e0b0b66b6f0c53f","1ebc6f9ff3aa9e3a","4a561c111a78d452","68141cbe3994a5ff","a2be0f0cb59f0d4a"]}, "86d1d0e9cfebb240": {"kind":"union","types":["5f758f3de5f00c18","6487b10ba7542f3c"]}, "86e1e384495ee09a": {"kind":"object","properties":[{"name":"name","type":"4cd48f3e5108bd6e","optional":false},{"name":"text","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "871bfd6f7bb91061": {"kind":"literal","value":"provisioning"}, @@ -1053,6 +1058,7 @@ "8f6d609794bf5afc": {"kind":"tuple","elements":[{"type":"4e88776858652856","optional":false,"rest":false}]}, "8fcb0b44a575e998": {"kind":"object","properties":[{"name":"kind","type":"a720757f704a2512","optional":false}],"indices":[]}, "904bf499d98be4a3": {"kind":"union","types":["7382c4fc95fe8c5d","b765ee31033cea46","f6cb462f808309e3"]}, + "920d87e22b7409bc": {"kind":"object","properties":[{"name":"data","type":"c2a13242efc3bea4","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"d8a476d2c6fd30df","optional":false}],"indices":[]}, "930c1cd289def119": {"kind":"union","types":["2142e391b9ccb60d","2c4c21f19f426a58","811229eedcec5797","fab35c4520883c0d"]}, "935c8d486292b13c": {"kind":"literal","value":"web_search"}, "938f7c7357bc403e": {"kind":"primitive","type":"null"}, @@ -1062,6 +1068,7 @@ "94c56aeb1564ad73": {"kind":"literal","value":"in_progress"}, "95080295115fbd83": {"kind":"object","properties":[{"name":"index","type":"0657d3ca5ac4e7bf","optional":false},{"name":"text","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"5abab3a27fd27366","optional":false}],"indices":[]}, "95a3ffccbf3c8afc": {"kind":"literal","value":"owner"}, + "95b5cf90ea1f57da": {"kind":"array","element":"e272493e3ea0b33b"}, "95c0e88fcf0bafb2": {"kind":"literal","value":"assistant"}, "9645cf93769a6dfb": {"kind":"literal","value":"user-question-reply"}, "96bfb916a727cd44": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, @@ -1136,6 +1143,7 @@ "b0127044ab31a702": {"kind":"object","properties":[{"name":"data","type":"ca59530d1a4f3e19","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"1986c2664009c991","optional":false}],"indices":[]}, "b062078e899958f9": {"kind":"object","properties":[{"name":"kind","type":"1f1385dbc3e875c0","optional":false}],"indices":[]}, "b0b0eb1b16490f6b": {"kind":"object","properties":[{"name":"attachmentId","type":"4cd48f3e5108bd6e","optional":false},{"name":"bytes","type":"0657d3ca5ac4e7bf","optional":false},{"name":"height","type":"0657d3ca5ac4e7bf","optional":false},{"name":"mediaType","type":"2f1e6a7ebace43d7","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":true},{"name":"originalDimensions","type":"0f46153645c29784","optional":true},{"name":"width","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "b120362fd216a16a": {"kind":"union","types":["0203243b72fc867a","022623b9ef98ec47","02b2c23f2d94b028","0ed8d1108fe7a5e1","0eeb17e1df89b1eb","11638dc58a31496f","1938c8c20f1cba7c","1e709346005826eb","1fdad624334d3548","2152caf1a71e588e","2235d957bf4b7c6c","224aa11d4d5f88c4","24ce31aa528272e5","260523b412c19ad8","28936c614bd4e7e2","290f32bfd81ff85a","297fbd8067d7815a","2aad1179f9576314","312aee2f0c42aabb","31907de956375182","3247e566f4340029","3b04790e08b35241","3b4f2c9ef2c1217e","3bdb8dc6989655b0","408927267422a7b2","45f005e6bbb76452","4991dfc415c9efeb","49dab019d00e8c26","4a9bef9f27a94252","52c6113cb0e53f34","53cf1704ae0709c7","5654685d7557ec74","58015da1c1950a66","5bb69b88517f6214","5bc66269cfaf614c","5cdbf342b7748d15","5d7a64ed6aac5551","6085488d223aeffd","6292bcbedaa97fea","65a8400e451e667e","65d583e66518c523","672c4aa2a9b736e3","6ccb6bc423fb3952","6ea608bdc8a7a63d","6f22a9d02b9a1f11","6f44f32be9bcd9a8","719c51f67074f970","7554565be416b034","756b5e23a5cb2c7f","7df62978a2b7de85","7eaef7a763ba5b94","7f959241c24128b4","80677c8c8b804189","825ca23416b1ea9d","8338ee303e2bd448","848639f8787f035b","854db3efdfb0cd39","85fe96bf15d5c755","8620189c8e335827","87b1a33564b9a4d6","88841202942c7683","8a046b52331959aa","8c1c4f72755a78ea","8c627d7a45fa406c","8f324cea3916c051","8fcb0b44a575e998","96bfb916a727cd44","99135d8a39379779","99f0e98184709ad1","9edc162949abf93b","9f65c43e340bba31","a15a19ef63cf5d1b","a16e567c8cd2c55c","a2066bd52f61218b","a3c6f1ea38e9a61d","a4afec59b75aad5d","aa2bf9e82b9fdeb9","aae343bc3d663c83","ada310bf0bdb8fed","ade057f4fdc387ae","b062078e899958f9","b2a5c8c008a31e3d","b3681c026a0b98e9","b4ec0af39fd52ed9","b613f83572fe2fbf","b6f94c17dd9bbd7c","b70f3e81fe11b7a5","b79641a9d25e7566","ba2a1a8f194b1bb2","ba4c40b4381e0dcd","baccc97ec34be6c4","c5228b7a55498d84","c55827fba7018198","cb1c8959e1726f82","cda2345a9be9d139","d85590a5675c6e2f","d9c462b9f62fdd3e","db06ef35b27a3842","dba54c09c6e7aafc","dd00cdccd0ba487b","dd5ee8762bd8a14b","dd6cc2bb2a8f2824","dde0cdf9d426768b","deaae5cd30987d4f","e7f4cbb3a57f4cbd","edbbfff1128099fd","ee577a0cc18664c9","f230c0b583e23b25","f29d63336e8266d8","f4a66f654996c093","f56c5b59ca016131","f6329fb30c7ea801","f6605d6c57933aea","f6afe41e3926f422","f7283223aacdc39b","f97f47b0eb87905e","fcf81d3c58c13593","fe1a7acdeed4d0f5"]}, "b158f3ff69237148": {"kind":"object","properties":[{"name":"contextWindow","type":"0657d3ca5ac4e7bf","optional":true},{"name":"model","type":"4cd48f3e5108bd6e","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false},{"name":"systemPromptUpdate","type":"f311671e07bc7fa6","optional":true}],"indices":[]}, "b1664fa89f8eab49": {"kind":"array","element":"bbc5696e3f13c758"}, "b1ebd5cda47d90d9": {"kind":"object","properties":[{"name":"member","type":"6598cd6da253948d","optional":false},{"name":"teamId","type":"4cd48f3e5108bd6e","optional":false},{"name":"version","type":"a0f906856fc06f1c","optional":false}],"indices":[]}, @@ -1186,6 +1194,7 @@ "c1f0f17b6493b85d": {"kind":"object","properties":[{"name":"arguments","type":"4cd48f3e5108bd6e","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"1f0133217eeeffd8","optional":false}],"indices":[]}, "c1f9e0405de6d18c": {"kind":"object","properties":[{"name":"data","type":"a6da9ad5d42ae83a","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"cdca80b8dad24d4e","optional":false}],"indices":[]}, "c219efbdfd53b836": {"kind":"literal","value":"step/end"}, + "c2a13242efc3bea4": {"kind":"object","properties":[{"name":"enabled","type":"aa93010d0e521ad7","optional":false}],"indices":[]}, "c2deb7c9183735d4": {"kind":"union","types":["1ebc6f9ff3aa9e3a","2142e391b9ccb60d","b57a2889408de36e"]}, "c2f49004d9ae38a9": {"kind":"union","types":["1ebc6f9ff3aa9e3a","b57a2889408de36e","ef226a90ddac6cd8"]}, "c43b119ce579b3e9": {"kind":"literal","value":"paused"}, @@ -1195,7 +1204,6 @@ "c589e938cc0e4989": {"kind":"literal","value":"tool/call"}, "c592ce75aab73fca": {"kind":"union","types":["0657d3ca5ac4e7bf","4cd48f3e5108bd6e","529e0ccd0e34079a","72ec79127a9c0d02","938f7c7357bc403e","aa93010d0e521ad7"]}, "c748fb42c80a1200": {"kind":"array","element":"86e1e384495ee09a"}, - "c78b9097bfae83cd": {"kind":"union","types":["09f3f8dc417f2782","4035903993dfee62","4b259f7f8f0a44ad","64ef542b1bfc9883","fa87537f4a9c1e25"]}, "c8e6f48878cc2a2f": {"kind":"literal","value":"active"}, "c99e20441bf15341": {"kind":"literal","value":"feedback/record"}, "ca085dba2c877fec": {"kind":"literal","value":"sandbox/mode"}, @@ -1207,21 +1215,21 @@ "cda2345a9be9d139": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "cdca80b8dad24d4e": {"kind":"literal","value":"tool-workflow/run-start"}, "cdee10e87dbed844": {"kind":"object","properties":[{"name":"delayMs","type":"0657d3ca5ac4e7bf","optional":false},{"name":"failure","type":"5f2117d1856a5ab4","optional":false},{"name":"maxRetries","type":"0657d3ca5ac4e7bf","optional":false},{"name":"mode","type":"3bc287e765d58dc3","optional":false},{"name":"policyKey","type":"4cd48f3e5108bd6e","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false},{"name":"retry","type":"0657d3ca5ac4e7bf","optional":false},{"name":"retryId","type":"4cd48f3e5108bd6e","optional":false},{"name":"step","type":"0657d3ca5ac4e7bf","optional":false},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, - "cdf988172b589b23": {"kind":"object","properties":[{"name":"data","type":"d4d960185026266e","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"829a2fb3be6efd4b","optional":false}],"indices":[]}, "cef171adf021a186": {"kind":"object","properties":[{"name":"arguments","type":"bbc5696e3f13c758","optional":false},{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"error","type":"a81059ca06999da1","optional":true},{"name":"isError","type":"aa93010d0e521ad7","optional":false},{"name":"meta","type":"c592ce75aab73fca","optional":true},{"name":"name","type":"4cd48f3e5108bd6e","optional":false},{"name":"parentCallId","type":"4cd48f3e5108bd6e","optional":false},{"name":"rootCallId","type":"4cd48f3e5108bd6e","optional":false},{"name":"subCallId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "cffcdc19881ab61e": {"kind":"object","properties":[{"name":"cleared","type":"0b6d718c693fdf7f","optional":false},{"name":"clearedAt","type":"0657d3ca5ac4e7bf","optional":false},{"name":"kind","type":"237c004c73b6b288","optional":false},{"name":"operation","type":"e7e154d47d5fbc53","optional":false},{"name":"version","type":"f88f1b7b6bb733f6","optional":false}],"indices":[]}, "d0fbb0ca1bbba059": {"kind":"literal","value":"delete"}, "d28765f87d6b6ab4": {"kind":"literal","value":"one-shot"}, "d4b739491689cc6b": {"kind":"object","properties":[{"name":"block","type":"8064c03c87c357fd","optional":false},{"name":"index","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"eefbea819caeeea9","optional":false}],"indices":[]}, "d4c799e3d51cae10": {"kind":"literal","value":"working-directory/change"}, - "d4d960185026266e": {"kind":"object","properties":[{"name":"inserted","type":"7c203f3d4dfa6638","optional":false},{"name":"outcome","type":"d4e4d575abeb4b72","optional":true},{"name":"removedCount","type":"0657d3ca5ac4e7bf","optional":true},{"name":"start","type":"0657d3ca5ac4e7bf","optional":false},{"name":"target","type":"96f4a9c81fc0f940","optional":false}],"indices":[]}, "d4e4d575abeb4b72": {"kind":"literal","value":"canceled"}, "d595ec73b32b016a": {"kind":"object","properties":[{"name":"data","type":"a40d12070f6f4a12","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"b57ae1b37e52bee0","optional":false}],"indices":[]}, "d63b8b8ffad9c02f": {"kind":"object","properties":[{"name":"data","type":"8bb8c3751a6ce64b","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"47e5d84f1a93450a","optional":false}],"indices":[]}, "d66e11dddf5dfa3c": {"kind":"object","properties":[{"name":"data","type":"cef171adf021a186","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"fb5e0517050d2c6d","optional":false}],"indices":[]}, "d6e654b88b584fcd": {"kind":"literal","value":"fallback"}, "d85590a5675c6e2f": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false}],"indices":[]}, + "d8a476d2c6fd30df": {"kind":"literal","value":"team/mode"}, "d9196086e41e4dd5": {"kind":"literal","value":"complete"}, + "d95c624eddecdacf": {"kind":"object","properties":[{"name":"data","type":"601327bedca5f98a","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"587bb11da730e06a","optional":false}],"indices":[]}, "d9c462b9f62fdd3e": {"kind":"object","properties":[{"name":"callId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"a13d8f379222ece8","optional":false}],"indices":[]}, "d9edf1862e4471d6": {"kind":"literal","value":"schedule"}, "da60ed58a8fa23d7": {"kind":"object","properties":[{"name":"cwd","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, @@ -1231,10 +1239,12 @@ "db874d463b0fdec7": {"kind":"object","properties":[{"name":"data","type":"3e4e7683b0192c9f","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"e2d0367e5716bc18","optional":false}],"indices":[]}, "dba54c09c6e7aafc": {"kind":"object","properties":[{"name":"kind","type":"4edec1b770e8ec4c","optional":false}],"indices":[]}, "dbdafceb9a900183": {"kind":"literal","value":"hook"}, + "dcb07e04496586d2": {"kind":"object","properties":[{"name":"data","type":"613660455704fb82","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"de25352d2dc41963","optional":false}],"indices":[]}, "dd00cdccd0ba487b": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "dd2780e7e0768433": {"kind":"literal","value":"fresh"}, "dd5ee8762bd8a14b": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false}],"indices":[]}, "dd6cc2bb2a8f2824": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false}],"indices":[]}, + "ddce54a3873dc571": {"kind":"object","properties":[{"name":"data","type":"e1880c67e735e228","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"829a2fb3be6efd4b","optional":false}],"indices":[]}, "dde0cdf9d426768b": {"kind":"object","properties":[{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "de25352d2dc41963": {"kind":"literal","value":"user/message"}, "de30b1fa12c75f81": {"kind":"literal","value":"image/gif"}, @@ -1244,6 +1254,8 @@ "df843efd87c78cf8": {"kind":"array","element":"4f91f187baeabef5"}, "dfce5db4aa42b7eb": {"kind":"union","types":["871bfd6f7bb91061","b57a2889408de36e","c8e6f48878cc2a2f"]}, "e0a787e6ec76c7c9": {"kind":"object","properties":[{"name":"data","type":"ef11a3b7fa2033ae","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"c219efbdfd53b836","optional":false}],"indices":[]}, + "e1880c67e735e228": {"kind":"object","properties":[{"name":"inserted","type":"690583582246babf","optional":false},{"name":"outcome","type":"d4e4d575abeb4b72","optional":true},{"name":"removedCount","type":"0657d3ca5ac4e7bf","optional":true},{"name":"start","type":"0657d3ca5ac4e7bf","optional":false},{"name":"target","type":"96f4a9c81fc0f940","optional":false}],"indices":[]}, + "e272493e3ea0b33b": {"kind":"union","types":["01c161379a3c3394","09f3f8dc417f2782","4b259f7f8f0a44ad","613660455704fb82","fa87537f4a9c1e25"]}, "e2ad8f5bcd813f26": {"kind":"object","properties":[{"name":"childCreatedAt","type":"0657d3ca5ac4e7bf","optional":false},{"name":"childId","type":"4cd48f3e5108bd6e","optional":false},{"name":"label","type":"4cd48f3e5108bd6e","optional":false},{"name":"mode","type":"e5d3828df1ec3e2a","optional":false},{"name":"version","type":"f88f1b7b6bb733f6","optional":false}],"indices":[]}, "e2d0367e5716bc18": {"kind":"literal","value":"compaction/start"}, "e2f9a41e0989f54e": {"kind":"object","properties":[{"name":"data","type":"9947aa5938fe19a8","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"ef4f00923b9edbae","optional":false}],"indices":[]}, @@ -1320,7 +1332,6 @@ "fe1a7acdeed4d0f5": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "fe792a6cd4304945": {"kind":"literal","value":"tool-workflow/run-end"}, "fec5442b9ee4122e": {"kind":"object","properties":[{"name":"cacheReadTokens","type":"0657d3ca5ac4e7bf","optional":true},{"name":"cacheWriteTokens","type":"0657d3ca5ac4e7bf","optional":true},{"name":"inputTokens","type":"0657d3ca5ac4e7bf","optional":false},{"name":"outputTokens","type":"0657d3ca5ac4e7bf","optional":false},{"name":"reasoningTokens","type":"0657d3ca5ac4e7bf","optional":true},{"name":"totalTokens","type":"0657d3ca5ac4e7bf","optional":true}],"indices":[]}, - "fee1caf8ab6a65c1": {"kind":"literal","value":"instructions"}, - "ff2cbb53582d1827": {"kind":"array","element":"c78b9097bfae83cd"} + "fee1caf8ab6a65c1": {"kind":"literal","value":"instructions"} } } diff --git a/docs/subsystems/README.i18n.yaml b/docs/subsystems/README.i18n.yaml index 069f78cc33..9bd04642b1 100644 --- a/docs/subsystems/README.i18n.yaml +++ b/docs/subsystems/README.i18n.yaml @@ -3,5 +3,5 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/README.md /subsystems: - en: 81b2c48d922de5a9 - zh: cf0db50b34d2feb3 + en: e95b837b8f47eac8 + zh: bd69de97c80fe2ae diff --git a/docs/subsystems/README.md b/docs/subsystems/README.md index 86e30a5519..7d66caa4dd 100644 --- a/docs/subsystems/README.md +++ b/docs/subsystems/README.md @@ -51,7 +51,7 @@ One page per subsystem of the DeepSeek Harness: what it is, the data structures | [subagent.md](subagent.md) | the subagent seam: the named-provider registry, `SubagentStartRequest`/`Result`/`Run`, the start-time-vs-runtime capability split | | [voice-input.md](voice-input.md) | experimental named recognizers, transient audio and revision-guarded draft insertion | | [translation.md](translation.md) | experimental anonymous text translation and explicit provider requests | -| [agent-team.md](agent-team.md) | Agent Teams: implicit Lead identity, named continuable teammates, direct peer inbox delivery, and shared task DAG | +| [agent-team.md](agent-team.md) | Agent Teams: shared task DAG and task ownership for a subagent tree | | [liveshare.md](liveshare.md) | the opt-in LiveShare bundle: nested share modes, a fork's ceiling and durability, enforcement installation, and the constraint a shared Session's externally driven turns run under | | [claude-code-mods.md](claude-code-mods.md) | Claude Code mods through the experimental bridge: every event, `$` member, chain rule, drawing feature, and example mod that differs from Claude Code | | [web.md](web.md) | the web access seam: `WebSearchRequest`/`Result`, `WebFetchRequest`/`Result`, `WebFetchBody`, provider availability, `WebError` | diff --git a/docs/subsystems/README.zh.md b/docs/subsystems/README.zh.md index 289a6a71f3..7358fe0203 100644 --- a/docs/subsystems/README.zh.md +++ b/docs/subsystems/README.zh.md @@ -51,7 +51,7 @@ | [subagent.md](subagent.zh.md) | subagent seam:命名提供方注册表、`SubagentStartRequest`/`Result`/`Run`、启动时与运行时能力拆分 | | [voice-input.md](voice-input.zh.md) | 实验性具名语音识别、临时音频和受版本保护的草稿插入 | | [translation.md](translation.zh.md) | 实验性免登录文本翻译与显式 Provider 请求 | -| [agent-team.md](agent-team.zh.md) | Agent Teams:隐式 Lead 身份、具名 continuable teammate、直接 peer inbox 投递 与共享任务 DAG | +| [agent-team.md](agent-team.zh.md) | Agent Teams:子代理树共享的任务 DAG 与任务归属 | | [liveshare.md](liveshare.zh.md) | 可选的 LiveShare bundle:嵌套分享模式、fork 的上限与持久性、上限安装,以及共享会话外部驱动轮次所受的约束 | | [claude-code-mods.md](claude-code-mods.zh.md) | 经实验性桥接运行的 Claude Code 模组:与 Claude Code 不同的每个事件、`$` 成员、链规则、绘制特性与示例模组 | | [web.md](web.zh.md) | Web 访问 seam:`WebSearchRequest`/`Result`、`WebFetchRequest`/`Result`、`WebFetchBody`、提供方可用性、`WebError` | diff --git a/docs/subsystems/agent-team.i18n.yaml b/docs/subsystems/agent-team.i18n.yaml index a4c599562f..435e1b6bbb 100644 --- a/docs/subsystems/agent-team.i18n.yaml +++ b/docs/subsystems/agent-team.i18n.yaml @@ -3,20 +3,20 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/agent-team.md /agent-teams: - en: f571e223220bb616 - zh: dd7e37fdf359eeae -/agent-teams/identity-and-roster: - en: df72d9409b89714d - zh: 982934dd18864ca2 + en: 152195aeb3b5dea1 + zh: 1cf6b3b36bd6f02c +/agent-teams/identity-and-historical-roster: + en: 3a574287e378ecf9 + zh: 818667289271c1af /agent-teams/messages-and-historical-mailbox: - en: 2aed7dd369f47747 - zh: 4f080e50c584ad37 + en: e50b723dfffd6758 + zh: 6e4d5891b5d913fc /agent-teams/shared-task-dag: - en: 4515005d07e4d2a3 - zh: 26811b31f69009d6 + en: c70f0fd199fc7db5 + zh: c6b77cb5a5cfd1c2 /agent-teams/web-projection: - en: 6af99eff8036597a - zh: f172f994e01f12c1 + en: 059d4e5d6a9ddc71 + zh: cb3fd92096144e19 /agent-teams/replay: - en: fe32f71c6d799ebc - zh: aa2b6ccddba8cfd5 + en: 448d32856f87b067 + zh: 7fafd96314c7e2c5 diff --git a/docs/subsystems/agent-team.md b/docs/subsystems/agent-team.md index c5c6a0f775..49b27ea176 100644 --- a/docs/subsystems/agent-team.md +++ b/docs/subsystems/agent-team.md @@ -2,14 +2,14 @@ English | [中文](agent-team.zh.md) -Types shared by the experimental implicit-root Team domain, model tools, and host adapters. The [Agent Teams Agent Note](../../.agents/notes/implemented/feature/2026-08-05-agent-teams.md) owns identity, mailbox, task, and shared-checkout decisions; this page records the durable and client-visible forms from [`packages/experimental/agent-team/src/types.ts`](../../packages/experimental/agent-team/src/types.ts). +Types shared by the shared task board, model tools, and host adapters. The [task-board wait decision](../../.agents/notes/implemented/architecture/2026-10-19-team-task-board-inbox.md) owns coordination semantics; this page records durable and client-visible forms from [`packages/team/agent-team/src/types.ts`](../../packages/team/agent-team/src/types.ts). -## Identity and roster +## Identity and historical roster -`TeamId` is the root `SessionId` under a distinct [brand](core.md#branded-ids). `TeamTaskId` is allocated as `task-`. New sends return the target inbox `MessageId`; `TeamMessageId` is retained only for historical mailbox records. Teammate names are immutable labels. +`TeamId` brands the root `SessionId`; `TeamTaskId` uses `task-`. `TeamMessageId` and roster records remain for historical decoding. The subagent runtime resolves the tree and discovers eligible task owners at arbitrary depth; historical roster entries grant no authority. ```ts type-equiv -/** Whole durable value written on every teammate lifecycle change. */ +/** Historical roster value written by earlier Team builds on every teammate lifecycle change. */ interface TeamMemberSnapshot { readonly id: SessionId readonly name: string @@ -21,11 +21,11 @@ interface TeamMemberSnapshot { } ``` -Every member starts in `provisioning` and reaches exactly one terminal roster phase, `active` or `failed`. Roster `running`/`inactive` status is derived separately and never rewrites this record. +Historical records retain their recorded `provisioning`, `active`, or `failed` phase. Current turn activity belongs to ordinary subagent discovery and Session status, independently of task completion. ## Messages and historical mailbox -New sends return inbox acceptance or reject. The target stores the existing `agent-message` source with the real sender Session id and a sender-name content prefix. Team has no new-message outbox or resend deduplication. The [package contract](../../packages/experimental/agent-team/README.md#understand-the-implementation) defines persistence. The following mailbox types remain readable so historical Lead logs still replay; Team does not deliver their pending records. +Members exchange ordinary [subagent messages](subagent.md), retaining the real sender Session id. Task changes do not end inbox waits, add inbox messages, or start idle agents. Send an explicit message when an agent needs to act on a task update. The following historical mailbox types remain readable; Team never delivers their pending records. ```ts type-equiv /** One peer message recorded by the historical Team mailbox. */ @@ -38,8 +38,6 @@ interface TeamMessageSnapshot { } ``` -Every message attempts Steer delivery. A running target receives it at the nearest step boundary; an inactive target starts a turn if loaded or cold-resumes otherwise. Scheduling is not stored in the durable record because callers cannot select another mode. - Historical `team-message` sources retain the old message id and sender attribution; the client uses them for the message title and icon. ```ts type-equiv @@ -71,25 +69,15 @@ interface TeamTaskSnapshot { } ``` -`pending` is unstarted or released, `in_progress` carries an owner, `completed` satisfies blockers, and `deleted` is a retained tombstone. Views add owner name, readiness, and write-scope overlap warnings without changing the durable snapshot. +`pending` is unstarted or released, `in_progress` carries an owner, `completed` satisfies blockers, and `deleted` is a retained tombstone. Views expose `ownerId`, readiness, and advisory write-scope overlap without changing the durable snapshot. ## Web projection -The Lead Session publishes `SessionProjectionMap.agentTeam` with durable roster rows and non-deleted task views. `failure` reports a rejected persisted record beside the last valid state. Member activity comes from Session status; model labels come from each member's `modelSelection` projection. +`agentTeam.enabled` exposes the root Session’s saved selection. New Sessions default to false; `team/mode` records explicit selections. Historical tasks imply enabled until an explicit selection. Only the root can call `setEnabled`; descendants share that mode. The Web menu calls the same method through `remote.agentTeams.setEnabled`, with the root Session id resolved to its live Agent by the Gateway. Mode writes take effect before awaiting persistence. Tools resolve availability at step preparation; prompt providers read the selection when evaluated. Task mutations check the current mode when their queued operation starts; admitted mutations may finish. Mode changes do not end inbox waits. A failed flush leaves the appended selection active; retrying the same selection flushes it without a duplicate event. -```ts type-equiv -/** One durable roster row published through the `agentTeam` Session projection. */ -interface TeamMemberProjection { - readonly id: SessionId - readonly name: string - readonly role: 'lead' | 'teammate' - /** Durable lifecycle; the Lead row is always `active`. Turn activity comes from Session status. */ - readonly phase: TeamMemberPhase - readonly error?: string -} -``` +The root Session publishes non-deleted task views through `SessionProjectionMap.agentTeam`. `failure` reports a rejected persisted task or mode record beside the last valid state. Historical roster and mailbox records are not projected. The task panel identifies every owner by its Session id. ```ts type-equiv /** Runtime-enriched task view returned to tools and hosts. */ @@ -101,7 +89,7 @@ interface TeamTaskView { readonly status: TeamTaskStatus readonly blockedBy: TeamTaskId[] readonly writeScopes: string[] - readonly ownerName?: string + readonly ownerId?: SessionId readonly ready: boolean readonly writeScopeWarnings: string[] } @@ -111,10 +99,10 @@ interface TeamTaskView { /** * Durable Team state published to browser clients through the Lead Session's * `agentTeam` projection. `failure` names the first rejected persisted Team - * record; members and tasks then stay at the last valid state. + * record; tasks then stay at the last valid state. */ interface TeamProjection { - readonly members: TeamMemberProjection[] + readonly enabled: boolean readonly tasks: TeamTaskView[] readonly failure?: string } @@ -122,7 +110,7 @@ interface TeamProjection { ## Replay -The `agentTeam` Session projection replays one root Session into the roster, task board, and historical queued-minus-delivered mailbox. It selects records by `TeamId`, so events inherited by an ordinary fork retain the ancestor id and never enter the new root's state. Session event `seq` and `time` remain the ordering and timing record; Team snapshots do not duplicate them. Roster and task reads reach callers as views; pending mail is not exposed to callers. The package [README](../../packages/experimental/agent-team/README.md) owns operation, authorization, recovery, and limit behavior. +The `agentTeam` projection replays task records selected by `TeamId` and the Session’s own mode selections. Subagent discovery owns current tree membership and ignores inherited catalog authority. Session event `seq` and `time` own ordering and timing. Committed durable generations are preserved. The [package README](../../packages/team/agent-team/README.md) defines task operations and limits. @@ -140,42 +128,35 @@ Agent Teams service backed by the exact live Lead Session log. ```ts cordis-catalog /** - * Resolve one exact live Agent's Team role. - * @param agent - exact live Agent used as the authority credential. - * @returns its root, Team identity, role, and model-facing name. + * Read the Team selection shared by the caller's subagent tree. + * @param caller - exact live root or descendant. + * @returns whether Team tools and guidance are enabled. */ -membership(agent: Agent): TeamMembership +isEnabled(caller: Agent): boolean /** - * List the runtime-enriched roster visible to one Team member. - * @param agent - exact live Team member. - * @returns Lead and teammate rows in creation order. + * Save a root Session's Team selection without stopping children or deleting tasks. + * The selection takes effect before awaiting persistence. Tools resolve availability at + * step preparation; prompt providers read the selection when evaluated. Task mutations + * check the current mode when their queued operation starts; admitted operations finish. + * @param agent - exact live root whose tree receives the selection. + * @param enabled - selected Team mode. + * @param signal - cancellation checked before changing the selection. + * @returns fulfillment after the selection flushes. + * @throws when called by a descendant or persistence fails. An appended selection remains active; + * retrying the same selection flushes without a duplicate event. */ -listMembers(agent: Agent): TeamMemberView[] - -/** - * Create one named, continuable direct child of the Team Lead. - * @param caller - exact live Lead Agent. - * @param request - immutable name, description, prompt, context mode, provider, and cancellation. - * @returns the active roster row. - */ -async spawnTeammate(caller: Agent, request: SpawnTeammateRequest): Promise - -/** - * Steer one peer message into the target inbox or reject the attempt. - * @param caller - exact live sending Team member. - * @param request - target name, content, and cancellation before acceptance. - * @returns accepted inbox identity; acceptance follows normal Agent persistence and does not await model processing. - */ -async sendMessage(caller: Agent, request: SendTeamMessageRequest): Promise +@Remote async setEnabled(agent: Agent, enabled: boolean, signal?: AbortSignal): Promise /** * Create one unowned pending task in the Team Lead log. * @param caller - exact live Team member creating the task. * @param request - task text, blockers, and advisory write scopes. + * @param signal - cancellation before the task event is appended; omitted means no cancellation. * @returns the revision-one task view. + * @throws TEAM_TASK_PERSISTENCE_UNCONFIRMED when the task is visible but durability is unknown; read it before retrying. */ -async createTask(caller: Agent, request: CreateTeamTaskRequest): Promise +async createTask(caller: Agent, request: CreateTeamTaskRequest, signal?: AbortSignal): Promise /** * Return one task, including a deleted tombstone. @@ -196,36 +177,14 @@ listTasks(caller: Agent): TeamTaskView[] * Compare-and-set one authorized task transition. * @param caller - exact live Team member authorizing the mutation. * @param request - task identity, expected revision, action, and action fields. + * @param signal - cancellation before the task event is appended; omitted means no cancellation. * @returns the committed next task revision. + * @throws TEAM_TASK_PERSISTENCE_UNCONFIRMED when the task is visible but durability is unknown; read it before retrying. */ -async updateTask(caller: Agent, request: UpdateTeamTaskRequest): Promise - -/** - * Wait for the next Team-domain or member-status change. - * @param caller - exact live Team member waiting for activity. - * @param timeoutMs - bounded wait duration from ten seconds through one hour. - * @param signal - caller cancellation for the wait only. - * @returns one observed change or a timeout result. - */ -async waitForChange(caller: Agent, timeoutMs: number, signal: AbortSignal): Promise - -/** - * Interrupt one live teammate turn without clearing its pending inbox. - * @param caller - exact live Lead Agent. - * @param targetName - durable teammate name. - * @returns the target status sampled before cancellation. - */ -interrupt(caller: Agent, targetName: string): { previousStatus: 'running' | 'inactive' } - -/** - * Resolve a caller without throwing, used by scoped-tool installation and observers. - * @param agent - candidate exact live Agent. - * @returns Team membership, or undefined for non-Team subagents and stale identities. - */ -tryMembership(agent: Agent): TeamMembership | undefined +async updateTask(caller: Agent, request: UpdateTeamTaskRequest, signal?: AbortSignal): Promise ``` Types: [Agent](core.md) -Source: [`packages/experimental/agent-team/src/index.ts`](../../packages/experimental/agent-team/src/index.ts) +Source: [`packages/team/agent-team/src/index.ts`](../../packages/team/agent-team/src/index.ts) diff --git a/docs/subsystems/agent-team.zh.md b/docs/subsystems/agent-team.zh.md index 2e3c0b040e..fb52d49be4 100644 --- a/docs/subsystems/agent-team.zh.md +++ b/docs/subsystems/agent-team.zh.md @@ -2,14 +2,14 @@ [English](agent-team.md) | 中文 -实验性隐式 Root Team 领域、模型工具与宿主适配器共享的类型。[Agent Teams Agent Note](../../.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md)负责身份、mailbox、task 与共享 checkout 决策;本页记录 [`packages/experimental/agent-team/src/types.ts`](../../packages/experimental/agent-team/src/types.ts) 中的持久与客户端可见形式。 +共享任务板、模型工具和 host 适配器共享的类型。[任务板等待决策](../../.agents/notes/implemented/architecture/2026-10-19-team-task-board-inbox.zh.md)负责协调语义;本页记录 [`packages/team/agent-team/src/types.ts`](../../packages/team/agent-team/src/types.ts) 中的持久化和客户端可见形式。 -## 身份与 roster +## 身份与历史名册 -`TeamId` 是使用独立[品牌类型](core.zh.md#branded-ids)的 root `SessionId`。`TeamTaskId` 按 `task-` 分配。新发送返回目标 inbox 的 `MessageId`;`TeamMessageId` 仅供历史 mailbox 记录保留。teammate 名称是不可变标签。 +`TeamId` 品牌化根 `SessionId`;`TeamTaskId` 使用 `task-`。`TeamMessageId` 和名册记录为历史解码保留。subagent 运行时解析树,并发现任意深度的有效任务所有者;历史名册条目不授予权限。 ```ts type-equiv -/** Whole durable value written on every teammate lifecycle change. */ +/** Historical roster value written by earlier Team builds on every teammate lifecycle change. */ interface TeamMemberSnapshot { readonly id: SessionId readonly name: string @@ -21,11 +21,11 @@ interface TeamMemberSnapshot { } ``` -每个 member 都从 `provisioning` 开始,并且只到达一个终态 roster phase:`active` 或 `failed`。roster 的 `running`/`inactive` 状态单独派生,绝不会重写该记录。 +历史记录保留其记录的 `provisioning`、`active` 或 `failed` 阶段。当前轮次活动属于普通 subagent 发现和 Session 状态,与任务完成相互独立。 ## 消息与历史 mailbox -新发送返回 inbox 接收结果或拒绝。目标保存现有 `agent-message` source,包含真实发送者 Session id 和发送者名称正文前缀。Team 不为新消息提供 outbox 或重发去重。[包约定](../../packages/experimental/agent-team/README.zh.md#understand-the-implementation)定义持久化。以下 mailbox 类型继续可读,以便历史 Lead 日志仍能回放;Team 不投递其中的 pending 记录。 +成员交换普通 [subagent 消息](subagent.zh.md),保留真实发送方 Session id。任务变更不会结束收件箱等待、添加收件箱消息或启动空闲 agent。需要 agent 处理任务更新时,发送明确消息。下列历史邮箱类型保持可读;Team 绝不投递其待处理记录。 ```ts type-equiv /** One peer message recorded by the historical Team mailbox. */ @@ -38,8 +38,6 @@ interface TeamMessageSnapshot { } ``` -每条消息都会尝试 Steer 投递。running target 在最近的步骤边界收到消息,inactive target 在已加载时启动一个轮次,否则冷恢复。调用方不能选择其他模式,因此持久记录不存储调度方式。 - 历史 `team-message` source 保留旧消息 id 与发送者归属;客户端用它们展示消息标题和图标。 ```ts type-equiv @@ -71,25 +69,15 @@ interface TeamTaskSnapshot { } ``` -`pending` 表示尚未开始或已经释放,`in_progress` 携带 owner,`completed` 满足 blocker,`deleted` 是保留的 tombstone。view 会添加 owner name、readiness 和 write-scope 重叠警告,但不会改变持久快照。 +`pending` 表示未开始或已释放,`in_progress` 携带负责人,`completed` 满足依赖,`deleted` 是保留的墓碑记录。视图提供 `ownerId`、就绪性和写入范围重叠提示,不改变持久快照。 ## Web 投影 -Lead Session 通过 `SessionProjectionMap.agentTeam` 发布持久 roster 行与未删除任务视图。`failure` 在最后有效状态旁报告被拒绝的持久记录。成员活动来自 Session 状态;模型标签来自各成员的 `modelSelection` 投影。 +`agentTeam.enabled` 提供根 Session 保存的选择。新 Session 默认为 false;`team/mode` 记录显式选择。历史任务在显式选择前视为开启。仅根可调用 `setEnabled`;后代共享该模式。Web 菜单通过 `remote.agentTeams.setEnabled` 调用同一方法,Gateway 将根 Session id 解析为其存活 Agent。模式写入先立即生效,再等待刷盘。工具在步骤准备时确定可用性;提示词提供方在求值时读取选择。任务修改在排队操作开始时检查当前模式;已获准的修改可继续完成。模式变更不会结束收件箱等待。刷盘失败时已追加的选择仍然生效;重试相同选择会再次刷盘,不重复追加事件。 -```ts type-equiv -/** One durable roster row published through the `agentTeam` Session projection. */ -interface TeamMemberProjection { - readonly id: SessionId - readonly name: string - readonly role: 'lead' | 'teammate' - /** Durable lifecycle; the Lead row is always `active`. Turn activity comes from Session status. */ - readonly phase: TeamMemberPhase - readonly error?: string -} -``` +根 Session 通过 `SessionProjectionMap.agentTeam` 发布未删除任务视图。`failure` 在最后有效状态旁报告被拒绝的持久化任务或模式记录。历史名册与邮箱记录不参与投影。任务面板以 Session id 标识每个负责人。 ```ts type-equiv /** Runtime-enriched task view returned to tools and hosts. */ @@ -101,7 +89,7 @@ interface TeamTaskView { readonly status: TeamTaskStatus readonly blockedBy: TeamTaskId[] readonly writeScopes: string[] - readonly ownerName?: string + readonly ownerId?: SessionId readonly ready: boolean readonly writeScopeWarnings: string[] } @@ -111,10 +99,10 @@ interface TeamTaskView { /** * Durable Team state published to browser clients through the Lead Session's * `agentTeam` projection. `failure` names the first rejected persisted Team - * record; members and tasks then stay at the last valid state. + * record; tasks then stay at the last valid state. */ interface TeamProjection { - readonly members: TeamMemberProjection[] + readonly enabled: boolean readonly tasks: TeamTaskView[] readonly failure?: string } @@ -122,7 +110,7 @@ interface TeamProjection { ## 回放 -`agentTeam` Session 投影把一个 Root Session 回放成roster、任务板与历史 queued-minus-delivered mailbox。它按 `TeamId` 选取记录,因此普通 fork 继承的 event 保留 ancestor id,绝不会进入新 Root 的状态。Session event 的 `seq` 与 `time` 继续负责顺序和时间记录,Team snapshot 不再重复保存它们。roster 与 task 读取以 view 形式到达调用方,而 pending 邮件不对调用方公开。包 [README](../../packages/experimental/agent-team/README.zh.md)负责 operation、authorization、recovery 和限制行为。 +`agentTeam` 投影按 `TeamId` 重放任务记录,并重放 Session 自身的模式选择。subagent 发现负责当前树成员关系,不将继承目录作为权限来源。Session 事件的 `seq` 和 `time` 负责排序和时间。已提交的持久化代次保持不变。[包 README](../../packages/team/agent-team/README.zh.md)定义任务操作和限制。 @@ -140,42 +128,35 @@ Agent Teams service backed by the exact live Lead Session log. ```ts cordis-catalog /** - * Resolve one exact live Agent's Team role. - * @param agent - exact live Agent used as the authority credential. - * @returns its root, Team identity, role, and model-facing name. + * Read the Team selection shared by the caller's subagent tree. + * @param caller - exact live root or descendant. + * @returns whether Team tools and guidance are enabled. */ -membership(agent: Agent): TeamMembership +isEnabled(caller: Agent): boolean /** - * List the runtime-enriched roster visible to one Team member. - * @param agent - exact live Team member. - * @returns Lead and teammate rows in creation order. + * Save a root Session's Team selection without stopping children or deleting tasks. + * The selection takes effect before awaiting persistence. Tools resolve availability at + * step preparation; prompt providers read the selection when evaluated. Task mutations + * check the current mode when their queued operation starts; admitted operations finish. + * @param agent - exact live root whose tree receives the selection. + * @param enabled - selected Team mode. + * @param signal - cancellation checked before changing the selection. + * @returns fulfillment after the selection flushes. + * @throws when called by a descendant or persistence fails. An appended selection remains active; + * retrying the same selection flushes without a duplicate event. */ -listMembers(agent: Agent): TeamMemberView[] - -/** - * Create one named, continuable direct child of the Team Lead. - * @param caller - exact live Lead Agent. - * @param request - immutable name, description, prompt, context mode, provider, and cancellation. - * @returns the active roster row. - */ -async spawnTeammate(caller: Agent, request: SpawnTeammateRequest): Promise - -/** - * Steer one peer message into the target inbox or reject the attempt. - * @param caller - exact live sending Team member. - * @param request - target name, content, and cancellation before acceptance. - * @returns accepted inbox identity; acceptance follows normal Agent persistence and does not await model processing. - */ -async sendMessage(caller: Agent, request: SendTeamMessageRequest): Promise +@Remote async setEnabled(agent: Agent, enabled: boolean, signal?: AbortSignal): Promise /** * Create one unowned pending task in the Team Lead log. * @param caller - exact live Team member creating the task. * @param request - task text, blockers, and advisory write scopes. + * @param signal - cancellation before the task event is appended; omitted means no cancellation. * @returns the revision-one task view. + * @throws TEAM_TASK_PERSISTENCE_UNCONFIRMED when the task is visible but durability is unknown; read it before retrying. */ -async createTask(caller: Agent, request: CreateTeamTaskRequest): Promise +async createTask(caller: Agent, request: CreateTeamTaskRequest, signal?: AbortSignal): Promise /** * Return one task, including a deleted tombstone. @@ -196,36 +177,14 @@ listTasks(caller: Agent): TeamTaskView[] * Compare-and-set one authorized task transition. * @param caller - exact live Team member authorizing the mutation. * @param request - task identity, expected revision, action, and action fields. + * @param signal - cancellation before the task event is appended; omitted means no cancellation. * @returns the committed next task revision. + * @throws TEAM_TASK_PERSISTENCE_UNCONFIRMED when the task is visible but durability is unknown; read it before retrying. */ -async updateTask(caller: Agent, request: UpdateTeamTaskRequest): Promise - -/** - * Wait for the next Team-domain or member-status change. - * @param caller - exact live Team member waiting for activity. - * @param timeoutMs - bounded wait duration from ten seconds through one hour. - * @param signal - caller cancellation for the wait only. - * @returns one observed change or a timeout result. - */ -async waitForChange(caller: Agent, timeoutMs: number, signal: AbortSignal): Promise - -/** - * Interrupt one live teammate turn without clearing its pending inbox. - * @param caller - exact live Lead Agent. - * @param targetName - durable teammate name. - * @returns the target status sampled before cancellation. - */ -interrupt(caller: Agent, targetName: string): { previousStatus: 'running' | 'inactive' } - -/** - * Resolve a caller without throwing, used by scoped-tool installation and observers. - * @param agent - candidate exact live Agent. - * @returns Team membership, or undefined for non-Team subagents and stale identities. - */ -tryMembership(agent: Agent): TeamMembership | undefined +async updateTask(caller: Agent, request: UpdateTeamTaskRequest, signal?: AbortSignal): Promise ``` Types: [Agent](core.zh.md) -Source: [`packages/experimental/agent-team/src/index.ts`](../../packages/experimental/agent-team/src/index.ts) +Source: [`packages/team/agent-team/src/index.ts`](../../packages/team/agent-team/src/index.ts) diff --git a/docs/subsystems/core.i18n.yaml b/docs/subsystems/core.i18n.yaml index a2b168f3fd..363584a5fd 100644 --- a/docs/subsystems/core.i18n.yaml +++ b/docs/subsystems/core.i18n.yaml @@ -9,8 +9,8 @@ en: c90cb23528ddb6c8 zh: 62724b7255b586b6 /core/creation-and-ownership: - en: e516e14228c6bd35 - zh: dba25d7da1d5a44e + en: d71c669c1a36bf52 + zh: 9b335f90c84a894e /core/the-agent-handle: en: 6ee15fec81bc0686 zh: b6f3fcd48823ea75 diff --git a/docs/subsystems/core.md b/docs/subsystems/core.md index 48ff2ce633..40264ec898 100644 --- a/docs/subsystems/core.md +++ b/docs/subsystems/core.md @@ -46,9 +46,9 @@ interface AgentHandle { } ``` -`CreateAgentOptions` carries the shared identity and everything a fresh agent needs before publication: an optional live `parentAgent`, session metadata (`meta` — validated `cwd`, fork lineage, the `isSeeded` marker, origin classification, delegation depth, and `agentPreset`), the exact fork cut in sibling field `inheritedEventCount`, an optional `seed` replay prefix, per-agent `AgentOptions`, a creation-only cancellation `signal`, and `setup`. `ResumeAgentOptions` is the persisted-identity counterpart: `resumeSessionId`, `parentAgent`, `agentOptions`, `signal`, and `setup`. The `setup` callback (`AgentSetup`) receives `(agentCtx, agent)` after Session store entry and completed `session/created`, before Agent registration: the context owns scoped registrations, while the explicit Agent supplies the exact child Session without a reverse property on the Context. Everything registered through `agentCtx` exists before `agent/created` and the first prompt assembly. Setup may return a synchronous commit invoked immediately before publication; a setup rejection, commit throw, or owner disposal unwinds the transaction, removes the Session, and preserves restored pending input and already stored events. +`CreateAgentOptions` carries the shared identity and everything a fresh agent needs before publication: session metadata (`meta` — validated `cwd`, fork lineage, the `isSeeded` marker, origin classification, delegation depth, and `agentPreset`), the exact fork cut in sibling field `inheritedEventCount`, an optional `seed` replay prefix, per-agent `AgentOptions`, a creation-only cancellation `signal`, and `setup`. `ResumeAgentOptions` is the persisted-identity counterpart: `resumeSessionId`, `agentOptions`, `signal`, and `setup`. The `setup` callback (`AgentSetup`) receives `(agentCtx, agent)` after Session store entry and completed `session/created`, before Agent registration: the context owns scoped registrations, while the explicit Agent supplies the exact child Session without a reverse property on the Context. Everything registered through `agentCtx` exists before `agent/created` and the first prompt assembly. Setup may return a synchronous commit invoked immediately before publication; a setup rejection, commit throw, or owner disposal unwinds the transaction, removes the Session, and preserves restored pending input and already stored events. -`AgentFactory` is the creation interface behind the registry: the loop registers its factory via `ctx.agents.setFactory()`, so consumers use `ctx.agents` without depending on the concrete loop package. A runtime child creator sets `options.parentAgent`; the registry passes the options and caller Context to the factory without deriving one from the other. The exact `create`/`resume` signatures and rollback contracts are in the [generated section](#ctxagents--agentregistry) below. +`AgentFactory` is the creation interface behind the registry: the loop registers its factory via `ctx.agents.setFactory()`, so consumers use `ctx.agents` without depending on the concrete loop package. The registry passes creation options and the caller Context to the factory; the caller Context owns the returned handle's lifetime. The exact `create`/`resume` signatures and rollback contracts are in the [generated section](#ctxagents--agentregistry) below. ## The agent handle @@ -764,7 +764,7 @@ setFactory(factory: AgentFactory): () => void * agent): this constructs the agent and its session. Rejects if no factory is * registered or creation/setup fails. The resolved {@link AgentHandle} lets * the owner tear down exactly this agent. - * @param options - shared identity, optional live parent, session seed/metadata, and agent options. + * @param options - shared identity, session seed/metadata, and agent options. * @returns the handle after setup, rollback-covered publication, and loop start complete. */ async create(options: CreateAgentOptions): Promise @@ -773,7 +773,7 @@ async create(options: CreateAgentOptions): Promise * Load a persisted session and resume an agent on it through the registered * factory. Rejects if no factory is registered; the factory rejects if * session persistence is not configured or persistence/setup fails. - * @param options - persisted identity, optional live parent, configuration, and setup. + * @param options - persisted identity, configuration, and setup. * @returns the handle after setup, rollback-covered publication, and loop start complete. */ async resume(options: ResumeAgentOptions): Promise @@ -785,8 +785,7 @@ async resume(options: ResumeAgentOptions): Promise * (`scopeTarget(agent, agent)`): the subject is the agent in hand, so the * emits are scope-filtered regardless of which context invoked `register` * (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always - * requires passing the carrier). The entry is a runtime root; factory-backed - * creation uses `options.parentAgent` for child ownership. Await the registration before using the agent. + * requires passing the carrier). Await registration before using the agent. * @param agent - the already-constructed agent to record in the store. * @returns the awaitable Cordis effect disposer (single-shot; a repeat call * returns undefined without awaiting an in-flight teardown). Exact @@ -806,15 +805,12 @@ register(agent: Agent): ReturnType * returned detach closure into its pre-installed composite teardown before * calling {@link announce}. Ordinary callers use {@link register}. * @param agent - the prepared, unpublished agent. - * @param owner - explicitly supplied live runtime owner, or - * undefined for a top-level runtime root. This is runtime ownership, not - * the resumed session's durable parent lineage. * @returns an idempotent closure that removes this exact entry and emits * `agent/disposed` with listener failures contained. When called from a * `agent/created` listener, removal and disposal wait until the serial * creation dispatch settles. */ -enter(agent: Agent, owner: Agent | undefined): () => void +enter(agent: Agent): () => void /** * Announce an agent previously inserted with {@link enter}. @@ -835,29 +831,11 @@ async announce(agent: Agent, source: SessionStartSource, signal?: AbortSignal): */ get(id: SessionId): Agent | undefined -/** - * Test whether a live agent was created through one exact parent agent's - * scoped context. Runtime ownership is independent of durable session - * lineage and remains unambiguous when unrelated providers reuse an id. - * @param id - the candidate child agent's shared agent/session id. - * @param owner - the expected runtime creator agent. - * @returns true only while the exact child entry is live under that owner. - */ -isOwnedBy(id: SessionId, owner: Agent): boolean - /** * All live agents, in registration order. * @returns a fresh array; mutating it does not affect the registry. */ list(): Agent[] - -/** - * All live top-level agents in registration order. A top-level agent was - * created without an owning agent context; durable session lineage does not - * affect this runtime relation, so a resumed fork may still be a root. - * @returns a fresh array; mutating it does not affect the registry. - */ -roots(): Agent[] ``` Types: [Session](session.md) diff --git a/docs/subsystems/core.zh.md b/docs/subsystems/core.zh.md index 19acf51b51..ff1d3b579e 100644 --- a/docs/subsystems/core.zh.md +++ b/docs/subsystems/core.zh.md @@ -48,9 +48,9 @@ interface AgentHandle { } ``` -`CreateAgentOptions` 携带共享标识以及新 agent 发布前所需的一切:可选的存活 `parentAgent`、会话元数据(`meta`——已校验的 `cwd`、fork 谱系、`isSeeded` 标记、来源分类、委派深度与 `agentPreset`)、同级字段 `inheritedEventCount` 所表示的精确 fork cut、可选的 `seed` 回放前缀、按 agent 的 `AgentOptions`、仅创建期有效的取消 `signal`,以及 `setup`。`ResumeAgentOptions` 是持久标识的对应项:`resumeSessionId`、`parentAgent`、`agentOptions`、`signal` 与 `setup`。`setup` 回调(`AgentSetup`)在 Session 入库且 `session/created` 完成后、Agent 注册前接收 `(agentCtx, agent)`:上下文拥有作用域注册,显式 Agent 提供确切的子 Session,Context 无需反向属性。凡经 `agentCtx` 注册的内容都先于 `agent/created` 与第一次提示词组装存在。Setup 可以返回在发布前一刻调用的同步 commit;setup 拒绝、commit 抛出或所有者 dispose(资源释放)都会撤销事务并移除 Session,同时保留恢复出的待处理输入及已存储事件。 +`CreateAgentOptions` 携带共享标识以及新 agent 发布前所需的一切:会话元数据(`meta`——已校验的 `cwd`、fork 谱系、`isSeeded` 标记、来源分类、委派深度与 `agentPreset`)、同级字段 `inheritedEventCount` 所表示的精确 fork cut、可选的 `seed` 回放前缀、按 agent 的 `AgentOptions`、仅创建期有效的取消 `signal`,以及 `setup`。`ResumeAgentOptions` 是持久标识的对应项:`resumeSessionId`、`agentOptions`、`signal` 与 `setup`。`setup` 回调(`AgentSetup`)在 Session 入库且 `session/created` 完成后、Agent 注册前接收 `(agentCtx, agent)`:上下文拥有作用域注册,显式 Agent 提供确切的子 Session,Context 无需反向属性。凡经 `agentCtx` 注册的内容都先于 `agent/created` 与第一次提示词组装存在。Setup 可以返回在发布前一刻调用的同步 commit;setup 拒绝、commit 抛出或所有者 dispose(资源释放)都会撤销事务并移除 Session,同时保留恢复出的待处理输入及已存储事件。 -`AgentFactory` 是注册表背后的创建接口:循环经 `ctx.agents.setFactory()` 注册其工厂,因此消费方使用 `ctx.agents` 时无需依赖具体循环包。运行时子 Agent 的创建方设置 `options.parentAgent`;注册表把 options 与调用方 Context 传给工厂,不从其中一项推导另一项。确切的 `create`/`resume` 签名及回滚约定见下方[生成区块](#ctxagents--agentregistry)。 +`AgentFactory` 是注册表背后的创建接口:循环经 `ctx.agents.setFactory()` 注册其工厂,因此消费方使用 `ctx.agents` 时无需依赖具体循环包。注册表把创建选项和调用方 Context 传给工厂;调用方 Context 拥有返回句柄的生命周期。确切的 `create`/`resume` 签名及回滚约定见下方[生成区块](#ctxagents--agentregistry)。 @@ -776,7 +776,7 @@ setFactory(factory: AgentFactory): () => void * agent): this constructs the agent and its session. Rejects if no factory is * registered or creation/setup fails. The resolved {@link AgentHandle} lets * the owner tear down exactly this agent. - * @param options - shared identity, optional live parent, session seed/metadata, and agent options. + * @param options - shared identity, session seed/metadata, and agent options. * @returns the handle after setup, rollback-covered publication, and loop start complete. */ async create(options: CreateAgentOptions): Promise @@ -785,7 +785,7 @@ async create(options: CreateAgentOptions): Promise * Load a persisted session and resume an agent on it through the registered * factory. Rejects if no factory is registered; the factory rejects if * session persistence is not configured or persistence/setup fails. - * @param options - persisted identity, optional live parent, configuration, and setup. + * @param options - persisted identity, configuration, and setup. * @returns the handle after setup, rollback-covered publication, and loop start complete. */ async resume(options: ResumeAgentOptions): Promise @@ -797,8 +797,7 @@ async resume(options: ResumeAgentOptions): Promise * (`scopeTarget(agent, agent)`): the subject is the agent in hand, so the * emits are scope-filtered regardless of which context invoked `register` * (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always - * requires passing the carrier). The entry is a runtime root; factory-backed - * creation uses `options.parentAgent` for child ownership. Await the registration before using the agent. + * requires passing the carrier). Await registration before using the agent. * @param agent - the already-constructed agent to record in the store. * @returns the awaitable Cordis effect disposer (single-shot; a repeat call * returns undefined without awaiting an in-flight teardown). Exact @@ -818,15 +817,12 @@ register(agent: Agent): ReturnType * returned detach closure into its pre-installed composite teardown before * calling {@link announce}. Ordinary callers use {@link register}. * @param agent - the prepared, unpublished agent. - * @param owner - explicitly supplied live runtime owner, or - * undefined for a top-level runtime root. This is runtime ownership, not - * the resumed session's durable parent lineage. * @returns an idempotent closure that removes this exact entry and emits * `agent/disposed` with listener failures contained. When called from a * `agent/created` listener, removal and disposal wait until the serial * creation dispatch settles. */ -enter(agent: Agent, owner: Agent | undefined): () => void +enter(agent: Agent): () => void /** * Announce an agent previously inserted with {@link enter}. @@ -847,29 +843,11 @@ async announce(agent: Agent, source: SessionStartSource, signal?: AbortSignal): */ get(id: SessionId): Agent | undefined -/** - * Test whether a live agent was created through one exact parent agent's - * scoped context. Runtime ownership is independent of durable session - * lineage and remains unambiguous when unrelated providers reuse an id. - * @param id - the candidate child agent's shared agent/session id. - * @param owner - the expected runtime creator agent. - * @returns true only while the exact child entry is live under that owner. - */ -isOwnedBy(id: SessionId, owner: Agent): boolean - /** * All live agents, in registration order. * @returns a fresh array; mutating it does not affect the registry. */ list(): Agent[] - -/** - * All live top-level agents in registration order. A top-level agent was - * created without an owning agent context; durable session lineage does not - * affect this runtime relation, so a resumed fork may still be a root. - * @returns a fresh array; mutating it does not affect the registry. - */ -roots(): Agent[] ``` Types: [Session](session.zh.md) diff --git a/docs/subsystems/slots.i18n.yaml b/docs/subsystems/slots.i18n.yaml index 0a2bb60f40..3397485d88 100644 --- a/docs/subsystems/slots.i18n.yaml +++ b/docs/subsystems/slots.i18n.yaml @@ -3,8 +3,8 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/slots.md /web-client-slots: - en: 808a284f5a4280b5 - zh: 1650d6c3a94eae6e + en: 15c49af3ed8b57ff + zh: e7f9189de6f4334c /web-client-slots/declaration-and-lifecycle: en: f2d2c0d3b8f14ec7 zh: 441ed13ff3f81d2a diff --git a/docs/subsystems/slots.md b/docs/subsystems/slots.md index fe8589f685..1e551c2e51 100644 --- a/docs/subsystems/slots.md +++ b/docs/subsystems/slots.md @@ -10,10 +10,12 @@ This page documents slot ownership, component inputs, extension APIs, and the sh The plugin catalog declares `plugins.add.actions`, a root-scoped list for `MenuItemButton` rows after installation in the Add plugin menu. Its owner supplies `onDismiss()` to close the menu before starting an action. The slot follows the owning Plugins page declaration lifetime. -The root-scoped lists `sidebar.panellist.actions`, `conversation.session.header.views.actions`, and `sidebar.right.tab.guide.actions` supply the rows of the More menus after the panel rows, the View tabs, and the right-sidebar guide cards. A region draws its More button and menu only while its list has registrations, and passes the customizable entries with their pin state, `onOpen`, `onPinnedChange`, and `onDismiss`. Panel and View entries also carry optional `active` state for the current selection, independently of pin state. Each region keeps its own unpinned entry ids in memory; [`ui-entry-customization`](../../packages/client/ui-entry-customization/README.md) registers the shipped rows. +The root-scoped lists `sidebar.panellist.actions`, `conversation.session.header.views.actions`, and `sidebar.right.tab.guide.actions` supply the rows of the More menus after the panel rows, the View tabs, and the right-sidebar guide cards. A region draws its More button and menu only while its list has registrations, and passes the customizable entries with their pin state, `onOpen`, `onPinnedChange`, and `onDismiss`. Panel and View entries also carry optional `active` state for the current selection, independently of pin state. The `sidebar.panel.icon` root-scoped factory owns the `sidebar.panellist` list and renders its registered icon by `id`, `size`, and `active` for both sidebar rows and More entries. Each region keeps its own unpinned entry ids in memory; [`ui-entry-customization`](../../packages/client/ui-entry-customization/README.md) registers the shipped rows. [ui-workspace](../../packages/client/ui-workspace/README.md#understand-the-implementation) documents Session-row extension positions, visibility, and keyboard behavior. +[ui-settings](../../packages/client/ui-settings/README.md#filling-the-settings-slots) documents the settings page and navigation-icon extension points. + ## Declaration and lifecycle `SlotMap` is the compile-time registry. A package declaration-merges the key, cardinality, scope, owner props, keyed props, and optional slot-level inject face. The runtime declaration is the matching `children` entry on the component that owns the render location. diff --git a/docs/subsystems/slots.zh.md b/docs/subsystems/slots.zh.md index 288b4b85f1..8260a34075 100644 --- a/docs/subsystems/slots.zh.md +++ b/docs/subsystems/slots.zh.md @@ -10,10 +10,12 @@ Slots 是 Web Client 的类型化 React 组合系统。[`dsh-client-ui-slots`](. 插件目录声明 `plugins.add.actions`,它是 root 作用域的 list,在添加插件菜单的安装项之后提供 `MenuItemButton` 行。owner 提供 `onDismiss()`,用于开始动作前关闭菜单。该 slot 遵循所属插件页面声明的生命周期。 -root 作用域的 list `sidebar.panellist.actions`、`conversation.session.header.views.actions` 和 `sidebar.right.tab.guide.actions` 提供面板行、View 标签和右侧栏引导卡片之后“更多”菜单的菜单行。区域只在其 list 有注册项时画“更多”按钮与菜单,并传入可自定义的入口及其 Pin 状态、`onOpen`、`onPinnedChange` 与 `onDismiss`。面板和 View 入口还通过可选的 `active` 状态标识当前选中项,该状态独立于 Pin 状态。各区域在内存中维护已收起的入口 id;[`ui-entry-customization`](../../packages/client/ui-entry-customization/README.zh.md) 注册随产品提供的菜单行。 +root 作用域的 list `sidebar.panellist.actions`、`conversation.session.header.views.actions` 和 `sidebar.right.tab.guide.actions` 提供面板行、View 标签和右侧栏引导卡片之后“更多”菜单的菜单行。区域只在其 list 有注册项时画“更多”按钮与菜单,并传入可自定义的入口及其 Pin 状态、`onOpen`、`onPinnedChange` 与 `onDismiss`。面板和 View 入口还通过可选的 `active` 状态标识当前选中项,该状态独立于 Pin 状态。root 作用域的 `sidebar.panel.icon` factory 拥有 `sidebar.panellist` list,按 `id`、`size` 与 `active` 为侧栏行和更多菜单入口渲染已注册图标。各区域在内存中维护已收起的入口 id;[`ui-entry-customization`](../../packages/client/ui-entry-customization/README.zh.md) 注册随产品提供的菜单行。 [ui-workspace](../../packages/client/ui-workspace/README.zh.md#understand-the-implementation) 记录 Session 行的扩展位置、显示条件和键盘行为。 +[ui-settings](../../packages/client/ui-settings/README.zh.md#filling-the-settings-slots) 记录设置页面与导航图标扩展点。 + ## 声明与生命周期 `SlotMap` 是编译期注册表。包通过声明合并写入 key、cardinality(基数)、scope、owner props、keyed props 与可选的 slot 级 inject face。运行时声明则是拥有该渲染位置的组件在 `children` 中给出的对应条目。 diff --git a/docs/subsystems/subagent.i18n.yaml b/docs/subsystems/subagent.i18n.yaml index 21fa5dbbde..b2d7d88b37 100644 --- a/docs/subsystems/subagent.i18n.yaml +++ b/docs/subsystems/subagent.i18n.yaml @@ -3,26 +3,26 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/subagent.md /subagents: - en: 9d48a27a49585a3d - zh: 89da32c8a16c290a + en: 3d828004dee084c6 + zh: f71296b677691151 /subagents/capabilities-and-activation-requests: - en: 8c92b131f67997f6 - zh: c2dc606a0859420c + en: f09270fcd7bfa26a + zh: 082664920b3c65e2 /subagents/local-children-and-activations: en: 6f87231d18d82916 zh: 9f064b23058f8766 /subagents/messages-and-interruption: - en: 623b06b3c12540bc - zh: f56c6341e1c62768 + en: 9befacb931520842 + zh: c214b4fffa779b06 /subagents/durable-enumeration: - en: 30c3607a969696ce - zh: e6359cdf22b1e235 + en: e02e1703b183594c + zh: 130d475a4304f453 /subagents/results-and-backend-handles: en: 69ea5bed7d9fb64a zh: b187a822b813e80a /subagents/the-provider-contract-subagentprovider: en: 226ba8094a8cf96c zh: 719678662905d1eb -/subagents/durable-enumeration-listchildren-listdescendants-and-their-entries: - en: d8ce7969b65ad86e - zh: d39dac6592681bf2 +/subagents/durable-enumeration-listdescendants-and-catalog-entries: + en: 8000138f84fd15fa + zh: 32fa91d3aebd3d01 diff --git a/docs/subsystems/subagent.md b/docs/subsystems/subagent.md index b54ce57175..0e3235cf91 100644 --- a/docs/subsystems/subagent.md +++ b/docs/subsystems/subagent.md @@ -2,13 +2,13 @@ English | [中文](subagent.zh.md) -The unified activation API manages local conversations and external executions. +The unified activation API manages local conversations and external executions. Creation ancestry, runtime ownership, and communication belong to the subagent tree. [Subagent controls](../../packages/subagent/tool-subagent-control/README.md) own agent identity and exact-caller inbox waiting; Team tools add shared tasks and task policy. ## Capabilities and activation requests Every backend uses startActivation. Capability flags validate request options; prepareContinuable selects local Agent creation, while start selects one external execution. The model-facing tool uses parent delivery. Workflow and code mode consumers can await caller results without injecting another parent message. Historical children with unavailable descriptors remain discoverable as `mode: 'unknown'` without granting continuation capabilities; the [package README](../../packages/subagent/subagent/README.md) defines catalog persistence. -Optional `request.cwd` selects the child's initial directory; relative paths resolve against the parent's current effective directory. Omitting it captures that directory at admission. Local child Sessions retain the parent's original project, and cold resume retains their logged directory state. +Optional `cwd` selects the child's initial directory; relative paths resolve against the parent's current effective directory. Omitting it captures that directory at admission. Local child Sessions retain the parent's original project, and cold resume retains their logged directory state. ```ts type-equiv /** @@ -28,15 +28,13 @@ interface SubagentCapabilities { ```ts type-equiv /** One managed child execution and its result delivery policy. */ -interface SubagentActivationSpec { +interface SubagentActivationSpec extends Omit { /** Registered backend to use. */ readonly provider: string /** Short task label retained in cataloged child membership. */ readonly label: string /** Optional reserved identity for a local child; external backends allocate their own ids. */ readonly childId?: SessionId - /** Task, parent, and backend-supported execution options. */ - readonly request: Omit /** Cancellation before publication; callers own later cancellation through dispose. */ readonly signal: AbortSignal /** Parent delivery notifies the model; caller delivery only returns the result. Local children always enter the parent catalog. */ @@ -56,16 +54,15 @@ interface SubagentActivation { * Teardown failures are reported by dispose() without replacing a captured result. */ readonly result: Promise - /** Stop and release this activation and its owned descendants; rejects on teardown failure. */ + /** Stop and release only this activation; rejects on teardown failure. */ dispose(): Promise } ``` ```ts type-equiv /** - * Task and optional capabilities supplied to a backend. startActivation carries - * these fields in request while owning its label, cancellation, and delivery - * policy separately. + * Task and optional capabilities supplied to a backend. The activation spec + * shares these task options and defines its own label and cancellation policy. */ interface SubagentStartRequest { /** Initial child directory; relative paths resolve against the parent's current directory. Omitted inherits that directory at start. */ @@ -173,7 +170,7 @@ interface ContinuableCreateSpec { ## Messages and interruption -sendMessage authorizes the exact live sender and permits only adjacent local agents. Running children receive steering input; absent continuable children resume from persistence. External executions reject follow-ups. Both local and external executions automatically report their final text, structured result, diagnostic, and status in the parent settlement notice. Local children can also send messages through send_message. caller delivery suppresses settlement notices. drainDescendants and drainChildren await owned work teardown for local and external activations. +`sendMessage` requires the exact live sender and a target Session id in the same root tree. Root and continuable members can message across branches; admission revalidates the sender and root. Running targets receive Steer; only an absent target cold-resumes, while its creation ancestors may remain offline. External executions reject follow-ups. Local and external executions report final text, structured output, diagnostics, and status to their direct creator only while it is online; caller delivery suppresses those notices. `drainDescendants` awaits whole-tree teardown; an activation receipt’s `dispose()` releases only that execution. ```ts type-equiv /** Durable attribution for one model-authored message between Agents. */ @@ -200,13 +197,15 @@ interface SubagentSettledMessageSource { readonly form: 'notice' /** One-line account of how the child ended. */ readonly summary: string + /** Exact execution stop reason; absent on historical notices. Writers always include it. */ + readonly stopReason?: SubagentResult['stopReason'] /** Session id of the child that settled. */ readonly senderSessionId: SessionId } ``` ```ts type-equiv -/** Options for one model-authored message between adjacent Agents. */ +/** Options for one model-authored message within a subagent tree. */ interface SubagentSendMessageOptions { /** Caller cancellation, owning the operation only until inbox acceptance. */ readonly signal: AbortSignal @@ -215,18 +214,18 @@ interface SubagentSendMessageOptions { ```ts type-equiv /** - * Authority under which one interrupt request is admitted. `user` carries the - * durable direct-parent address a human client presented; `ancestor` carries - * the exact live Agent object whose recorded lineage must contain the caller. + * Authority under which one interrupt request is admitted. `user` is a trusted + * host operation; Remote callers need permission on the target Session before + * invoking it. `ancestor` carries an exact live Agent in the target's creation ancestry. */ type SubagentInterruptAuthority = - | { readonly kind: 'user'; readonly parentSessionId: SessionId } + | { readonly kind: 'user' } | { readonly kind: 'ancestor'; readonly agent: Agent } ``` ## Durable enumeration -`listChildren` reads the parent-owned catalog of all local children and parent-delivery external children. Caller-delivery external executions leave membership and result collection to their caller; local workflow children remain discoverable after completion. External entries carry `mode: 'external'` and cannot open a child Session or accept follow-ups. Catalog entries record membership at creation without execution status. `list_agents` exposes direct continuable children with `running` or `inactive` activity; see the [control tool](../../packages/subagent/tool-subagent-control/README.md#list_agents). +The parent-owned `subagentCatalog` projection lists all local children and parent-delivery external children. Caller-delivery external executions leave membership and result collection to their caller; local workflow children remain discoverable after completion. External entries carry `mode: 'external'` and cannot open a child Session or accept follow-ups. Catalog entries record membership at creation without execution status. `list_agents` exposes the root and reachable continuable descendants with `running` or `inactive` activity; see the [control tool](../../packages/subagent/tool-subagent-control/README.md#list_agents). ## Results and backend handles @@ -372,11 +371,11 @@ interface SubagentProvider { } ``` -## Durable enumeration: `listChildren()`, `listDescendants()`, and their entries +## Durable enumeration: `listDescendants()` and catalog entries The model-facing `list_agents` adapter reports current activity as `running` or `inactive`. These values do not describe task completion or guarantee that `send_message` will succeed. -`SubagentRuntime.listChildren(parentSessionId, signal?)` reads the parent's `subagentCatalog` view through a live-preferred Session observation and releases that observation on success or failure. It returns direct-child entries in parent event order without reading child logs or enumerating the Session corpus. Query failures propagate; a missing catalog projection fails explicitly. Browser rows derive membership from the shared projection store and add activity from Session status; the control stream pushes complete catalog updates. `listDescendants()` recursively reads those catalogs and derives `hasChildren` from each child catalog. [The parent-catalog Agent Note](../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md) owns creation, fork isolation, ordering, and persistence costs. +Direct-child entries are available as `observation.projections?.values.subagentCatalog` through `ctx.sessionQuery.observeSession(parentSessionId, { signal })`; callers release the observation with `using`. Entries follow parent event order without reading child logs or enumerating the Session corpus. Query failures propagate; a missing catalog projection means discovery is unavailable. Browser rows derive membership from the shared projection store and add activity from Session status; the control stream pushes complete catalog updates. `listDescendants()` recursively reads those catalogs and derives `hasChildren` from each child catalog. [The parent-catalog Agent Note](../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md) owns creation, fork isolation, ordering, and persistence costs. `SubagentRuntime.listDescendants(rootSessionId)` recursively calls the same catalog reader in stable pre-order, preserving each parent's event order. External entries are leaves without local Sessions. One-shot and unknown-mode entries remain traversal nodes; unknown modes produce `unsupported` diagnostics. An unreadable child catalog produces `corrupt` or `unavailable` and stops only that branch. Root read failures, missing services or projections, and cancellation reject the listing. Each reachable catalog is observed once and released before the next read; repeated ids and cycles are skipped. Sessions absent from reachable catalogs are not discovered, including ordinary Session forks and any subagents below those forks. Each row carries its catalog parent and root-relative depth: @@ -437,32 +436,39 @@ startActivation(spec: SubagentActivationSpec & { readonly childId: SessionId }): startActivation(spec: SubagentActivationSpec): Promise /** - * Join progressing descendants without cancelling them. Idle descendants whose - * inboxes require a later wake stay resident and do not delay host completion. - * @param parent - the exact parent whose descendant work is observed. - * @returns whether work was joined; hosts recheck parent idle after true. + * Resolve the root shared by an Agent and its other agents in its subagent tree. + * @param caller - exact live Agent whose subagent ancestry selects the tree. + * @returns the live root; ordinary Session forks lead independent trees. + * @throws when the caller or its runtime owner is unavailable. */ -async waitForChildren(parent: Agent): Promise +rootOf(caller: Agent): Agent /** - * Steer one model-authored message to the sender's direct parent or direct - * continuable child. A running target admits it at the nearest step boundary; - * an idle target starts a turn, and an absent direct child cold-resumes from - * persistence. The service derives durable sender attribution from the exact - * live sender. Caller cancellation stops only pre-acceptance work. - * @param sender - exact live Agent authorizing and originating the message. - * @param targetId - durable direct-parent or direct-child session id. + * Join progressing subagent tree work for its root. Idle members with parked input + * stay resident without delaying host completion. Ordinary members return false. + * @param root - exact live subagent tree root whose member work is awaited. + * @returns whether work was joined; hosts recheck root idle after true. + */ +async waitForTreeWork(root: Agent): Promise + +/** + * Steer one model-authored message to another member of the same subagent tree. + * A running target receives it at its next step; an idle target starts a turn. + * A cold target resumes under its runtime subagent tree root without restoring + * its creation ancestors. Sender identity is rechecked before inbox acceptance; cancellation + * stops only pre-acceptance work and never retracts an accepted message. + * @param sender - exact live root or resident continuable Agent originating the message. + * @param targetId - another root or continuable member's durable Session id. * @param content - model-authored content to deliver. * @param options - caller cancellation before inbox acceptance. * @returns the accepted message's inbox id. - * @throws when continuation services are unavailable, adjacency is rejected, - * or the message was not admitted. + * @throws when the target is outside the tree, cannot resume, or rejects input. */ async sendMessage( sender: Agent, targetId: SessionId, content: ContentBlock[], options: SubagentSendMessageOptions, ): Promise /** - * Interrupt one live child's current execution under a human parent - * address or an exact live ancestor Agent. Fire-and-return: the cancel + * Interrupt one live child's current execution under a trusted user + * operation or an exact live ancestor Agent. Fire-and-return: the cancel * signal is issued before this returns, but the target may keep running * until it observes the signal. Unclaimed pending inbox work, the Activation, * and published descendants are preserved; claimed work is not requeued. @@ -471,47 +477,22 @@ async sendMessage( sender: Agent, targetId: SessionId, content: ContentBlock[], * is an accepted no-op, as is a manager-less composition, which cannot own a * live Activation. * @param targetSessionId - the durable child session id to interrupt. - * @param authority - the human parent address or exact live ancestor Agent. + * @param authority - a trusted user operation or exact live ancestor Agent. * @throws {SubagentError} `UNAUTHORIZED` when the authority does not own the * live target. */ interrupt(targetSessionId: SessionId, authority: SubagentInterruptAuthority): void /** - * Close subagent admission below exact live parent Agents, stop only their - * visible descendant Activations synchronously, then await admitted scoped - * materializations and release those forests child-first. The scoped cutoff - * lasts until each exact parent leaves the registry; unrelated parent trees - * remain live. - * @param parents - exact host-owned parent Agents entering teardown. - * @returns once every retained descendant activation released its execution handle. + * Close admission and stop all instances in the selected subagent trees, then await + * pending startup and cleanup. Ordinary members are ignored. The cutoff lasts + * until each exact root leaves the registry; unrelated subagent trees remain live. + * @param parents - exact live subagent tree roots entering teardown. + * @returns once all admitted startup and member cleanup have settled. * @throws an aggregate error after all branches settle when any failed. */ async drainDescendants(parents: readonly Agent[]): Promise -/** - * Release selected resident direct children of one exact live - * parent. Other children of the same parent remain admitted and resident. - * Absent targets and a manager-less composition are accepted no-ops. - * @param parent - exact live direct parent authorizing the selected release. - * @param childIds - durable direct-child ids to release when resident. - * @returns once every selected activation released its execution handle. - * @throws {SubagentError} `UNAUTHORIZED` when a resident target belongs to a - * different parent or the supplied parent identity is stale. - */ -async drainChildren(parent: Agent, childIds: readonly SessionId[]): Promise - -/** - * Read the parent's durable direct-child catalog without loading or resuming an Agent. - * The service owns and releases the live-preferred Session observation. - * @param parentSessionId - parent whose direct children are requested. - * @param signal - cancellation forwarded to the Session query. - * @returns catalog children in parent event order. - * @throws {@link SubagentError} when query or catalog projection is unavailable. - * @throws SessionQueryError when the parent cannot be read or the query is cancelled. - */ -listChildren(parentSessionId: SessionId, signal?: AbortSignal): Promise - /** * Recursively list reachable parent catalogs in stable pre-order, preserving * each catalog's event order. Each row carries its catalog parent and depth; @@ -531,8 +512,8 @@ listChildren(parentSessionId: SessionId, signal?: AbortSignal): Promise /** - * Deliver one browser-authored message to a continuable child through the - * exact live direct parent, retaining the caller-minted request identity and + * Deliver one browser-authored message to a continuable child through its + * live subagent tree root, retaining the caller-minted request identity and * validated browser zone on the accepted message. Success identifies the * message the child's inbox accepted; later execution is independent of this * call. Queue delivery targets a later turn; steer delivery targets the @@ -540,31 +521,27 @@ listDescendants(rootSessionId: SessionId, signal?: AbortSignal): Promise +@Remote({ name: 'prompt', resources: { session: request => request.childSessionId } }) async prompt(request: SubagentPromptRequest, signal: AbortSignal): Promise /** - * Remote face of {@link interrupt} under one durable parent address. No - * catalog, history, persistence, or parent Agent lookup runs: the core - * primitive alone authorizes the address against the live Activation, which - * is what keeps a live child interruptible while its parent Agent is offline. + * Remote face of {@link interrupt}, authorized on the target Session by the Gateway. + * No catalog, history, persistence, or parent Agent lookup runs. * Absent, idle, and already-completed targets are accepted no-ops there. * @param childSessionId - durable child session id to interrupt. - * @param parentSessionId - durable direct parent whose authority is claimed. - * @param mode - required continuable-address discriminator. * @returns acknowledgement that the cancel signal was admitted, not that the target is quiescent. * @throws {RemoteError} `gateway/bad-request` for an empty id, - * `subagent/unauthorized` when the address does not own the live target, * otherwise `gateway/internal`. */ -@Remote('interruptByParent') interruptByParent( childSessionId: SessionId, parentSessionId: SessionId, mode: 'continuable', ): SubagentInterruptReceipt +@Remote({ name: 'interruptByUser', resources: { session: childSessionId => childSessionId } }) interruptByUser(childSessionId: SessionId): SubagentInterruptReceipt /** * Register a provider under its name. Registration is effect-scoped and HMR @@ -582,12 +559,6 @@ registerProvider(provider: SubagentProvider): () => void * @returns the provider, or undefined when absent. */ getProvider(name: string): SubagentProvider | undefined - -/** - * List registered provider names in insertion order. - * @returns the registered names. - */ -list(): string[] ``` Types: [Agent](core.md) · [ContentBlock](llm-streaming.md) · [MessageId](llm-streaming.md) · [SessionId](core.md) @@ -602,14 +573,14 @@ Source: [`packages/subagent/subagent/src/index.ts`](../../packages/subagent/suba #### `subagent/end` — emit -A published child settled. Scope-filtered dispatch uses the same delegating parent carrier as `subagent/start`, so the lifecycle pair reaches the same scoped audience. +A published child settled. Scope-filtered dispatch uses the same subagent tree-root carrier as `subagent/start`, so the lifecycle pair reaches the same scoped audience. ```ts cordis-catalog /** - * A published child settled. Scope-filtered dispatch uses the same delegating - * parent carrier as `subagent/start`, so the lifecycle pair reaches the + * A published child settled. Scope-filtered dispatch uses the same subagent tree-root + * carrier as `subagent/start`, so the lifecycle pair reaches the * same scoped audience. - * @param info - the run identity and terminal outcome. + * @param info - run identity, durable direct creator `parentSessionId`, and terminal outcome. * @mode emit */ 'subagent/end'(this: Scoped, info: SubagentRunEndInfo): void @@ -657,16 +628,16 @@ Source: [`packages/subagent/subagent/src/index.ts`](../../packages/subagent/suba #### `subagent/start` — emit -A provider established a published child. For in-process providers, `ctx.agents.get(info.id)` resolves during this notification. Scope-filtered dispatch keys the carrier by the delegating parent, so a parent-scoped listener observes only its own delegations. Paired with `subagent/end`. +A provider established a published child. For in-process providers, `ctx.agents.get(info.id)` resolves during this notification. Scope-filtered dispatch keys the carrier by the subagent tree root, so a root-scoped listener observes all subagent tree activations. Paired with `subagent/end`. ```ts cordis-catalog /** * A provider established a published child. For in-process providers, * `ctx.agents.get(info.id)` resolves during this notification. - * Scope-filtered dispatch keys the carrier by the delegating parent, so a - * parent-scoped listener observes only its own delegations. Paired with + * Scope-filtered dispatch keys the carrier by the subagent tree root, so a + * root-scoped listener observes all subagent tree activations. Paired with * `subagent/end`. - * @param info - the provider and published child identity. + * @param info - provider, child identity, and durable direct creator `parentSessionId`. * @mode emit */ 'subagent/start'(this: Scoped, info: SubagentRunInfo): void diff --git a/docs/subsystems/subagent.zh.md b/docs/subsystems/subagent.zh.md index 0ca87c8503..785644d1cf 100644 --- a/docs/subsystems/subagent.zh.md +++ b/docs/subsystems/subagent.zh.md @@ -2,13 +2,13 @@ [English](subagent.md) | 中文 -统一的 activation API 管理本地会话和外部执行。 +统一的 activation API 管理本地对话和外部执行。创建祖先关系、运行归属和通信由 subagent 树管理。[subagent 控制工具](../../packages/subagent/tool-subagent-control/README.zh.md)负责 agent 身份和当前调用者的收件箱等待;Team 工具添加共享任务与任务策略。 ## 能力与 activation 请求 所有后端都使用 startActivation。能力标记校验请求选项;prepareContinuable 选择本地 Agent 创建,start 选择一次外部执行。面向模型的工具采用 parent 回传。Workflow 与 code mode 消费者可以等待 caller 结果,不额外向父代理注入消息。历史子会话的 descriptor 不可用时,以 `mode: 'unknown'` 保持可发现性,但不授予继续执行能力;[包 README](../../packages/subagent/subagent/README.zh.md) 定义目录持久化语义。 -可选的 `request.cwd` 指定子级的初始目录;相对路径以父级当前有效目录为基准解析。省略时,在准入阶段捕获该目录。本地子 Session 保留父级的原始项目,冷恢复保留其日志中的目录状态。 +可选的 `cwd` 指定子级的初始目录;相对路径以父级当前有效目录为基准解析。省略时,在准入阶段捕获该目录。本地子 Session 保留父级的原始项目,冷恢复保留其日志中的目录状态。 ```ts type-equiv /** @@ -28,15 +28,13 @@ interface SubagentCapabilities { ```ts type-equiv /** One managed child execution and its result delivery policy. */ -interface SubagentActivationSpec { +interface SubagentActivationSpec extends Omit { /** Registered backend to use. */ readonly provider: string /** Short task label retained in cataloged child membership. */ readonly label: string /** Optional reserved identity for a local child; external backends allocate their own ids. */ readonly childId?: SessionId - /** Task, parent, and backend-supported execution options. */ - readonly request: Omit /** Cancellation before publication; callers own later cancellation through dispose. */ readonly signal: AbortSignal /** Parent delivery notifies the model; caller delivery only returns the result. Local children always enter the parent catalog. */ @@ -56,16 +54,15 @@ interface SubagentActivation { * Teardown failures are reported by dispose() without replacing a captured result. */ readonly result: Promise - /** Stop and release this activation and its owned descendants; rejects on teardown failure. */ + /** Stop and release only this activation; rejects on teardown failure. */ dispose(): Promise } ``` ```ts type-equiv /** - * Task and optional capabilities supplied to a backend. startActivation carries - * these fields in request while owning its label, cancellation, and delivery - * policy separately. + * Task and optional capabilities supplied to a backend. The activation spec + * shares these task options and defines its own label and cancellation policy. */ interface SubagentStartRequest { /** Initial child directory; relative paths resolve against the parent's current directory. Omitted inherits that directory at start. */ @@ -173,7 +170,7 @@ interface ContinuableCreateSpec { ## 消息与中断 -sendMessage 根据确切的在线发送者实例授权,仅允许相邻本地代理通信。运行中的子代理接收引导输入;不在线的可继续子代理从持久化恢复。外部执行拒绝后续消息。本地与外部执行都会通过父级结束通知自动回传最终文本、结构化结果、诊断和状态。本地子代理也可以通过 send_message 发送消息。caller 回传会抑制结束通知。drainDescendants 与 drainChildren 对本地和外部 activation 都等待所属任务清理完成。 +`sendMessage` 要求确切在线发送方和同一根树中的目标 Session id。根和可继续成员可跨分支发消息;接纳时重新校验发送方和根。运行中目标接收 Steer;只冷恢复不在线的目标,其创建祖先可以保持离线。外部执行拒绝后续输入。本地和外部执行仅向仍在线的直接创建者报告最终文本、结构化输出、诊断和状态;caller 投递抑制这些通知。`drainDescendants` 等待整棵树释放;activation 回执的 `dispose()` 仅释放那一次执行。 ```ts type-equiv /** Durable attribution for one model-authored message between Agents. */ @@ -200,13 +197,15 @@ interface SubagentSettledMessageSource { readonly form: 'notice' /** One-line account of how the child ended. */ readonly summary: string + /** Exact execution stop reason; absent on historical notices. Writers always include it. */ + readonly stopReason?: SubagentResult['stopReason'] /** Session id of the child that settled. */ readonly senderSessionId: SessionId } ``` ```ts type-equiv -/** Options for one model-authored message between adjacent Agents. */ +/** Options for one model-authored message within a subagent tree. */ interface SubagentSendMessageOptions { /** Caller cancellation, owning the operation only until inbox acceptance. */ readonly signal: AbortSignal @@ -215,18 +214,18 @@ interface SubagentSendMessageOptions { ```ts type-equiv /** - * Authority under which one interrupt request is admitted. `user` carries the - * durable direct-parent address a human client presented; `ancestor` carries - * the exact live Agent object whose recorded lineage must contain the caller. + * Authority under which one interrupt request is admitted. `user` is a trusted + * host operation; Remote callers need permission on the target Session before + * invoking it. `ancestor` carries an exact live Agent in the target's creation ancestry. */ type SubagentInterruptAuthority = - | { readonly kind: 'user'; readonly parentSessionId: SessionId } + | { readonly kind: 'user' } | { readonly kind: 'ancestor'; readonly agent: Agent } ``` ## 持久枚举 -`listChildren` 读取父级拥有的目录,其中包含所有本地子级及 parent 投递的外部子级。caller 投递的外部执行由调用方负责成员关系与结果收集;本地工作流子级在完成后仍可被发现。外部条目携带 `mode: 'external'`,不能打开子 Session 或接受后续消息。目录在创建时记录成员关系,不携带执行状态。`list_agents` 展示可继续的直接子级,其活跃状态为 `running` 或 `inactive`;参见[控制工具](../../packages/subagent/tool-subagent-control/README.zh.md#list_agents)。 +父级拥有的 `subagentCatalog` 投影列出所有本地子级及 parent 投递的外部子级。caller 投递的外部执行由调用方负责成员关系与结果收集;本地工作流子级在完成后仍可被发现。外部条目携带 `mode: 'external'`,不能打开子 Session 或接受后续消息。目录在创建时记录成员关系,不携带执行状态。`list_agents` 展示根和可达的可继续后代,其活跃状态为 `running` 或 `inactive`;参见[控制工具](../../packages/subagent/tool-subagent-control/README.zh.md#list_agents)。 ## 结果与后端句柄 @@ -372,11 +371,11 @@ interface SubagentProvider { } ``` -## 持久化枚举:`listChildren()`、`listDescendants()` 与其条目 +## 持久化枚举:`listDescendants()` 与目录条目 模型侧的 `list_agents` 适配器将当前活动表示为 `running` 或 `inactive`。这些值不描述任务完成情况,也不保证 `send_message` 会成功。 -`SubagentRuntime.listChildren(parentSessionId, signal?)` 通过优先使用在线 Session 的观察读取父会话的 `subagentCatalog` 视图,并在成功或失败时释放观察。它按父会话事件顺序返回直接子级条目,不读取子级日志,也不枚举 Session 语料库。查询失败直接传播;缺少目录投影时显式失败。浏览器条目从共享 projection store 派生成员关系,并从 Session 状态补充活动状态;control stream 推送完整目录更新。`listDescendants()` 递归读取这些目录,并从每个子级目录推导 `hasChildren`。[父目录 Agent Note](../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md) 说明创建、fork 隔离、排序和持久化成本。 +通过 `ctx.sessionQuery.observeSession(parentSessionId, { signal })`,可从 `observation.projections?.values.subagentCatalog` 获取直接子级条目;调用方使用 `using` 释放观察对象。条目遵循父会话事件顺序,不读取子级日志,也不枚举 Session 语料库。查询失败直接传播;缺少目录投影意味着发现功能不可用。浏览器条目从共享 projection store 派生成员关系,并从 Session 状态补充活动状态;control stream 推送完整目录更新。`listDescendants()` 递归读取这些目录,并从每个子级目录推导 `hasChildren`。[父目录 Agent Note](../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md) 说明创建、fork 隔离、排序和持久化成本。 `SubagentRuntime.listDescendants(rootSessionId)` 以稳定前序递归调用同一目录读取函数,保留每个父级的事件顺序。外部条目是没有本地 Session 的叶节点。一次性和未知模式条目仍是遍历节点;未知模式产生 `unsupported` 诊断。无法读取的子级目录产生 `corrupt` 或 `unavailable`,只停止该分支。根读取失败、服务或投影缺失、取消会使整个查询失败。每个可达目录只观察一次,并在下一次读取前释放;重复 id 和循环引用会被跳过。可达目录中不存在的 Session 不会被发现,包括普通 Session fork 及其下的子 agent。每行携带目录中的父级和相对根的深度: @@ -439,32 +438,39 @@ startActivation(spec: SubagentActivationSpec & { readonly childId: SessionId }): startActivation(spec: SubagentActivationSpec): Promise /** - * Join progressing descendants without cancelling them. Idle descendants whose - * inboxes require a later wake stay resident and do not delay host completion. - * @param parent - the exact parent whose descendant work is observed. - * @returns whether work was joined; hosts recheck parent idle after true. + * Resolve the root shared by an Agent and its other agents in its subagent tree. + * @param caller - exact live Agent whose subagent ancestry selects the tree. + * @returns the live root; ordinary Session forks lead independent trees. + * @throws when the caller or its runtime owner is unavailable. */ -async waitForChildren(parent: Agent): Promise +rootOf(caller: Agent): Agent /** - * Steer one model-authored message to the sender's direct parent or direct - * continuable child. A running target admits it at the nearest step boundary; - * an idle target starts a turn, and an absent direct child cold-resumes from - * persistence. The service derives durable sender attribution from the exact - * live sender. Caller cancellation stops only pre-acceptance work. - * @param sender - exact live Agent authorizing and originating the message. - * @param targetId - durable direct-parent or direct-child session id. + * Join progressing subagent tree work for its root. Idle members with parked input + * stay resident without delaying host completion. Ordinary members return false. + * @param root - exact live subagent tree root whose member work is awaited. + * @returns whether work was joined; hosts recheck root idle after true. + */ +async waitForTreeWork(root: Agent): Promise + +/** + * Steer one model-authored message to another member of the same subagent tree. + * A running target receives it at its next step; an idle target starts a turn. + * A cold target resumes under its runtime subagent tree root without restoring + * its creation ancestors. Sender identity is rechecked before inbox acceptance; cancellation + * stops only pre-acceptance work and never retracts an accepted message. + * @param sender - exact live root or resident continuable Agent originating the message. + * @param targetId - another root or continuable member's durable Session id. * @param content - model-authored content to deliver. * @param options - caller cancellation before inbox acceptance. * @returns the accepted message's inbox id. - * @throws when continuation services are unavailable, adjacency is rejected, - * or the message was not admitted. + * @throws when the target is outside the tree, cannot resume, or rejects input. */ async sendMessage( sender: Agent, targetId: SessionId, content: ContentBlock[], options: SubagentSendMessageOptions, ): Promise /** - * Interrupt one live child's current execution under a human parent - * address or an exact live ancestor Agent. Fire-and-return: the cancel + * Interrupt one live child's current execution under a trusted user + * operation or an exact live ancestor Agent. Fire-and-return: the cancel * signal is issued before this returns, but the target may keep running * until it observes the signal. Unclaimed pending inbox work, the Activation, * and published descendants are preserved; claimed work is not requeued. @@ -473,47 +479,22 @@ async sendMessage( sender: Agent, targetId: SessionId, content: ContentBlock[], * is an accepted no-op, as is a manager-less composition, which cannot own a * live Activation. * @param targetSessionId - the durable child session id to interrupt. - * @param authority - the human parent address or exact live ancestor Agent. + * @param authority - a trusted user operation or exact live ancestor Agent. * @throws {SubagentError} `UNAUTHORIZED` when the authority does not own the * live target. */ interrupt(targetSessionId: SessionId, authority: SubagentInterruptAuthority): void /** - * Close subagent admission below exact live parent Agents, stop only their - * visible descendant Activations synchronously, then await admitted scoped - * materializations and release those forests child-first. The scoped cutoff - * lasts until each exact parent leaves the registry; unrelated parent trees - * remain live. - * @param parents - exact host-owned parent Agents entering teardown. - * @returns once every retained descendant activation released its execution handle. + * Close admission and stop all instances in the selected subagent trees, then await + * pending startup and cleanup. Ordinary members are ignored. The cutoff lasts + * until each exact root leaves the registry; unrelated subagent trees remain live. + * @param parents - exact live subagent tree roots entering teardown. + * @returns once all admitted startup and member cleanup have settled. * @throws an aggregate error after all branches settle when any failed. */ async drainDescendants(parents: readonly Agent[]): Promise -/** - * Release selected resident direct children of one exact live - * parent. Other children of the same parent remain admitted and resident. - * Absent targets and a manager-less composition are accepted no-ops. - * @param parent - exact live direct parent authorizing the selected release. - * @param childIds - durable direct-child ids to release when resident. - * @returns once every selected activation released its execution handle. - * @throws {SubagentError} `UNAUTHORIZED` when a resident target belongs to a - * different parent or the supplied parent identity is stale. - */ -async drainChildren(parent: Agent, childIds: readonly SessionId[]): Promise - -/** - * Read the parent's durable direct-child catalog without loading or resuming an Agent. - * The service owns and releases the live-preferred Session observation. - * @param parentSessionId - parent whose direct children are requested. - * @param signal - cancellation forwarded to the Session query. - * @returns catalog children in parent event order. - * @throws {@link SubagentError} when query or catalog projection is unavailable. - * @throws SessionQueryError when the parent cannot be read or the query is cancelled. - */ -listChildren(parentSessionId: SessionId, signal?: AbortSignal): Promise - /** * Recursively list reachable parent catalogs in stable pre-order, preserving * each catalog's event order. Each row carries its catalog parent and depth; @@ -533,8 +514,8 @@ listChildren(parentSessionId: SessionId, signal?: AbortSignal): Promise /** - * Deliver one browser-authored message to a continuable child through the - * exact live direct parent, retaining the caller-minted request identity and + * Deliver one browser-authored message to a continuable child through its + * live subagent tree root, retaining the caller-minted request identity and * validated browser zone on the accepted message. Success identifies the * message the child's inbox accepted; later execution is independent of this * call. Queue delivery targets a later turn; steer delivery targets the @@ -542,31 +523,27 @@ listDescendants(rootSessionId: SessionId, signal?: AbortSignal): Promise +@Remote({ name: 'prompt', resources: { session: request => request.childSessionId } }) async prompt(request: SubagentPromptRequest, signal: AbortSignal): Promise /** - * Remote face of {@link interrupt} under one durable parent address. No - * catalog, history, persistence, or parent Agent lookup runs: the core - * primitive alone authorizes the address against the live Activation, which - * is what keeps a live child interruptible while its parent Agent is offline. + * Remote face of {@link interrupt}, authorized on the target Session by the Gateway. + * No catalog, history, persistence, or parent Agent lookup runs. * Absent, idle, and already-completed targets are accepted no-ops there. * @param childSessionId - durable child session id to interrupt. - * @param parentSessionId - durable direct parent whose authority is claimed. - * @param mode - required continuable-address discriminator. * @returns acknowledgement that the cancel signal was admitted, not that the target is quiescent. * @throws {RemoteError} `gateway/bad-request` for an empty id, - * `subagent/unauthorized` when the address does not own the live target, * otherwise `gateway/internal`. */ -@Remote('interruptByParent') interruptByParent( childSessionId: SessionId, parentSessionId: SessionId, mode: 'continuable', ): SubagentInterruptReceipt +@Remote({ name: 'interruptByUser', resources: { session: childSessionId => childSessionId } }) interruptByUser(childSessionId: SessionId): SubagentInterruptReceipt /** * Register a provider under its name. Registration is effect-scoped and HMR @@ -584,12 +561,6 @@ registerProvider(provider: SubagentProvider): () => void * @returns the provider, or undefined when absent. */ getProvider(name: string): SubagentProvider | undefined - -/** - * List registered provider names in insertion order. - * @returns the registered names. - */ -list(): string[] ``` Types: [Agent](core.zh.md) · [ContentBlock](llm-streaming.zh.md) · [MessageId](llm-streaming.zh.md) · [SessionId](core.zh.md) @@ -604,14 +575,14 @@ Source: [`packages/subagent/subagent/src/index.ts`](../../packages/subagent/suba #### `subagent/end` — emit -A published child settled. Scope-filtered dispatch uses the same delegating parent carrier as `subagent/start`, so the lifecycle pair reaches the same scoped audience. +A published child settled. Scope-filtered dispatch uses the same subagent tree-root carrier as `subagent/start`, so the lifecycle pair reaches the same scoped audience. ```ts cordis-catalog /** - * A published child settled. Scope-filtered dispatch uses the same delegating - * parent carrier as `subagent/start`, so the lifecycle pair reaches the + * A published child settled. Scope-filtered dispatch uses the same subagent tree-root + * carrier as `subagent/start`, so the lifecycle pair reaches the * same scoped audience. - * @param info - the run identity and terminal outcome. + * @param info - run identity, durable direct creator `parentSessionId`, and terminal outcome. * @mode emit */ 'subagent/end'(this: Scoped, info: SubagentRunEndInfo): void @@ -659,16 +630,16 @@ Source: [`packages/subagent/subagent/src/index.ts`](../../packages/subagent/suba #### `subagent/start` — emit -A provider established a published child. For in-process providers, `ctx.agents.get(info.id)` resolves during this notification. Scope-filtered dispatch keys the carrier by the delegating parent, so a parent-scoped listener observes only its own delegations. Paired with `subagent/end`. +A provider established a published child. For in-process providers, `ctx.agents.get(info.id)` resolves during this notification. Scope-filtered dispatch keys the carrier by the subagent tree root, so a root-scoped listener observes all subagent tree activations. Paired with `subagent/end`. ```ts cordis-catalog /** * A provider established a published child. For in-process providers, * `ctx.agents.get(info.id)` resolves during this notification. - * Scope-filtered dispatch keys the carrier by the delegating parent, so a - * parent-scoped listener observes only its own delegations. Paired with + * Scope-filtered dispatch keys the carrier by the subagent tree root, so a + * root-scoped listener observes all subagent tree activations. Paired with * `subagent/end`. - * @param info - the provider and published child identity. + * @param info - provider, child identity, and durable direct creator `parentSessionId`. * @mode emit */ 'subagent/start'(this: Scoped, info: SubagentRunInfo): void diff --git a/docs/subsystems/user-questions.md b/docs/subsystems/user-questions.md index 2a2463b475..2c86b22e1c 100644 --- a/docs/subsystems/user-questions.md +++ b/docs/subsystems/user-questions.md @@ -278,17 +278,16 @@ async askTimed( request: AskUserQuestionRequest & { agent: Agent }, callId: Tool * Ask the scoped answerer waterfall and wait for the user's answer. * * When a caller supplies an agent, human interaction is valid only for the - * exact live runtime root. Runtime ownership, not durable session lineage, - * decides this boundary: an owned child has no human answerer and would - * block forever, while a lineage-bearing session resumed as a new runtime - * root may ask normally. + * exact live instance of an ordinary Session. Explicit subagent origin + * rejects delegated callers; historical ordinary forks may retain parent + * metadata and still ask normally. * * @param request Questions, owner agent, and abort signal. * @returns The answer chosen or typed by the human. * @throws {UserQuestionError} code `ASK_ABORTED` when the supplied signal * is already or becomes aborted, `CALLER_NOT_LIVE` when a supplied agent * is not the registry's exact live instance, or `DELEGATED_CALLER` when - * that live agent is owned by another agent. + * that live agent has subagent origin. */ async ask(request: AskUserQuestionRequest): Promise ``` diff --git a/docs/subsystems/user-questions.zh.md b/docs/subsystems/user-questions.zh.md index f433339045..ad39e1004f 100644 --- a/docs/subsystems/user-questions.zh.md +++ b/docs/subsystems/user-questions.zh.md @@ -278,17 +278,16 @@ async askTimed( request: AskUserQuestionRequest & { agent: Agent }, callId: Tool * Ask the scoped answerer waterfall and wait for the user's answer. * * When a caller supplies an agent, human interaction is valid only for the - * exact live runtime root. Runtime ownership, not durable session lineage, - * decides this boundary: an owned child has no human answerer and would - * block forever, while a lineage-bearing session resumed as a new runtime - * root may ask normally. + * exact live instance of an ordinary Session. Explicit subagent origin + * rejects delegated callers; historical ordinary forks may retain parent + * metadata and still ask normally. * * @param request Questions, owner agent, and abort signal. * @returns The answer chosen or typed by the human. * @throws {UserQuestionError} code `ASK_ABORTED` when the supplied signal * is already or becomes aborted, `CALLER_NOT_LIVE` when a supplied agent * is not the registry's exact live instance, or `DELEGATED_CALLER` when - * that live agent is owned by another agent. + * that live agent has subagent origin. */ async ask(request: AskUserQuestionRequest): Promise ``` diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml index 68f3651eaf..880989b55c 100644 --- a/docs/tool-catalog.i18n.yaml +++ b/docs/tool-catalog.i18n.yaml @@ -9,8 +9,8 @@ en: 6cc2cd9ced70bb09 zh: 1c0e6cb4ba2ed637 /tool-schema-catalog/tool-package-map: - en: 4aa572ac1710a97a - zh: e11431504a2a7398 + en: 6e8419f195bc96a1 + zh: 564c4f04420d329a /tool-schema-catalog/deepseek-ai-dsh-plugin-manager: en: e0143916bb634762 zh: e0143916bb634762 @@ -249,20 +249,23 @@ en: 207228068bc61885 zh: 36e9344ee1af8869 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent/subagent: - en: 140c965c2b04d02f - zh: 95495dad5c77046d + en: 430672da3c1a6ca6 + zh: bd3a9ab4089f9dbb /tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control: en: ab82e130c3413f04 zh: ab82e130c3413f04 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control/interrupt-agent: - en: 9190a32df5e10ddd + en: a622b15cc2598d51 zh: 48e1b05956d63e26 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control/list-agents: - en: ae22e8543d123c39 - zh: 0991a73ef49bcdea + en: a56168133c21c6a0 + zh: f9df1c0fe49fa225 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control/send-message: - en: 9d180d8811903366 - zh: d63f94cffcb952d1 + en: b53326fedeab850d + zh: ecb75b6ce31da009 +/tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control/wait-agent: + en: f3cf5a0c56ddc07a + zh: 7f6754d71e1b4ae4 /tool-schema-catalog/deepseek-ai-dsh-tool-jobs: en: 9be673b1604d0114 zh: 9be673b1604d0114 @@ -273,38 +276,23 @@ en: 0c37fb1fed979d72 zh: 5ea9d07f5a4a8a6c /tool-schema-catalog/deepseek-ai-dsh-tool-jobs/job-output: - en: 1e084359a8cd42b3 - zh: 3f6c245dbbb2b920 -/tool-schema-catalog/deepseek-ai-dsh-experimental-tool-agent-team: - en: 6b1106f417b50db6 - zh: 6b1106f417b50db6 -/tool-schema-catalog/deepseek-ai-dsh-experimental-tool-agent-team/interrupt-agent: - en: c3c7af05ceb9f8c9 - zh: ec379e0b42110c6a -/tool-schema-catalog/deepseek-ai-dsh-experimental-tool-agent-team/list-agents: - en: f9457f033f75f00a - zh: a2d60ec44118c34c -/tool-schema-catalog/deepseek-ai-dsh-experimental-tool-agent-team/send-message: - en: ec50e6a0b86aecc6 - zh: 29c8728536a5adf1 -/tool-schema-catalog/deepseek-ai-dsh-experimental-tool-agent-team/spawn-teammate: - en: 02d6cc746e234b99 - zh: 49bc60ac098bca48 -/tool-schema-catalog/deepseek-ai-dsh-experimental-tool-agent-team/team-task-create: - en: 91e84febe063c3bd - zh: 550555d0f44f603b -/tool-schema-catalog/deepseek-ai-dsh-experimental-tool-agent-team/team-task-get: - en: 1c6ae0aba0b8041d - zh: f8d634387b74121d -/tool-schema-catalog/deepseek-ai-dsh-experimental-tool-agent-team/team-task-list: - en: 2084d494aae8798a - zh: e4c136c91028a7b1 -/tool-schema-catalog/deepseek-ai-dsh-experimental-tool-agent-team/team-task-update: - en: 2eac7360ba68b1bd - zh: b5d676bf288c245e -/tool-schema-catalog/deepseek-ai-dsh-experimental-tool-agent-team/wait-agent: - en: e70c77a45832bb28 - zh: 7d03bec294b65452 + en: 2977f731fb90d147 + zh: 4f422613ff720055 +/tool-schema-catalog/deepseek-ai-dsh-tool-agent-team: + en: 3f1a9c3509853972 + zh: 3f1a9c3509853972 +/tool-schema-catalog/deepseek-ai-dsh-tool-agent-team/team-task-create: + en: ac9ed819b705b4b8 + zh: 306477d2d457369a +/tool-schema-catalog/deepseek-ai-dsh-tool-agent-team/team-task-get: + en: bca1abf330638c19 + zh: 903bfa977f75c046 +/tool-schema-catalog/deepseek-ai-dsh-tool-agent-team/team-task-list: + en: 8001eda7e9ff6a25 + zh: 2414761029f21404 +/tool-schema-catalog/deepseek-ai-dsh-tool-agent-team/team-task-update: + en: 9acd1c5b2fac9853 + zh: 3bd81e9cd1f1c28d /tool-schema-catalog/deepseek-ai-dsh-tool-todo: en: dea62d26fb18ff68 zh: dea62d26fb18ff68 diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index 1d7900cf6b..1dea12b08f 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -41,9 +41,9 @@ This table connects model-visible tool names to the plugin package and service s | `@deepseek-ai/dsh-tool-session-query` | `session_event_read`, `session_event_search`, `session_event_trace`, `session_search`, `session_trace` | `ctx.tools`, `ctx.systemPrompt`, `ctx.sessionQuery`, `ctx.agents`, `a calling Agent for workspace authority` | `tool/call`, `tool/result` | - | The five read-only tools hide provider cursors and authorize every result from the immutable calling agent session. The package is opt-in; compositions that need enforced deadlines or bounded inline output also mount the generic timeout or spill policies. | | `@deepseek-ai/dsh-experimental-tool-subagent-oar` | `subagent_oar` | `ctx.tools`, `ctx.subagents` | `tool/call`, `tool/result`, `parent subagent catalog and completion notices` | - | The subagent_oar tool selects among configured aliases for external providers. This catalog shows Codex and Claude Code aliases; each deployment chooses its providers. Native model and permission settings belong to the selected provider configuration. | | `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`, `subagent` | `ctx.tools`, `ctx.subagents`, `ctx.systemPrompt`, `ctx.sessionProjections`, `ctx.llm for model discovery and selected-route validation` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The load-time `toolName` selects the delegation name (default `subagent`); `modelSelection` adds optional route fields. `modelDiscovery` defaults off and registers shared discovery on one instance per scope. Web and Desktop presets enable spawn discovery and route fields and leave `subagent_fork` route fields disabled. The Host settings service records authorization for each new top-level Session; child Sessions inherit it. Without a recorded policy, discovery and explicit selection permit only the current parent route. Every delegation returns a child id promptly; local children accept follow-ups, while external backends report their single execution automatically. | -| `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`, `list_agents`, `send_message` | `ctx.tools`, `ctx.subagents`, `ctx.agents` | `tool/call`, `tool/result`, `child session events through ctx.subagents` | - | The globally named discovery, interruption, and local-message tools over managed subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while one instance of this package registers `send_message`, `interrupt_agent`, and `list_agents`. | +| `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`, `list_agents`, `send_message`, `wait_agent` | `ctx.tools`, `ctx.subagents`, `ctx.agents` | `tool/call`, `tool/result`, `child session events through ctx.subagents` | - | The shared control tools discover and contact agents in the subagent tree, interrupt owned descendants, and wait for input in the caller’s inbox. Provider-bound `tool-subagent` instances supply delegation tools. | | `@deepseek-ai/dsh-tool-jobs` | `job_kill`, `job_list`, `job_output` | `ctx.tools`, `ctx.jobs`, `ctx.systemPrompt` | `tool/call`, `tool/result`, `user/message via agent.inject() for background completion notices` | - | The kind-agnostic background-job controller: background bash commands and PTY sends are read, listed, and killed through the same three tools. Loading the plugin attaches the controller that arms producers' `ctx.jobs.start()`. | -| `@deepseek-ai/dsh-experimental-tool-agent-team` | `interrupt_agent`, `list_agents`, `send_message`, `spawn_teammate`, `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update`, `wait_agent` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `an exact live Team member Agent` | `tool/call`, `team/member`, `team/task`, `agent/inbox/spliced`, `user/message on inbox claim`, `tool/result` | - | All nine tools are scoped to implicit Team Leads and durable teammates. The shipped dsh-base bundle keeps the package disabled; the documented Agent Teams profile patch enables it while disabling the legacy continuable-child control names. | +| `@deepseek-ai/dsh-tool-agent-team` | `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `ctx.subagents`, `an exact live Team member Agent` | `tool/call`, `team/task`, `tool/result` | - | Four task-board tools share the subagent tree’s root Session. Enabling Team mode on the root Session adds task-board guidance alongside standard subagent delegation and control tools. Standard subagent controls provide required identity independently of Team mode. | | `@deepseek-ai/dsh-tool-todo` | `todo_write` | `ctx.tools`, `owning Agent session` | `tool/call`, `todo/write`, `tool/result` | - | todo_write is session-owned state; UIs render the latest todo/write event as a checklist. `allowParallelInProgress` is required with no default, so the catalog states its choice: `true`, whose description invites several `in_progress` items. A deployment choosing `false` receives the same tool with a description asking for exactly one active task. | | `@deepseek-ai/dsh-tool-working-directory` | `working_directory` | `ctx.tools`, `ctx.workingDirectory` | `tool/call`, `working-directory/change`, `user context for directory changes`, `tool/result` | - | Reads or changes the calling Session directory. Existing shells and processes retain their own directories; origin metadata and permission roots stay fixed. | | `@deepseek-ai/dsh-experimental-tool-worktree` | `create_worktree` | `ctx.tools`, `ctx.worktrees`, `a calling Agent` | `tool/call`, `Git branch and checkout`, `working-directory/change`, `tool/result` | - | Explicit experimental composition only. Creates a new branch and checkout from a pinned local commit under existing write permissions, then changes the Session directory. Leaving retains the branch and checkout. | @@ -2061,7 +2061,7 @@ Source: [`packages/subagent/tool-subagent/src/list-models.ts`](../packages/subag ### `subagent` -Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. +Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes. ```json { @@ -2073,7 +2073,7 @@ Delegate a self-contained task to a subagent (a separate agent that works in its }, "description": { "type": "string", - "description": "A short (3-5 word) description of the delegated task, for display." + "description": "A short (3-5 word) description of the delegated task, for display. Use the language of the user's current request." }, "prompt": { "type": "string", @@ -2097,7 +2097,7 @@ The load-time `toolName` selects the delegation name (default `subagent`); `mode ### `interrupt_agent` -Ask a subagent to stop its current work. This call returns without waiting for it to stop. You can continue a local direct child's conversation later with send_message. External executions stop permanently and cannot receive follow-ups. Subagents it started will keep running. +Ask a subagent to stop its current work. This call returns without waiting for it to stop. You can continue a local subagent's conversation later with send_message. External executions stop permanently and cannot receive follow-ups. Subagents it started will keep running. ```json { @@ -2118,21 +2118,12 @@ Source: [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/sub ### `list_agents` -List subagents you started, with their ids, labels, and status. running means it is working; inactive means it is not currently working. You will be notified when a subagent finishes; there is no need to keep checking its status. Use send_message to continue the conversation. +List your entire subagent tree: the root agent and all reachable continuable descendants, including yourself. Rows carry ids, descriptions, and status; descendant rows carry parent ids and depth from the root. You can message any other agent in the subagent tree listed as running or inactive. running means it is working; inactive means no turn is executing; it does not indicate task completion, success, or failure. While you remain active, you will be notified when a direct child finishes; there is no need to keep checking its status. Use send_message to continue the conversation. ```json { "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent.", - "enum": [ - "children", - "descendants" - ] - } - } + "properties": {} } ``` @@ -2140,7 +2131,7 @@ Source: [`packages/subagent/tool-subagent-control/src/list-agents.ts`](../packag ### `send_message` -Send a message to an agent. A working agent receives it at its next step; an idle agent starts a new turn with it. Returns delivery confirmation, not the agent's answer. +Send a message to an agent. A working agent receives it at its next step; an idle agent starts a new turn with it. Returns delivery confirmation. ```json { @@ -2148,7 +2139,7 @@ Send a message to an agent. A working agent receives it at its next step; an idl "properties": { "agent_id": { "type": "string", - "description": "The agent id of your direct continuable child, or your direct parent when you are a resident continuable child." + "description": "The agent id of another member of your subagent tree, as shown by list_agents." }, "message": { "type": "string", @@ -2164,7 +2155,25 @@ Send a message to an agent. A working agent receives it at its next step; an idl Source: [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/subagent/tool-subagent-control/src/index.ts) -The globally named discovery, interruption, and local-message tools over managed subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while one instance of this package registers `send_message`, `interrupt_agent`, and `list_agents`. +### `wait_agent` + +Wait for input at your next step, including agent messages, completion notifications, and user steering. Returns immediately when that input is already pending. + +```json +{ + "type": "object", + "properties": { + "timeout_ms": { + "type": "integer", + "description": "Wait duration in milliseconds, from 10000 through 3600000. Defaults to 30000." + } + } +} +``` + +Source: [`packages/subagent/tool-subagent-control/src/wait-agent.ts`](../packages/subagent/tool-subagent-control/src/wait-agent.ts) + +The shared control tools discover and contact agents in the subagent tree, interrupt owned descendants, and wait for input in the caller’s inbox. Provider-bound `tool-subagent` instances supply delegation tools. @@ -2239,108 +2248,9 @@ Source: [`packages/jobs/tool-jobs/src/index.ts`](../packages/jobs/tool-jobs/src/ The kind-agnostic background-job controller: background bash commands and PTY sends are read, listed, and killed through the same three tools. Loading the plugin attaches the controller that arms producers' `ctx.jobs.start()`. - + -## `@deepseek-ai/dsh-experimental-tool-agent-team` - -### `interrupt_agent` - -Interrupt one teammate's current turn while preserving its pending inbox. Team Lead only. - -```json -{ - "type": "object", - "properties": { - "target": { - "type": "string", - "description": "Teammate target returned by spawn_teammate or list_agents." - } - }, - "required": [ - "target" - ] -} -``` - -Source: [`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) - -### `list_agents` - -List the Lead and every durable teammate with an addressable target and current availability. inactive means no turn is executing, not a task result. provisioning and failed describe member creation. - -```json -{ - "type": "object", - "properties": {} -} -``` - -Source: [`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) - -### `send_message` - -Send one message to another Team member. A running target receives it at the nearest step boundary; an inactive target starts or resumes a turn. - -```json -{ - "type": "object", - "properties": { - "target": { - "type": "string", - "description": "Member target returned by spawn_teammate or list_agents, including lead." - }, - "message": { - "type": "string", - "description": "Self-contained message for the target." - } - }, - "required": [ - "target", - "message" - ] -} -``` - -Source: [`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) - -### `spawn_teammate` - -Create one named, durable teammate. Only the Team Lead may call this tool. - -```json -{ - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Unique lower-kebab-case teammate name." - }, - "description": { - "type": "string", - "description": "Short description of the delegated responsibility." - }, - "prompt": { - "type": "string", - "description": "Complete initial task for the teammate." - }, - "context": { - "type": "string", - "description": "fresh starts without Lead history; fork inherits completed Lead turns. Defaults to fresh.", - "enum": [ - "fresh", - "fork" - ] - } - }, - "required": [ - "name", - "description", - "prompt" - ] -} -``` - -Source: [`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) +## `@deepseek-ai/dsh-tool-agent-team` ### `team_task_create` @@ -2380,7 +2290,7 @@ Create one unowned pending task on the shared Team task board. } ``` -Source: [`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) +Source: [`packages/team/tool-agent-team/src/index.ts`](../packages/team/tool-agent-team/src/index.ts) ### `team_task_get` @@ -2401,7 +2311,7 @@ Read the complete latest value of one shared task before changing or executing i } ``` -Source: [`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) +Source: [`packages/team/tool-agent-team/src/index.ts`](../packages/team/tool-agent-team/src/index.ts) ### `team_task_list` @@ -2422,7 +2332,7 @@ List shared tasks, including readiness, owner, revision, blockers, and write-sco }, "owner": { "type": "string", - "description": "Optional member target from spawn_teammate or list_agents, matching ownerName; use unowned for tasks without an owner." + "description": "Optional exact ownerId filter; use unowned for tasks without an owner." }, "ready": { "type": "boolean", @@ -2440,7 +2350,7 @@ List shared tasks, including readiness, owner, revision, blockers, and write-sco } ``` -Source: [`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) +Source: [`packages/team/tool-agent-team/src/index.ts`](../packages/team/tool-agent-team/src/index.ts) ### `team_task_update` @@ -2496,7 +2406,7 @@ Compare-and-set a shared task action using the latest revision from team_task_ge }, "owner": { "type": "string", - "description": "Member target from spawn_teammate or list_agents for Lead-only reassign; omit to unassign." + "description": "Agent id of the root or any continuable descendant for root-only reassign; omit to unassign." } }, "required": [ @@ -2507,27 +2417,9 @@ Compare-and-set a shared task action using the latest revision from team_task_ge } ``` -Source: [`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) +Source: [`packages/team/tool-agent-team/src/index.ts`](../packages/team/tool-agent-team/src/index.ts) -### `wait_agent` - -Wait for the next teammate status, message, or shared-task change after this call starts. This never wakes inactive members and returns noProgress immediately when no other member is running or provisioning. Re-list after wakeup or timeout instead of polling. - -```json -{ - "type": "object", - "properties": { - "timeout_ms": { - "type": "integer", - "description": "Wait duration in milliseconds, from 10000 through 3600000. Defaults to 30000." - } - } -} -``` - -Source: [`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) - -All nine tools are scoped to implicit Team Leads and durable teammates. The shipped dsh-base bundle keeps the package disabled; the documented Agent Teams profile patch enables it while disabling the legacy continuable-child control names. +Four task-board tools share the subagent tree’s root Session. Enabling Team mode on the root Session adds task-board guidance alongside standard subagent delegation and control tools. Standard subagent controls provide required identity independently of Team mode. diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md index cc5c16d779..b9c2e6d41e 100644 --- a/docs/tool-catalog.zh.md +++ b/docs/tool-catalog.zh.md @@ -45,9 +45,9 @@ | `@deepseek-ai/dsh-tool-session-query` | `session_event_read`、`session_event_search`、`session_event_trace`、`session_search`、`session_trace` | `ctx.tools`、`ctx.systemPrompt`、`ctx.sessionQuery`、`a calling Agent for workspace authority` | `tool/call`、`tool/result` | - | 这 5 个只读工具会隐藏提供方游标,并根据不可变的调用 agent 会话为每个结果授权。该包需要选择启用;需要强制截止时间或限制行内输出的组合还会挂载通用超时或 spill 策略。 | | `@deepseek-ai/dsh-experimental-tool-subagent-oar` | `subagent_oar` | `ctx.tools`, `ctx.subagents` | `tool/call`, `tool/result`, `parent subagent catalog and completion notices` | - | subagent_oar 工具在为外部提供方配置的别名之间选择。本目录展示 Codex 和 Claude Code 别名;每个部署自行选择提供方。原生模型和权限设置由所选提供方的配置决定。 | | `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`ctx.sessionProjections`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 加载时的 `toolName` 选择委派名称(默认为 `subagent`);`modelSelection` 增加可选路由字段。`modelDiscovery` 默认关闭,每个作用域由一个实例注册共享发现工具。Web 与 Desktop preset 启用 spawn 发现和路由字段,并为 `subagent_fork` 保持路由字段关闭。Host 设置服务为每个新顶层 Session 记录授权;子 Session 继承该授权。没有已记录策略时,发现和显式选择只允许当前父级路由。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 | -| `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`、`list_agents`、`send_message` | `ctx.tools`、`ctx.subagents`、`ctx.agents` | `tool/call`、`tool/result`、`child session events through ctx.subagents` | - | 这些是用于发现、中断托管 subagent 和向本地子代理发送消息的全局命名工具:绑定提供方的 `tool-subagent` 实例注册不同的委派工具;本包的一个实例注册 `send_message`、`interrupt_agent` 和 `list_agents`。 | +| `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`、`list_agents`、`send_message`、`wait_agent` | `ctx.tools`、`ctx.subagents`、`ctx.agents` | `tool/call`、`tool/result`、`child session events through ctx.subagents` | - | 共享控制工具发现并联系 subagent 树中的 Agent,中断有权控制的后代,并等待调用者收件箱中的输入。绑定提供方的 `tool-subagent` 实例提供委派工具。 | | `@deepseek-ai/dsh-tool-jobs` | `job_kill`、`job_list`、`job_output` | `ctx.tools`、`ctx.jobs`、`ctx.systemPrompt` | `tool/call`、`tool/result`、`user/message via agent.inject() for background completion notices` | - | 与任务种类无关的后台任务控制器:后台 bash 命令与 PTY 发送 都通过相同的 3 个工具读取、列出和终止。加载该插件会挂接控制器,从而启用生产方的 `ctx.jobs.start()`。 | -| `@deepseek-ai/dsh-experimental-tool-agent-team` | `interrupt_agent`、`list_agents`、`send_message`、`spawn_teammate`、`team_task_create`、`team_task_get`、`team_task_list`、`team_task_update`、`wait_agent` | `ctx.tools`、`ctx.systemPrompt`、`ctx.agentTeams`、`an exact live Team member Agent` | `tool/call`、`team/member`、`team/task`、`agent/inbox/spliced`、`user/message on inbox claim`、`tool/result` | - | 这 9 个工具限定于隐式 Team Lead 与持久 teammate 作用域。随产品发布的 dsh-base bundle 默认禁用该包;文档中的 Agent Teams profile patch 会启用它,并禁用旧 continuable child 的同名控制工具。 | +| `@deepseek-ai/dsh-tool-agent-team` | `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `ctx.subagents`, `an exact live Team member Agent` | `tool/call`, `team/task`, `tool/result` | - | 四个任务板工具共享 subagent 树的根 Session。在根 Session 开启 Team 模式,会在标准 subagent 委派和控制工具之外增加任务板指导。标准 subagent 控制工具提供必需身份,不受 Team 模式影响。 | | `@deepseek-ai/dsh-tool-todo` | `todo_write` | `ctx.tools`、`owning Agent session` | `tool/call`、`todo/write`、`tool/result` | - | todo_write 是会话所有的状态;UI 将最新的 todo/write 事件渲染为检查清单。`allowParallelInProgress` 是没有默认值的必填项,因此本目录明确选择 `true`,对应描述允许同时存在多个 `in_progress` 项。选择 `false` 的部署会获得同一工具,但描述会要求只能有 1 个活动任务。 | | `@deepseek-ai/dsh-tool-working-directory` | `working_directory` | `ctx.tools`, `ctx.workingDirectory` | `tool/call`, `working-directory/change`, `user context for directory changes`, `tool/result` | - | 读取或改变调用方 Session 的目录。已有 shell 和进程保留自己的目录;原始元数据和权限根目录保持不变。 | | `@deepseek-ai/dsh-experimental-tool-worktree` | `create_worktree` | `ctx.tools`, `ctx.worktrees`, `a calling Agent` | `tool/call`, `Git branch and checkout`, `working-directory/change`, `tool/result` | - | 仅通过显式实验组合加载。在现有写权限下从固定的本地提交创建新分支与检出目录,再改变 Session 工作目录。离开时保留分支与检出目录。 | @@ -2067,7 +2067,7 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 ### `subagent` -将一项自包含任务委派给 subagent(在自身上下文中工作的独立 agent),用它卸载聚焦且独立的工作,例如研究、限定范围的实现或分析,以免消耗当前对话的上下文。subagent 会返回结果,但不会返回中间步骤。此工具启动独立管理的 subagent 并立即返回其 id。运行时会在任务结束时通知你。子代理通过 `send_message` 汇报结果;你可以使用 `send_message` 在运行时引导它,或在结束后继续对话。 +将一项自包含任务委派给 subagent(在自身上下文中工作的独立 agent),用它卸载聚焦且独立的工作,例如研究、限定范围的实现或分析,以免消耗当前对话的上下文。subagent 会返回结果,但不会返回中间步骤。此工具启动独立管理的 subagent 并立即返回其 id。如果它结束时你仍在线,运行时会自动递送其完成通知。通知包含最终回答,除非子任务在结束前刚给你发过消息。你可以使用 `send_message` 在运行时引导它,或在结束后继续对话。 ```json { @@ -2079,7 +2079,7 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 }, "description": { "type": "string", - "description": "A short (3-5 word) description of the delegated task, for display." + "description": "A short (3-5 word) description of the delegated task, for display. Use the language of the user's current request." }, "prompt": { "type": "string", @@ -2124,21 +2124,12 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 ### `list_agents` -按持久 ID 和标签列出调用方目录中可继续的直接子代理。使用此工具回忆已启动的子代理,不要轮询完成状态;完成时会收到通知。状态来自在线注册表:running 表示当前正在工作;inactive 表示当前没有执行轮次,无论子代理已加载还是需要恢复,并不表示任务完成、成功、失败或正在等待其他代理。`send_message` 可在最近的步骤边界引导正在运行的子代理,或启动、恢复非活跃子代理的轮次;任何状态下的直接子级都可作为 `send_message` 的候选目标。列表快照不保证投递成功,`send_message` 仍进行权威检查,且可能失败。 +列出整棵 subagent 树:根代理和所有可达的可继续后代,包括调用方自身。行包含 id、描述和状态;后代行包含父级 id 和相对根的深度。可以向树中列为 running 或 inactive 的任意其他代理发送消息。running 表示正在工作;inactive 表示当前没有轮次在执行,不表示任务完成、成功或失败。自身仍在线时,直属子代理结束会收到通知,无需反复查询其状态。使用 send_message 继续对话。 ```json { "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent.", - "enum": [ - "children", - "descendants" - ] - } - } + "properties": {} } ``` @@ -2146,7 +2137,7 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 ### `send_message` -根据 agent id 向直接可继续 child 发送消息。如果你是驻留的可继续 child,也可以把自己的直接 parent 作为目标。如果目标仍在工作,消息会 steer 其最近的 step;如果目标处于 idle,消息会启动一个轮次。此调用不会返回该 agent 的答案,只会确认消息已投递。调用失败表示消息**未**投递。 +向代理发消息。工作中的代理在下一步骤接收消息,空闲代理用消息启动新轮次。返回投递确认。 ```json { @@ -2154,7 +2145,7 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 "properties": { "agent_id": { "type": "string", - "description": "The agent id of your direct continuable child, or your direct parent when you are a resident continuable child." + "description": "The agent id of another member of your subagent tree, as shown by list_agents." }, "message": { "type": "string", @@ -2170,7 +2161,25 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 来源:[`packages/subagent/tool-subagent-control/src/index.ts`](../packages/subagent/tool-subagent-control/src/index.ts) -这些是用于发现、中断托管 subagent 和向本地子代理发送消息的全局命名工具:绑定提供方的 `tool-subagent` 实例注册不同的委派工具;本包的一个实例注册 `send_message`、`interrupt_agent` 和 `list_agents`。 +### `wait_agent` + +等待下一步可处理的输入,包括 Agent 消息、完成通知和用户引导。已有此类待处理输入时立即返回。 + +```json +{ + "type": "object", + "properties": { + "timeout_ms": { + "type": "integer", + "description": "Wait duration in milliseconds, from 10000 through 3600000. Defaults to 30000." + } + } +} +``` + +来源:[`packages/subagent/tool-subagent-control/src/wait-agent.ts`](../packages/subagent/tool-subagent-control/src/wait-agent.ts) + +共享控制工具发现并联系 subagent 树中的 Agent,中断有权控制的后代,并等待调用者收件箱中的输入。绑定提供方的 `tool-subagent` 实例提供委派工具。 @@ -2245,108 +2254,9 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 与任务种类无关的后台任务控制器:后台 bash 命令与 PTY 发送 都通过相同的 3 个工具读取、列出和终止。加载该插件会挂接控制器,从而启用生产方的 `ctx.jobs.start()`。 - + -## `@deepseek-ai/dsh-experimental-tool-agent-team` - -### `interrupt_agent` - -中断一名 teammate 的当前 turn,同时保留其待处理 inbox。仅 Team Lead 可用。 - -```json -{ - "type": "object", - "properties": { - "target": { - "type": "string", - "description": "Teammate target returned by spawn_teammate or list_agents." - } - }, - "required": [ - "target" - ] -} -``` - -来源:[`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) - -### `list_agents` - -列出 Lead 与所有持久 teammate,以及可用于寻址的 target 和当前可用状态。inactive 表示没有轮次在执行,不表示任务结果。provisioning 与 failed 描述成员创建状态。 - -```json -{ - "type": "object", - "properties": {} -} -``` - -来源:[`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) - -### `send_message` - -向另一名 Team member 发送一条消息。running target 会在最近的步骤边界收到消息;inactive target 会启动或恢复一个 turn。 - -```json -{ - "type": "object", - "properties": { - "target": { - "type": "string", - "description": "Member target returned by spawn_teammate or list_agents, including lead." - }, - "message": { - "type": "string", - "description": "Self-contained message for the target." - } - }, - "required": [ - "target", - "message" - ] -} -``` - -来源:[`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) - -### `spawn_teammate` - -创建一名具名、持久的 teammate。只有 Team Lead 可以调用此工具。 - -```json -{ - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Unique lower-kebab-case teammate name." - }, - "description": { - "type": "string", - "description": "Short description of the delegated responsibility." - }, - "prompt": { - "type": "string", - "description": "Complete initial task for the teammate." - }, - "context": { - "type": "string", - "description": "fresh starts without Lead history; fork inherits completed Lead turns. Defaults to fresh.", - "enum": [ - "fresh", - "fork" - ] - } - }, - "required": [ - "name", - "description", - "prompt" - ] -} -``` - -来源:[`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) +## `@deepseek-ai/dsh-tool-agent-team` ### `team_task_create` @@ -2386,7 +2296,7 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 } ``` -来源:[`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) +来源:[`packages/team/tool-agent-team/src/index.ts`](../packages/team/tool-agent-team/src/index.ts) ### `team_task_get` @@ -2407,7 +2317,7 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 } ``` -来源:[`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) +来源:[`packages/team/tool-agent-team/src/index.ts`](../packages/team/tool-agent-team/src/index.ts) ### `team_task_list` @@ -2428,7 +2338,7 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 }, "owner": { "type": "string", - "description": "Optional member target from spawn_teammate or list_agents, matching ownerName; use unowned for tasks without an owner." + "description": "Optional exact ownerId filter; use unowned for tasks without an owner." }, "ready": { "type": "boolean", @@ -2446,7 +2356,7 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 } ``` -来源:[`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) +来源:[`packages/team/tool-agent-team/src/index.ts`](../packages/team/tool-agent-team/src/index.ts) ### `team_task_update` @@ -2502,7 +2412,7 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 }, "owner": { "type": "string", - "description": "Member target from spawn_teammate or list_agents for Lead-only reassign; omit to unassign." + "description": "Agent id of the root or any continuable descendant for root-only reassign; omit to unassign." } }, "required": [ @@ -2513,28 +2423,9 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 } ``` -来源:[`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) - -### `wait_agent` - -等待本次调用开始后下一次 teammate 状态、消息或共享任务变更。它绝不会唤醒 inactive member;若没有其他 member 正在 running 或 provisioning,则立即返回 noProgress。唤醒或超时后应重新列出状态,而不是轮询。 - -```json -{ - "type": "object", - "properties": { - "timeout_ms": { - "type": "integer", - "description": "Wait duration in milliseconds, from 10000 through 3600000. Defaults to 30000." - } - } -} -``` - -来源:[`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) - -这 10 个工具限定于隐式 Team Lead 与持久 teammate 作用域。随产品发布的 dsh-base bundle 默认禁用该包;文档中的 Agent Teams profile patch 会启用它,并禁用旧 continuable child 的同名控制工具。 +来源:[`packages/team/tool-agent-team/src/index.ts`](../packages/team/tool-agent-team/src/index.ts) +四个任务板工具共享 subagent 树的根 Session。在根 Session 开启 Team 模式,会在标准子代理委派和控制工具之外增加任务板指导。标准子代理控制工具提供必需身份,不受 Team 模式影响。 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/team-direct-inbox/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.1/team-direct-inbox/guide.i18n.yaml index 0f10d46677..fee15bfbc4 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.1/team-direct-inbox/guide.i18n.yaml +++ b/docs/upgrade-guide/v0.2.1-alpha.1/team-direct-inbox/guide.i18n.yaml @@ -12,5 +12,5 @@ en: 9968decf3dd340af zh: 736a32caac4b5103 /agent-teams-messages-use-target-inboxes/migration: - en: 7ffd442bfd55769e - zh: ffb11a143f11ce7f + en: a4b8999943c76cc4 + zh: 042927b5c857c8d5 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/team-direct-inbox/guide.md b/docs/upgrade-guide/v0.2.1-alpha.1/team-direct-inbox/guide.md index 974c1aa13c..d2e03eacd5 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.1/team-direct-inbox/guide.md +++ b/docs/upgrade-guide/v0.2.1-alpha.1/team-direct-inbox/guide.md @@ -18,4 +18,4 @@ The `maxPendingMessagesPerMember` configuration key is removed. Historical `team 1. Before upgrading, let pending Team messages finish and confirm that their targets received them. After upgrading, explicitly resend any required undelivered message; check the target's history first to avoid duplicates. Do not edit historical Session records. 2. Remove `maxPendingMessagesPerMember` from the `@deepseek-ai/dsh-experimental-agent-team` plugin configuration in your `cordis.yml` or overlays. There is no replacement Team queue limit; `maxMessageBytes` still bounds each complete sender-framed message. 3. Update any consumer of `send_message` results to read `sent` instead of message IDs or delivery statuses, and handle send errors explicitly. Do not treat success as completed target work. The `ctx.agentTeams.sendMessage()` service still returns `messageId`. -4. Start your Team-enabled profile, send a message to another member, and confirm that the tool returns `{ "sent": true }` and that the target receives it. Consult the [package reference](../../../../packages/experimental/agent-team/README.md#use-this-package) for delivery semantics. +4. Start your Team-enabled profile, send a message to another member, and confirm that the tool returns `{ "sent": true }` and that the target receives it. Consult the [package reference](../../../../packages/team/agent-team/README.md#use-this-package) for delivery semantics. diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/team-direct-inbox/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.1/team-direct-inbox/guide.zh.md index c10791fbf7..1f5014e0c7 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.1/team-direct-inbox/guide.zh.md +++ b/docs/upgrade-guide/v0.2.1-alpha.1/team-direct-inbox/guide.zh.md @@ -18,4 +18,4 @@ description: "Agent Teams 移除待投递消息数量限制,并停止投递历 1. 升级前,让待投递的 Team 消息完成投递,并确认目标已收到。升级后,显式重发仍需处理的未投递消息;先检查目标历史,避免重复。不要编辑历史 Session 记录。 2. 从 `cordis.yml` 或覆盖配置中 `@deepseek-ai/dsh-experimental-agent-team` 插件的配置移除 `maxPendingMessagesPerMember`。没有替代的 Team 队列限制;`maxMessageBytes` 仍限制包含发送者标识的完整消息大小。 3. 更新所有读取 `send_message` 结果的调用方,改为读取 `sent` 而非消息 ID 或投递状态,并显式处理发送错误。不要把发送成功视为目标工作已完成。`ctx.agentTeams.sendMessage()` 服务仍返回 `messageId`。 -4. 启动启用 Team 的 profile,向其他成员发送消息,确认工具返回 `{ "sent": true }` 且目标收到消息。投递语义参见[包参考](../../../../packages/experimental/agent-team/README.zh.md#use-this-package)。 +4. 启动启用 Team 的 profile,向其他成员发送消息,确认工具返回 `{ "sent": true }` 且目标收到消息。投递语义参见[包参考](../../../../packages/team/agent-team/README.zh.md#use-this-package)。 diff --git a/docs/upgrade-guide/v0.2.1-rc.2/agent-runtime-owner/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.2/agent-runtime-owner/guide.i18n.yaml new file mode 100644 index 0000000000..2374c0b447 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.2/agent-runtime-owner/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.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 docs/upgrade-guide/v0.2.1-rc.2/agent-runtime-owner/guide.md +/: + en: ced4d23952f772d5 + zh: f8e7e8c069266fa1 +/agent-runtime-ownership-uses-owneragent: + en: 8a1164f53b3627da + zh: 19fae50e43c4dca4 +/agent-runtime-ownership-uses-owneragent/change: + en: 88457ac32ffc2c7d + zh: 05ea7db957db119b +/agent-runtime-ownership-uses-owneragent/migration: + en: d25ad901f4601369 + zh: 96b556991249635b diff --git a/docs/upgrade-guide/v0.2.1-rc.2/agent-runtime-owner/guide.md b/docs/upgrade-guide/v0.2.1-rc.2/agent-runtime-owner/guide.md new file mode 100644 index 0000000000..4648667d9e --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.2/agent-runtime-owner/guide.md @@ -0,0 +1,20 @@ +--- +kind: upgrade-guide +description: "Agent creation and resume options rename parentAgent to ownerAgent for runtime ownership." +--- + +# Agent runtime ownership uses `ownerAgent` + +English | [中文](guide.zh.md) + +## Change + +`CreateAgentOptions` and `ResumeAgentOptions` in `@deepseek-ai/dsh-agent` replace `parentAgent` with `ownerAgent`. This affects plugins and custom factories calling `ctx.agents.create()` or `ctx.agents.resume()`. The field selects the live Agent that owns the runtime instance; omitting it creates a runtime root. + +Session creation lineage remains in `meta.parentSession`. For a nested subagent, the runtime owner can be the tree root while the direct creator is another descendant. Persisted Session fields and formats are unchanged. + +## Migration + +1. Rename the `parentAgent` option to `ownerAgent` in creation and resume calls, and update custom `AgentFactory` implementations that read or forward it. Pass the same Agent as before. +2. Keep `meta.parentSession` and stored `parentSession` values unchanged. Do not substitute the runtime owner for the direct creator. +3. Typecheck consumers against the updated package, then verify that `ctx.agents.isOwnedBy(id, owner)` and `ctx.agents.roots()` report the same runtime relationships after creation and resume. diff --git a/docs/upgrade-guide/v0.2.1-rc.2/agent-runtime-owner/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.2/agent-runtime-owner/guide.zh.md new file mode 100644 index 0000000000..b1e36efd9f --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.2/agent-runtime-owner/guide.zh.md @@ -0,0 +1,20 @@ +--- +kind: upgrade-guide +description: "Agent 创建和恢复选项将表示运行归属的 parentAgent 改名为 ownerAgent。" +--- + +# Agent 运行归属使用 `ownerAgent` + +[English](guide.md) | 中文 + +## 变更 + +`@deepseek-ai/dsh-agent` 中的 `CreateAgentOptions` 和 `ResumeAgentOptions` 将 `parentAgent` 替换为 `ownerAgent`。这影响调用 `ctx.agents.create()` 或 `ctx.agents.resume()` 的插件与自定义工厂。该字段指定拥有运行实例的在线 Agent;省略时创建运行时根。 + +Session 创建来源仍由 `meta.parentSession` 记录。嵌套子代理的运行归属者可以是树根,而直接创建者是另一个后代。持久化 Session 字段和格式保持不变。 + +## 迁移 + +1. 将创建和恢复调用中的 `parentAgent` 选项改名为 `ownerAgent`,并更新读取或透传该字段的自定义 `AgentFactory` 实现。继续传入原来的 Agent。 +2. 保持 `meta.parentSession` 和存储的 `parentSession` 值不变。不要用运行归属者替换直接创建者。 +3. 针对更新后的包对消费者运行类型检查,再验证创建和恢复后的 `ctx.agents.isOwnedBy(id, owner)` 与 `ctx.agents.roots()` 仍报告相同的运行归属关系。 diff --git a/docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.i18n.yaml new file mode 100644 index 0000000000..1b127d0026 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.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 docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.md +/: + en: 1f073189776760d8 + zh: cfbb0403aea96f84 +/agent-teams-uses-subagents-and-session-ids: + en: 703c7766dd80e1d1 + zh: 157c67164c1cbc7d +/agent-teams-uses-subagents-and-session-ids/change: + en: d387c95a87d1bd6a + zh: 69dd000fbff0b6fa +/agent-teams-uses-subagents-and-session-ids/migration: + en: dcc698d29a4677f0 + zh: 2af5cea905ad4371 diff --git a/docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.md b/docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.md new file mode 100644 index 0000000000..dedc7025fb --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.md @@ -0,0 +1,31 @@ +--- +kind: upgrade-guide +description: "Agent Teams uses the standard subagent tools and Session ids; Team-specific creation, messaging, listing, interruption, and their configuration keys are removed." +--- + +# Agent Teams uses subagents and Session ids + +English | [中文](guide.zh.md) + +## Change + +Descriptions label responsibilities; only Session ids identify recipients. + +- `spawn_teammate` and the Team-scoped `send_message`, `list_agents`, and `interrupt_agent` are removed. The standard control tools use `agent_id`, not `target`. Members of one root tree can message each other across branches by id; interruption remains ancestor-only. +- `list_agents` takes no arguments and lists the entire tree. Remove scripted `scope` arguments. Rows use `kind: root`, `kind: child`, or `kind: diagnostic`; descendants include parent ids and root-relative depth. +- Team supplies four task tools and policy; controls supply required identity. `wait_agent` moves to ordinary subagent controls and waits for the caller’s next-step inbox, including pending input. Status changes and unrelated inboxes do not wake it; `noProgress` is removed. Timeout bounds/default remain unchanged. +- `agent-team` rejects `maxMembers`, `maxMessageBytes`, and `disposalTimeoutMs`; `tool-agent-team` rejects `interrupt`, `freshProvider`, and `forkProvider`. +- `ctx.agentTeams` removes creation, messaging, interruption, roster, membership, and wait APIs. `TeamMembership`, `TeamMemberView`, spawn/send request/result types, and `TeamWaitResult` are removed. Task ownership uses `ownerId`, including the root’s Session id. +- Runtime instances share root ownership without changing creation ancestry. Member settlement does not dispose descendants. Messages restore only their target. Hosts close the tree when its root closes; members obtain required results explicitly. +- Cold resume uses saved presets and permissions, not the creator’s current generation. Missing presets fail restoration; historical targets without one use host composition plus descriptor overrides. Lifecycle dispatch uses the root scope while retaining the direct creator in `parentSessionId`. Browser continuation requires the root online; failure uses `subagent/delivery-unavailable`. +- Historical Team roster and mailbox records remain readable. Historical names do not resolve message recipients or task owners. Subagent catalog payloads are unchanged. +- The Web Team panel shows tasks only; subagent navigation owns member browsing. The wire `agentTeam.members` field and `TeamMemberProjection` are removed; persisted roster state still decodes. Task commits do not end waits, deliver messages, or start idle Agents. Message another Agent when it needs to act on task changes. + +## Migration + +1. Move `interrupt: false` from `tool-agent-team` to `tool-subagent-control`. Remove other obsolete keys and any patch rows that disable subagent tools solely for Team support. +2. Replace `spawn_teammate({ name, description, prompt, context })` with `subagent({ description, prompt })`, or `subagent_fork` for inherited conversation history. Save the returned agent id. +3. Use `send_message({ agent_id, message })` and `interrupt_agent({ agent_id })`. Discover all Team member ids with `list_agents`. +4. Pass member Session ids to task `owner` fields and read `ownerId` in results. Use the Lead's Session id instead of the alias `lead`. +5. Replace `spawnTeammate`, `sendMessage`, and `interrupt` with `ctx.subagents.startActivation`, `sendMessage`, and `interrupt`. Replace `membership`/`tryMembership` with `rootOf`, and `listMembers` with `listDescendants`. Replace `waitForChange` with `wait_agent`, then re-read tasks. +6. SDK `session/resume` rejects subagent sessions. Resume the root Session instead. Verify navigation, nested task assignment, and that messages end inbox waits while task commits do not. diff --git a/docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.zh.md new file mode 100644 index 0000000000..177831b543 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.zh.md @@ -0,0 +1,31 @@ +--- +kind: upgrade-guide +description: "Agent Teams 使用标准 subagent 工具和 Session id;移除 Team 专属创建、消息、列举、中断操作及其配置键。" +--- + +# Agent Teams 使用子代理和 Session id + +[English](guide.md) | 中文 + +## 变更 + +描述标注职责;仅 Session id 标识接收者。 + +- 移除 `spawn_teammate` 以及 Team 作用域的 `send_message`、`list_agents` 和 `interrupt_agent`。标准控制工具使用 `agent_id` 而非 `target`。同一根下的成员可以通过 id 跨分支互发消息;中断仍只允许祖先执行。 +- `list_agents` 不接受参数,列出整棵树。移除脚本中的 `scope` 参数。行使用 `kind: root`、`kind: child` 或 `kind: diagnostic`;后代包含父 id 和相对根的深度。 +- Team 提供四个任务工具和策略;控制工具提供必需身份。`wait_agent` 移至普通 subagent 控制工具,等待调用者的下一步收件箱,包括待处理输入。状态变化和其他收件箱不会唤醒它;移除 `noProgress`。超时范围和默认值不变。 +- `agent-team` 拒绝 `maxMembers`、`maxMessageBytes` 和 `disposalTimeoutMs`;`tool-agent-team` 拒绝 `interrupt`、`freshProvider` 和 `forkProvider`。 +- `ctx.agentTeams` 移除创建、通信、中断、名册、成员关系和等待 API。`TeamMembership`、`TeamMemberView`、spawn/send 请求和结果类型以及 `TeamWaitResult` 被移除。任务归属使用 `ownerId`,包括根的 Session id。 +- 运行时实例共享根所有权,不改变创建祖先关系。成员收尾不释放后代。消息仅恢复目标。根关闭时 host 关闭整棵树;成员显式获取所需结果。 +- 冷恢复使用已保存的 preset 和权限,而非创建者当前代次。缺失 preset 会导致恢复失败;历史目标没有保存 preset 时,使用 host 组合及描述符覆盖。生命周期分派使用根作用域,同时在 `parentSessionId` 中保留直接创建者。浏览器继续执行需要根在线;失败使用 `subagent/delivery-unavailable`。 +- 历史 Team 名册和邮箱记录仍可读取。其中的名字仅作为历史数据,不用于解析消息收件人或任务所有者。Subagent catalog 载荷不变。 +- Web Team 面板仅显示任务;subagent 导航负责浏览成员。线上 `agentTeam.members` 字段与 `TeamMemberProjection` 被移除;持久化成员状态仍可解码。任务提交不结束等待、不投递消息,也不启动空闲 Agent。需要另一个 Agent 处理任务变更时,应发送消息。 + +## 迁移 + +1. 将 `interrupt: false` 从 `tool-agent-team` 移到 `tool-subagent-control`。删除其余废弃键,以及仅为支持 Team 而禁用 subagent 工具的 patch 行。 +2. 将 `spawn_teammate({ name, description, prompt, context })` 替换为 `subagent({ description, prompt })`;继承对话历史时使用 `subagent_fork`。保存返回的 agent id。 +3. 使用 `send_message({ agent_id, message })` 和 `interrupt_agent({ agent_id })`。通过 `list_agents` 查询所有 Team 成员的 id。 +4. 任务的 `owner` 字段传入成员 Session id,结果读取 `ownerId`。以 Lead 的 Session id 取代 `lead` 别名。 +5. 将 `spawnTeammate`、`sendMessage` 和 `interrupt` 替换为 `ctx.subagents.startActivation`、`sendMessage` 和 `interrupt`。将 `membership`/`tryMembership` 替换为 `rootOf`,将 `listMembers` 替换为 `listDescendants`。将 `waitForChange` 替换为 `wait_agent`,随后重新读取任务。 +6. SDK `session/resume` 拒绝子代理会话。改为恢复根 Session。验证导航、嵌套任务分配,以及消息结束收件箱等待而任务提交不会。 diff --git a/docs/upgrade-guide/v0.2.1-rc.2/session-team-mode/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.2/session-team-mode/guide.i18n.yaml new file mode 100644 index 0000000000..badcbe4e11 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.2/session-team-mode/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.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 docs/upgrade-guide/v0.2.1-rc.2/session-team-mode/guide.md +/: + en: 2b713df685f8b593 + zh: 4e185e636a3b5d99 +/select-agent-teams-per-session: + en: 9e82418f46f6275b + zh: d1282eec79b37b59 +/select-agent-teams-per-session/change: + en: 3ba5920179726841 + zh: 940e0d956a523dbc +/select-agent-teams-per-session/migration: + en: 7b6dc660602d9c3b + zh: 8f8459198300aa46 diff --git a/docs/upgrade-guide/v0.2.1-rc.2/session-team-mode/guide.md b/docs/upgrade-guide/v0.2.1-rc.2/session-team-mode/guide.md new file mode 100644 index 0000000000..8f275535b6 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.2/session-team-mode/guide.md @@ -0,0 +1,22 @@ +--- +kind: upgrade-guide +description: "Agent Teams defaults off for new Sessions and uses a saved per-Session selection." +--- + +# Select Agent Teams per Session + +English | [中文](guide.zh.md) + +## Change + +Agent Teams ships with base-backed profiles; new Sessions default to off. The Plugins page has no separate Agent Teams switch. The root Session’s selection controls task tools and Team guidance throughout its subagent tree. The model and reasoning menu offers **Use Agent Teams**, and the root conversation’s Subagent header control opens members above shared tasks in one panel while enabled. Child conversations retain navigation without a shared task board. + +Disabling Team retains tasks and subagents. Admitted task operations finish; queued operations check the current mode when they start. The selection survives restart. A historical task board without a saved selection remains enabled. The required `team/mode` event records selections; older readers reject this unfamiliar event. See the [persistence acknowledgement](../../../persistence-changes/2026-10-19-session-team-mode.md). + +## Migration + +1. Start each existing profile once. Profile loading removes the retired `@deepseek-ai/dsh-experimental-agent-team-profile` bundle selection; the base and Web bundles supply its plugins. Keep `agent-team`, `tool-agent-team`, and `ui-agent-team` row overrides. Custom compositions replace package names `@deepseek-ai/dsh-experimental-agent-team`, `@deepseek-ai/dsh-experimental-tool-agent-team`, and `@deepseek-ai/dsh-experimental-client-ui-agent-team` with `@deepseek-ai/dsh-agent-team`, `@deepseek-ai/dsh-tool-agent-team`, and `@deepseek-ai/dsh-client-ui-agent-team`. +2. Toggle **Use Agent Teams** in the root conversation’s model and reasoning menu. The `/team on` and `/team off` commands are removed. +3. Verify that enabling shows shared tasks in the root conversation below the Subagent tree in the same panel and exposes four task tools. Reload the Session to confirm the saved selection. Re-enabling after disabling restores the existing tasks. + +Client plugin consumers replace `TeamActionProps` with `TeamBoardProps`; task content registers in `conversation.session.header.actions.subagent.footer` instead of the header actions slot. diff --git a/docs/upgrade-guide/v0.2.1-rc.2/session-team-mode/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.2/session-team-mode/guide.zh.md new file mode 100644 index 0000000000..65f149a2f2 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.2/session-team-mode/guide.zh.md @@ -0,0 +1,22 @@ +--- +kind: upgrade-guide +description: "Agent Teams 在新 Session 中默认关闭,并使用按 Session 保存的选择。" +--- + +# 按 Session 选择智能体团队 + +[English](guide.md) | 中文 + +## 变更 + +Agent Teams 随基于 base 的 profile 提供;新 Session 默认关闭。插件页不再提供独立的智能体团队开关。根 Session 的选择控制整棵子代理树中的任务工具和 Team 提示词。模型与推理等级菜单提供**使用智能体团队**开关,开启后通过根会话的 Subagent 页头入口,在同一面板中查看上方成员与下方共享任务。子会话保留导航,不显示共享任务板。 + +关闭 Team 保留任务和子代理。已经进入执行的任务操作继续完成;排队中的操作在开始时检查当前模式。选择在重启后仍有效。没有保存选择的历史任务板保持开启。必读事件 `team/mode` 记录选择;旧读取器会拒绝该未知事件。参见[持久化变更确认](../../../persistence-changes/2026-10-19-session-team-mode.zh.md)。 + +## 迁移 + +1. 启动每个已有 profile 一次。加载时会移除已停用的 `@deepseek-ai/dsh-experimental-agent-team-profile` 组合选项;base 与 Web 组合提供对应插件。保留 `agent-team`、`tool-agent-team` 与 `ui-agent-team` 行覆盖。自定义组合将包名 `@deepseek-ai/dsh-experimental-agent-team`、`@deepseek-ai/dsh-experimental-tool-agent-team`、`@deepseek-ai/dsh-experimental-client-ui-agent-team` 分别替换为 `@deepseek-ai/dsh-agent-team`、`@deepseek-ai/dsh-tool-agent-team`、`@deepseek-ai/dsh-client-ui-agent-team`。 +2. 在根会话的模型与推理等级菜单中切换**使用智能体团队**。`/team on` 与 `/team off` 命令已移除。 +3. 在根会话中确认开启后通过 Subagent 入口在树下方查看共享任务板,并提供四个任务工具。重新加载 Session,确认选择已保存。关闭后再开启会恢复已有任务。 + +Client 插件消费者将 `TeamActionProps` 替换为 `TeamBoardProps`;任务内容注册到 `conversation.session.header.actions.subagent.footer`,而非页头操作插槽。 diff --git a/docs/upgrade-guide/v0.2.1-rc.2/subagent-activation-options/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.2/subagent-activation-options/guide.i18n.yaml new file mode 100644 index 0000000000..12139cd087 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.2/subagent-activation-options/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.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 docs/upgrade-guide/v0.2.1-rc.2/subagent-activation-options/guide.md +/: + en: b087162d9a24f772 + zh: a32f167c3529fb1c +/subagent-activation-options: + en: 20a5b01ef11bff72 + zh: 919de21bc865ee9e +/subagent-activation-options/change: + en: 7cc2554992e98c53 + zh: 434540bcd037d1f5 +/subagent-activation-options/migration: + en: 2243d08661eb457c + zh: a928a8e7fb3515ff diff --git a/docs/upgrade-guide/v0.2.1-rc.2/subagent-activation-options/guide.md b/docs/upgrade-guide/v0.2.1-rc.2/subagent-activation-options/guide.md new file mode 100644 index 0000000000..3f2a202b0c --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.2/subagent-activation-options/guide.md @@ -0,0 +1,18 @@ +--- +kind: upgrade-guide +description: "SubagentActivationSpec accepts task options directly instead of a nested request object." +--- + +# Subagent activation options + +English | [中文](guide.zh.md) + +## Change + +Plugin code calling `ctx.subagents.startActivation()` must pass `prompt`, `parent`, and optional execution settings directly on `SubagentActivationSpec`. The nested `request` field is removed. Type names, provider requests, cancellation, and result delivery are unchanged. + +## Migration + +1. In each `startActivation()` call, move the fields from `request` to the outer object. For an existing task object, replace `request: task` with `...task`; keep the activation's `label` and `signal` after the spread so they take precedence. +2. Replace reads such as `spec.request.prompt` with `spec.prompt`. Keep backend `SubagentStartRequest` implementations unchanged. +3. Typecheck the plugin and run its delegation tests to verify the updated calls. diff --git a/docs/upgrade-guide/v0.2.1-rc.2/subagent-activation-options/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.2/subagent-activation-options/guide.zh.md new file mode 100644 index 0000000000..32c2785bcc --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.2/subagent-activation-options/guide.zh.md @@ -0,0 +1,18 @@ +--- +kind: upgrade-guide +description: "SubagentActivationSpec 直接接受任务选项,不再使用嵌套的 request 对象。" +--- + +# 子代理 activation 选项 + +[English](guide.md) | 中文 + +## 变更 + +调用 `ctx.subagents.startActivation()` 的插件代码必须将 `prompt`、`parent` 和可选执行设置直接放在 `SubagentActivationSpec` 上。嵌套的 `request` 字段已移除。类型名称、提供方请求、取消和结果投递保持不变。 + +## 迁移 + +1. 在每个 `startActivation()` 调用中,将 `request` 内的字段移到外层对象。已有任务对象时,将 `request: task` 替换为 `...task`;将 activation 的 `label` 和 `signal` 放在展开操作之后,以确保它们优先生效。 +2. 将 `spec.request.prompt` 等读取替换为 `spec.prompt`。保持后端的 `SubagentStartRequest` 实现不变。 +3. 对插件运行类型检查和委派测试,验证更新后的调用。 diff --git a/docs/upgrade-guide/v0.2.1-rc.2/subagent-service-methods/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.2/subagent-service-methods/guide.i18n.yaml new file mode 100644 index 0000000000..464cd3ccbe --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.2/subagent-service-methods/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.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 docs/upgrade-guide/v0.2.1-rc.2/subagent-service-methods/guide.md +/: + en: 6955b11d6f698e4c + zh: 8a8d4d6edd5dea0c +/subagent-service-method-removal: + en: 102f43348f7c8eb0 + zh: be09ff117c31ada1 +/subagent-service-method-removal/change: + en: 8cd7f93df401c417 + zh: 27313127c2059e6b +/subagent-service-method-removal/migration: + en: f0f0f921f284efec + zh: 8b9e88e9412282c2 diff --git a/docs/upgrade-guide/v0.2.1-rc.2/subagent-service-methods/guide.md b/docs/upgrade-guide/v0.2.1-rc.2/subagent-service-methods/guide.md new file mode 100644 index 0000000000..a1eaad618b --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.2/subagent-service-methods/guide.md @@ -0,0 +1,19 @@ +--- +kind: upgrade-guide +description: "The subagent service removes drainChildren, listChildren, and list." +--- + +# Subagent service method removal + +English | [中文](guide.zh.md) + +## Change + +`ctx.subagents` removes `drainChildren(parent, childIds)`, `listChildren(parentSessionId, signal?)`, and `list()`. This affects plugins that release resident children by id, read direct-child catalogs through the service, or enumerate registered provider names. + +## Migration + +1. Retain the activation returned by `startActivation()` and await its `dispose()` to release that exact execution. An old activation cannot release a later execution of the same Session. There is no replacement for releasing the current resident child by id; use `drainDescendants([root])` only when closing the entire tree. `interrupt()` stops current work but preserves a local Activation. +2. For direct-child entries, use `ctx.sessionQuery.observeSession(parentSessionId, { signal })`, read `observation.projections?.values.subagentCatalog`, and dispose the observation with `using`. Load Session Query and the subagent service, and treat a missing catalog projection as unavailable. Keep `listDescendants()` for recursive discovery; its rows differ from direct catalog entries. +3. Use `getProvider(name)` to check known provider names. The service no longer enumerates all registered names. +4. Rebuild plugins against the updated service declarations. Verify registration and unload checks, exact-activation disposal, and catalog observation cleanup. diff --git a/docs/upgrade-guide/v0.2.1-rc.2/subagent-service-methods/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.2/subagent-service-methods/guide.zh.md new file mode 100644 index 0000000000..ce24ef1b90 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.2/subagent-service-methods/guide.zh.md @@ -0,0 +1,19 @@ +--- +kind: upgrade-guide +description: "子代理服务移除 drainChildren、listChildren 和 list。" +--- + +# 子代理服务方法移除 + +[English](guide.md) | 中文 + +## 变更 + +`ctx.subagents` 移除 `drainChildren(parent, childIds)`、`listChildren(parentSessionId, signal?)` 和 `list()`。这会影响按 id 释放驻留子代理、通过服务读取直接子级目录或枚举已注册 provider 名称的插件。 + +## 迁移 + +1. 保留 `startActivation()` 返回的 activation,并等待其 `dispose()` 完成以释放那一次执行。旧 activation 不能释放同一 Session 后来恢复的执行。按 id 释放当前驻留子代理没有替代方法;仅在关闭整棵树时使用 `drainDescendants([root])`。`interrupt()` 停止当前工作,但保留本地 Activation。 +2. 读取直接子级条目时,使用 `ctx.sessionQuery.observeSession(parentSessionId, { signal })`,读取 `observation.projections?.values.subagentCatalog`,并通过 `using` 释放观察对象。加载 Session Query 和子代理服务,并将缺失的目录投影视为不可用。递归发现继续使用 `listDescendants()`;它返回的行与直接目录条目不同。 +3. 使用 `getProvider(name)` 检查已知 provider 名称。服务不再枚举全部已注册名称。 +4. 根据更新后的服务声明重新构建插件,验证注册和卸载检查、指定 activation 的释放以及目录观察对象的清理。 diff --git a/docs/upgrade-guide/v0.2.1-rc.2/subagent-user-controls/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.2/subagent-user-controls/guide.i18n.yaml new file mode 100644 index 0000000000..1ef94f1d8f --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.2/subagent-user-controls/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.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 docs/upgrade-guide/v0.2.1-rc.2/subagent-user-controls/guide.md +/: + en: 9d77440fbd1ac5ee + zh: e51db37880a28684 +/subagent-user-controls-address-the-target-session: + en: 8f5fcee305e3a5d7 + zh: c6bb2c762cd2b86d +/subagent-user-controls-address-the-target-session/change: + en: 0a309d3a2a123b46 + zh: 416f075bfdb8664a +/subagent-user-controls-address-the-target-session/migration: + en: f3a61ba7c3a5110e + zh: 95c88d6f9d685fd4 diff --git a/docs/upgrade-guide/v0.2.1-rc.2/subagent-user-controls/guide.md b/docs/upgrade-guide/v0.2.1-rc.2/subagent-user-controls/guide.md new file mode 100644 index 0000000000..9080ef5f66 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.2/subagent-user-controls/guide.md @@ -0,0 +1,26 @@ +--- +kind: upgrade-guide +description: "Subagent user controls address only the target Session, rename the interrupt endpoint, and require target Session permissions instead of global permissions." +--- + +# Subagent user controls address the target Session + +English | [中文](guide.zh.md) + +## Change + +Browser and custom Remote callers send subagent prompts without `parentSessionId` or `mode`. `subagents/interruptByParent(childSessionId, parentSessionId, mode)` is replaced by `subagents/interruptByUser(childSessionId)`. Creation ancestry, history addresses, completion recipients, and persisted Session data are unchanged. + +Both operations name the child as a `session` access resource. Non-owner policies must grant the corresponding action on that Session; a `global` binding or a binding on its parent does not grant access. Owner behavior is unchanged. Model messages remain limited to one subagent tree, and model interruption remains ancestor-only. + +The same-process `SubagentInterruptAuthority` user variant is `{ kind: 'user' }`. The published `@deepseek-ai/dsh-subagent/internal` entry point is removed, including its host prompt adapters and tool identity markers. Remote authorization is performed by the Gateway. + +Old prompt payloads with extra fields inside `request` can decode on the new Host, subject to its new access rules. Old interrupt calls fail because the endpoint is removed. New callers require the new Host. Upgrade custom clients and hosts together; the bundled Web client is delivered with its Host. + +## Migration + +1. Remove `parentSessionId` and `mode` from subagent prompt requests. Keep `childSessionId`, `requestId`, `delivery`, `content`, and optional `clientTimeZone`. +2. Replace calls to `interruptByParent(childId, parentId, 'continuable')` with `interruptByUser(childId)`. +3. Update non-owner policies to grant `subagents/prompt` and/or `subagents/interruptByUser` on `{ kind: 'session', id: childId }`. Parent-to-child grants remain an explicit policy decision. +4. Remove `parentSessionId` from `{ kind: 'user' }` interrupt authorities. Replace imports from `@deepseek-ai/dsh-subagent/internal`: authenticated user prompts use `subagents/prompt`; model-authored messages use `ctx.subagents.sendMessage()`. There is no public arbitrary-source host prompt adapter. Custom tools own their instructions through tool descriptions and `systemPrompt.context()`, without identity markers. Keep parent ids in history addresses and creation records. +5. Verify that a target-authorized user can send and interrupt, while a user authorized only for the parent or global resource is refused before any input or cancellation is admitted. diff --git a/docs/upgrade-guide/v0.2.1-rc.2/subagent-user-controls/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.2/subagent-user-controls/guide.zh.md new file mode 100644 index 0000000000..8b9d8fab50 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.2/subagent-user-controls/guide.zh.md @@ -0,0 +1,26 @@ +--- +kind: upgrade-guide +description: "子代理用户控制仅按目标 Session 寻址,重命名中断端点,并以目标 Session 权限替代 global 权限。" +--- + +# 子代理用户控制按目标 Session 寻址 + +[English](guide.md) | 中文 + +## 变更 + +浏览器和自定义 Remote 调用方发送子代理提示词时,不再提供 `parentSessionId` 或 `mode`。`subagents/interruptByParent(childSessionId, parentSessionId, mode)` 替换为 `subagents/interruptByUser(childSessionId)`。创建祖先、历史地址、完成通知接收者和持久化 Session 数据保持不变。 + +两个操作都将子会话声明为 `session` 访问资源。非 owner 策略必须授予该 Session 上对应操作的权限;`global` 绑定或父会话上的绑定不授予访问权。owner 行为保持不变。模型消息仍限于同一棵子代理树,模型中断仍仅允许祖先发起。 + +同进程 `SubagentInterruptAuthority` 的用户分支为 `{ kind: 'user' }`。已发布的 `@deepseek-ai/dsh-subagent/internal` 入口已删除,包括宿主 prompt 适配器和工具身份标记。远程授权由 Gateway 执行。 + +旧提示词载荷在 `request` 内多带字段时,可以在新 Host 上解码,但须满足新的访问规则。旧中断调用因端点已移除而失败。新调用方要求新 Host。自定义客户端和宿主应一起升级;内置 Web 客户端随 Host 一起交付。 + +## 迁移 + +1. 从子代理提示词请求中移除 `parentSessionId` 和 `mode`。保留 `childSessionId`、`requestId`、`delivery`、`content` 和可选的 `clientTimeZone`。 +2. 将 `interruptByParent(childId, parentId, 'continuable')` 调用替换为 `interruptByUser(childId)`。 +3. 更新非 owner 策略,在 `{ kind: 'session', id: childId }` 上授予 `subagents/prompt` 和/或 `subagents/interruptByUser` 权限。父会话到子会话的授权仍由策略显式决定。 +4. 从 `{ kind: 'user' }` 中断授权中移除 `parentSessionId`。替换 `@deepseek-ai/dsh-subagent/internal` 的导入:经过认证的用户 prompt 使用 `subagents/prompt`;模型编写的消息使用 `ctx.subagents.sendMessage()`。不再提供允许任意来源的公开宿主 prompt 适配器。自定义工具通过工具描述和 `systemPrompt.context()` 提供说明,无需身份标记。历史地址和创建记录仍保留父级 id。 +5. 验证获准操作目标的用户能够发送和中断,而仅获准操作父会话或 global 资源的用户在输入或取消被接纳前遭到拒绝。 diff --git a/docs/upgrade-guide/v0.2.1-rc.3/inbox-only-agent-wait/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.3/inbox-only-agent-wait/guide.i18n.yaml new file mode 100644 index 0000000000..dadc616c76 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/inbox-only-agent-wait/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.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 docs/upgrade-guide/v0.2.1-rc.3/inbox-only-agent-wait/guide.md +/: + en: f09a5fde2cedb48c + zh: beec903a10ed62ee +/agent-waits-require-inbox-input: + en: 96c9f5fc6514557a + zh: aac52f37d543ed9b +/agent-waits-require-inbox-input/change: + en: 568583cb15f85337 + zh: 6c7e3028f23d1f3e +/agent-waits-require-inbox-input/migration: + en: 156a027d11ad3769 + zh: 301029cecd3ae640 diff --git a/docs/upgrade-guide/v0.2.1-rc.3/inbox-only-agent-wait/guide.md b/docs/upgrade-guide/v0.2.1-rc.3/inbox-only-agent-wait/guide.md new file mode 100644 index 0000000000..9252add26b --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/inbox-only-agent-wait/guide.md @@ -0,0 +1,19 @@ +--- +kind: upgrade-guide +description: "Task-board changes no longer end wait_agent calls; send a message to request further work." +--- +# Agent waits require inbox input + +English | [中文](guide.zh.md) + +## Change + +`wait_agent` waits only for the calling Agent's next-step inbox. Task creation, assignment, completion, and other task updates no longer end an active wait. They do not add inbox messages or start idle Agents. Queued next-turn input and other Agents' status changes also leave the wait pending. + +Existing next-step input returns immediately; new messages, completion notifications, and user steering end the wait without consuming that input. Timeout and cancellation behavior remain unchanged. + +## Migration + +1. After changing a task, use `send_message` when its owner or another member needs to act on the update. Do not rely on a task mutation to release `wait_agent`. +2. Read current task state before acting; a task update is not a message or an instruction to begin a new turn. +3. Verify that a task update leaves an empty-inbox wait pending, then send a message and confirm that the wait returns with `timedOut: false`. diff --git a/docs/upgrade-guide/v0.2.1-rc.3/inbox-only-agent-wait/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.3/inbox-only-agent-wait/guide.zh.md new file mode 100644 index 0000000000..27bd015147 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/inbox-only-agent-wait/guide.zh.md @@ -0,0 +1,19 @@ +--- +kind: upgrade-guide +description: "任务板变更不再结束 wait_agent 调用;需要继续工作时发送消息。" +--- +# Agent 等待需要收件箱输入 + +[English](guide.md) | 中文 + +## 变更 + +`wait_agent` 仅等待调用 Agent 的下一步收件箱。任务创建、分配、完成和其他任务更新不再结束正在进行的等待,也不添加收件箱消息或启动空闲 Agent。排队的下一轮输入和其他 Agent 的状态变化同样不会结束等待。 + +已有下一步输入时立即返回;新消息、完成通知和用户引导会结束等待,但不消费输入。超时和取消行为保持不变。 + +## 迁移 + +1. 修改任务后,需要任务所有者或其他成员处理更新时使用 `send_message`。不要依赖任务修改来结束 `wait_agent`。 +2. 行动前读取当前任务状态;任务更新不是消息,也不是开始新轮次的指令。 +3. 验证任务更新不会结束空收件箱上的等待,然后发送消息,确认等待返回 `timedOut: false`。 diff --git a/docs/upgrade-guide/v0.2.1-rc.3/subagent-runtime-ownership/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.3/subagent-runtime-ownership/guide.i18n.yaml new file mode 100644 index 0000000000..2972e9a489 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/subagent-runtime-ownership/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.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 docs/upgrade-guide/v0.2.1-rc.3/subagent-runtime-ownership/guide.md +/: + en: 10f7c9b47f2b39f2 + zh: 2c93aa4e6bef0f33 +/subagent-runtime-ownership: + en: 20c105e3d68b46ee + zh: 685c1ad4eef7eefb +/subagent-runtime-ownership/change: + en: bacb015157610964 + zh: 635055eab24394ac +/subagent-runtime-ownership/migration: + en: 3fb0ce93ad94074f + zh: 49e2f520191d6c53 diff --git a/docs/upgrade-guide/v0.2.1-rc.3/subagent-runtime-ownership/guide.md b/docs/upgrade-guide/v0.2.1-rc.3/subagent-runtime-ownership/guide.md new file mode 100644 index 0000000000..2cc07287e3 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/subagent-runtime-ownership/guide.md @@ -0,0 +1,21 @@ +--- +kind: upgrade-guide +description: Agent factory ownership parameters and registry tree queries are removed; subagent Activations own runtime tree membership. +--- + +# Subagent runtime ownership + +English | [中文](guide.zh.md) + +## Change + +`@deepseek-ai/dsh-agent` removes the create/resume runtime-owner parameter (`parentAgent` or `ownerAgent`), `AgentRegistry.isOwnedBy()`, `AgentRegistry.roots()`, and the owner argument of `enter()`. Direct factory creation no longer enrolls an Agent in a managed subagent tree. Custom factories no longer implement runtime tree ownership. + +The subagent service resolves runtime membership from its existing Activations through `rootOf(agent)`. The Session format and Cordis context ownership of handles are unchanged. + +## Migration + +1. Create delegated work through `ctx.subagents.startActivation()` with a registered provider. Remove ownership parameters from direct `ctx.agents.create()` and `resume()` calls; these operations create independent runtime instances. +2. Use `ctx.agents.get(id)` and `list()` for live lookup. To select ordinary live Agents, filter `list()` by `agent.session.header.origin !== 'subagent'`; historical ordinary forks can still have `parentSession`. +3. Query the live tree root with `ctx.subagents.rootOf(agent)` and compare Agent objects, not only ids. Ordinary live Agents resolve to themselves; unmanaged delegated or stale instances throw `SubagentError` with code `UNAUTHORIZED`. Read `session.header.parentSession` for the direct creator; use durable subagent catalogs when traversing offline creation ancestors. +4. Update custom factory implementations to call `ctx.agents.enter(agent)` after setup. Retain caller-context disposal, initialization rollback, and exact-instance registration checks. Runtime tree membership is available after managed activation publication; `agent/created` runs before that point. Verify stale instances cannot reuse a disposed instance's ownership. diff --git a/docs/upgrade-guide/v0.2.1-rc.3/subagent-runtime-ownership/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.3/subagent-runtime-ownership/guide.zh.md new file mode 100644 index 0000000000..ee212f5c96 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/subagent-runtime-ownership/guide.zh.md @@ -0,0 +1,21 @@ +--- +kind: upgrade-guide +description: Agent 工厂归属参数和注册表树查询已删除;subagent Activation 拥有运行时树成员关系。 +--- + +# Subagent 运行时归属 + +[English](guide.md) | 中文 + +## 变更 + +`@deepseek-ai/dsh-agent` 删除 create/resume 的运行时归属参数(`parentAgent` 或 `ownerAgent`)、`AgentRegistry.isOwnedBy()`、`AgentRegistry.roots()` 以及 `enter()` 的 owner 参数。直接通过工厂创建 Agent 不再将其加入托管 subagent 树。自定义工厂不再实现运行时树归属。 + +subagent 服务通过 `rootOf(agent)` 从已有 Activation 解析运行时成员关系。Session 格式和 Cordis 上下文对句柄的所有权不变。 + +## 迁移 + +1. 通过 `ctx.subagents.startActivation()` 和已注册提供方创建委派工作。删除直接调用 `ctx.agents.create()` 与 `resume()` 时的归属参数;这些操作创建独立的运行时实例。 +2. 通过 `ctx.agents.get(id)` 与 `list()` 查询存活实例。筛选普通存活 Agent 时,用 `agent.session.header.origin !== 'subagent'` 过滤 `list()`;历史普通 fork 仍可能包含 `parentSession`。 +3. 通过 `ctx.subagents.rootOf(agent)` 查询存活树根,并比较 Agent 对象,而非仅比较 id。普通存活 Agent 返回自身;非托管的委派实例或失效实例抛出代码为 `UNAUTHORIZED` 的 `SubagentError`。直接创建者读取 `session.header.parentSession`;遍历离线创建祖先时使用持久化 subagent 目录。 +4. 更新自定义工厂,在 setup 后调用 `ctx.agents.enter(agent)`。保留调用方上下文释放、初始化回滚和确切实例注册检查。运行时树成员关系在托管 activation 发布后可用;`agent/created` 早于该时点。验证失效实例不能复用已释放实例的归属。 diff --git a/docs/upgrade-guide/v0.2.1-rc.3/team-mode-menu/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.3/team-mode-menu/guide.i18n.yaml new file mode 100644 index 0000000000..8916474764 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/team-mode-menu/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.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 docs/upgrade-guide/v0.2.1-rc.3/team-mode-menu/guide.md +/: + en: 1c39dada6c6162a7 + zh: ab2f5e2824bfbf0c +/select-team-mode-from-the-conversation-menu: + en: d570d0f970f6a77a + zh: 7a722461f27bc620 +/select-team-mode-from-the-conversation-menu/change: + en: 37b569a98d41587d + zh: ce9c8ee4c35ac634 +/select-team-mode-from-the-conversation-menu/migration: + en: d548bbca6276ed74 + zh: 2468a89bd342b3ff diff --git a/docs/upgrade-guide/v0.2.1-rc.3/team-mode-menu/guide.md b/docs/upgrade-guide/v0.2.1-rc.3/team-mode-menu/guide.md new file mode 100644 index 0000000000..0d54d68096 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/team-mode-menu/guide.md @@ -0,0 +1,18 @@ +--- +kind: upgrade-guide +description: "The Team mode slash command is removed; use the root conversation menu." +--- + +# Select Team mode from the conversation menu + +English | [中文](guide.zh.md) + +## Change + +`/team on` and `/team off` are no longer registered. The root conversation’s **Use Agent Teams** switch calls `remote.agentTeams.setEnabled` directly. Saved selections, task retention, and root-only authorization are unchanged. + +## Migration + +1. Enable or disable **Use Agent Teams** in the root conversation’s model and reasoning menu. +2. Plugins that execute the command should call `ctx.agentTeams.setEnabled(rootAgent, enabled)` instead; browser clients call `remote.agentTeams.setEnabled(rootSessionId, enabled)`. +3. Confirm the switch reflects the saved selection after reloading the Session. diff --git a/docs/upgrade-guide/v0.2.1-rc.3/team-mode-menu/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.3/team-mode-menu/guide.zh.md new file mode 100644 index 0000000000..c8e847e91f --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/team-mode-menu/guide.zh.md @@ -0,0 +1,18 @@ +--- +kind: upgrade-guide +description: "Team 模式斜杠命令已移除,请使用根会话菜单。" +--- + +# 从会话菜单选择 Team 模式 + +[English](guide.md) | 中文 + +## 变更 + +`/team on` 和 `/team off` 不再注册。根会话的**使用智能体团队**开关直接调用 `remote.agentTeams.setEnabled`。保存的选择、任务保留以及仅允许根会话修改的授权规则不变。 + +## 迁移 + +1. 在根会话的模型与推理等级菜单中打开或关闭**使用智能体团队**。 +2. 执行该命令的插件改为调用 `ctx.agentTeams.setEnabled(rootAgent, enabled)`;浏览器客户端调用 `remote.agentTeams.setEnabled(rootSessionId, enabled)`。 +3. 重新加载 Session,确认开关反映已保存的选择。 diff --git a/docs/upgrade-guide/v0.2.1-rc.3/team-projection/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.3/team-projection/guide.i18n.yaml new file mode 100644 index 0000000000..b709e46f55 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/team-projection/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.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 docs/upgrade-guide/v0.2.1-rc.3/team-projection/guide.md +/: + en: 185304d86b229610 + zh: b98e40567653a55f +/team-projection-retains-tasks-and-mode: + en: c20510084cf9d412 + zh: ee26f2365ab5d216 +/team-projection-retains-tasks-and-mode/change: + en: 5b92baca861b7281 + zh: 57b4162fb425163c +/team-projection-retains-tasks-and-mode/migration: + en: 6c30148dea17af5e + zh: 81b7fa72cc118252 diff --git a/docs/upgrade-guide/v0.2.1-rc.3/team-projection/guide.md b/docs/upgrade-guide/v0.2.1-rc.3/team-projection/guide.md new file mode 100644 index 0000000000..fe0258e51e --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/team-projection/guide.md @@ -0,0 +1,20 @@ +--- +kind: upgrade-guide +description: "Team projection stops restoring historical teammates and mailbox state while retaining tasks and saved mode." +--- + +# Team projection retains tasks and mode + +English | [中文](guide.zh.md) + +## Change + +The `agentTeam` projection no longer restores `members`, `messages`, or `delivered` from historical `team/member` and `team/message/*` events. It ignores their payloads, so historical roster or mailbox validation failures no longer block task reads or writes. Historical teammates are not restored and queued mail is not delivered. + +Task revisions, task owners, deleted-task records, and the saved Team mode remain supported. Ordinary subagent discovery and messaging are unchanged. The Session format and committed log files do not change; projection checkpoint version 7 rebuilds older cached state from the log. + +## Migration + +1. No data conversion or configuration change is required. Open the Session normally to rebuild incompatible projection checkpoints. +2. Use `list_agents` and `send_message` for current teammates. Read historical roster or mailbox events from the original Session log if you need an archive; the Team projection no longer supplies these collections. +3. Confirm that the task board and saved Team mode are restored. Historical roster and mailbox records do not grant task-assignment authority. diff --git a/docs/upgrade-guide/v0.2.1-rc.3/team-projection/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.3/team-projection/guide.zh.md new file mode 100644 index 0000000000..b6b131599e --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/team-projection/guide.zh.md @@ -0,0 +1,20 @@ +--- +kind: upgrade-guide +description: "Team 投影停止恢复历史队友与邮箱状态,同时保留任务和保存的模式。" +--- + +# Team 投影保留任务与模式 + +[English](guide.md) | 中文 + +## 变更 + +`agentTeam` 投影不再从历史 `team/member` 和 `team/message/*` 事件恢复 `members`、`messages` 或 `delivered`。投影忽略这些载荷,因此历史名册或邮箱校验失败不再阻止任务读写。不恢复历史队友,也不投递排队邮件。 + +任务修订、任务所有者、已删除任务记录和保存的 Team 模式仍受支持。普通子代理发现与通信保持不变。Session 格式和已提交日志文件不变;投影检查点版本 7 从日志重建旧缓存状态。 + +## 迁移 + +1. 无需转换数据或修改配置。正常打开 Session 即可重建不兼容的投影检查点。 +2. 使用 `list_agents` 和 `send_message` 与当前队友协作。需要归档信息时,从原始 Session 日志读取历史名册或邮箱事件;Team 投影不再提供这些集合。 +3. 确认任务板和保存的 Team 模式已恢复。历史名册与邮箱记录不授予任务分配权限。 diff --git a/docs/upgrade-guide/v0.2.2-alpha.2/cua-source-dev-helper/guide.i18n.yaml b/docs/upgrade-guide/v0.2.2-alpha.2/cua-source-dev-helper/guide.i18n.yaml new file mode 100644 index 0000000000..1e9d8c17c2 --- /dev/null +++ b/docs/upgrade-guide/v0.2.2-alpha.2/cua-source-dev-helper/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.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 docs/upgrade-guide/v0.2.2-alpha.2/cua-source-dev-helper/guide.md +/: + en: 2f745e146345238c + zh: 9b41a7625ae45170 +/source-built-computer-use-helper-identity: + en: 72a8ea364f7427fa + zh: 9e7ed2e00425bddb +/source-built-computer-use-helper-identity/change: + en: c1aa84364896b994 + zh: a4559c563d995b2b +/source-built-computer-use-helper-identity/migration: + en: a86e330ab60ef800 + zh: ce32e1959ca707fc diff --git a/docs/upgrade-guide/v0.2.2-alpha.2/cua-source-dev-helper/guide.md b/docs/upgrade-guide/v0.2.2-alpha.2/cua-source-dev-helper/guide.md new file mode 100644 index 0000000000..92d0339b4e --- /dev/null +++ b/docs/upgrade-guide/v0.2.2-alpha.2/cua-source-dev-helper/guide.md @@ -0,0 +1,19 @@ +--- +kind: upgrade-guide +description: "Source-built Computer Use helpers use a separate development bundle identifier and need one new macOS permission grant." +--- + +# Source-built Computer Use helper identity + +English | [中文](guide.zh.md) + +## Change + +Source checkouts previously built `DSH Computer Use.app` with the release bundle identifier `com.deepseek.harness.computer-use` and an ad-hoc signature. They now build `DSH Computer Use Dev.app` with bundle identifier `com.deepseek.harness.computer-use.dev`. Accessibility and Screen Recording grants for the old entry do not apply to it. Desktop and npm installations are unchanged. Source builds still default to ad-hoc signing; developers generate and maintain fixed identities outside DSH home and select them through configuration. + +## Migration + +1. Keep the default `sourceSigning: ad-hoc` for temporary signatures. To reduce repeated grants after rebuilds and across worktrees, follow the [developer signing skill](../../../../.agents/skills/dsh-cua-macos-signing/SKILL.md) to generate or reuse a certificate, then write `sourceSigning: { identity, keychain }` into the DSH home’s `cordis.patch.yml`; `keychain` must be an absolute path. +2. Start DSH from the source checkout, call Computer Use, and grant Accessibility and Screen Recording to `DSH Computer Use Dev` in System Settings. +3. Confirm that `getCapabilities` reports both permissions as `true`. With a fixed signature, check again after changing and rebuilding Swift code and from another worktree. Remove an old `DSH Computer Use` entry only when no installed DSH uses it. +4. After the keychain locks, rerun setup as described in the skill to unlock and reuse the identity. To stop using it, select `ad-hoc` or remove the configuration, then remove the identity through the skill; deleting DSH home does not delete the developer keychain. diff --git a/docs/upgrade-guide/v0.2.2-alpha.2/cua-source-dev-helper/guide.zh.md b/docs/upgrade-guide/v0.2.2-alpha.2/cua-source-dev-helper/guide.zh.md new file mode 100644 index 0000000000..64552b7524 --- /dev/null +++ b/docs/upgrade-guide/v0.2.2-alpha.2/cua-source-dev-helper/guide.zh.md @@ -0,0 +1,19 @@ +--- +kind: upgrade-guide +description: "源码构建的 Computer Use helper 使用独立的开发版 Bundle ID,需要重新授予一次 macOS 权限。" +--- + +# 源码构建的 Computer Use helper 身份 + +[English](guide.md) | 中文 + +## 变更 + +源码检出原先以临时签名构建 `DSH Computer Use.app`,使用正式版 Bundle ID `com.deepseek.harness.computer-use`。现在构建 `DSH Computer Use Dev.app`,使用 `com.deepseek.harness.computer-use.dev`。旧条目的辅助功能和屏幕录制授权不能沿用。Desktop 和 npm 安装版不变。源码构建默认仍为临时签名;固定身份由开发者在 DSH home 之外生成和维护,再通过配置指定。 + +## 迁移 + +1. 保持默认 `sourceSigning: ad-hoc` 即可使用临时签名。需要减少重新编译和跨 worktree 的授权次数时,按[开发者签名 skill](../../../../.agents/skills/dsh-cua-macos-signing/SKILL.md)生成或复用证书,并将 `sourceSigning: { identity, keychain }` 写入 DSH home 的 `cordis.patch.yml`;`keychain` 必须为绝对路径。 +2. 从源码检出启动 DSH,调用 Computer Use,在系统设置中给 `DSH Computer Use Dev` 授予辅助功能和屏幕录制权限。 +3. 确认 `getCapabilities` 的两项权限均为 `true`。使用固定签名时,在修改 Swift 源码重建和另一个 worktree 中分别复查。只有确认没有安装版 DSH 使用旧条目时,才移除旧 `DSH Computer Use` 条目。 +4. 钥匙串锁定后,按 skill 重新运行 setup 解锁并复用现有身份。停止使用时改回 `ad-hoc` 或移除该配置,再按 skill 删除身份;删除 DSH home 不会删除开发者钥匙串。 diff --git a/docs/user/guide/schedule.i18n.yaml b/docs/user/guide/schedule.i18n.yaml index 73c5f4dae9..31fba2c702 100644 --- a/docs/user/guide/schedule.i18n.yaml +++ b/docs/user/guide/schedule.i18n.yaml @@ -12,14 +12,14 @@ en: c1d163bf7d366e24 zh: 34dccfbb0bf3701d /schedule-reminders/create-reminders: - en: 9bd962e3653a8a72 - zh: 6b0887ed1477420c + en: 762ecb7a7743cd2d + zh: 1b163ae679601767 /schedule-reminders/inspect-run-and-delete-tasks: - en: 9407dca2b849be4a - zh: 9e158a42f4d95305 + en: 9f1e9d732597aa28 + zh: 66feec44adf24332 /schedule-reminders/edit-a-task: - en: 45f9dac1f9d86696 - zh: 3193f010cc6168da + en: fe8c59251f0b4620 + zh: a422e44f758adae1 /schedule-reminders/timing-and-delivery-reference: en: acb84b04d567d15c zh: c658a81d8c6875f4 diff --git a/docs/user/guide/schedule.md b/docs/user/guide/schedule.md index 37ed142182..bdee6c86b3 100644 --- a/docs/user/guide/schedule.md +++ b/docs/user/guide/schedule.md @@ -27,7 +27,7 @@ A conversation that a delivery creates joins the Workspace registered at its wor For example, ask: “Remind me every day at 23:00 in Asia/Shanghai to check the weather.” Below the reply, a card shows the created reminder's name and frequency; Open shows its details in the right sidebar. Check that the frequency repeats every day at 23:00, not Once. A one-time reminder does not become recurring after delivery. After the reminder is deleted, the card shows Deleted and no longer opens it. -To create a reminder without the model, open Automation tasks from the sidebar, open Choose a creation method beside New, and choose Create manually; when no task exists, New automation task opens the same dialog. The Create manually dialog asks for an Automation task name, an Automation task instruction, Run in, Linked workspace, and Run time. It starts with Once at one hour from now in the browser's time zone and with Create a conversation and reuse it. Run in offers Create a conversation and reuse it, which creates one conversation at the first run and keeps using it, and New conversation for every run, which creates a separate conversation at each run and shows the Host's minimum run interval. Linked workspace lists each registered Workspace with its directory and selects the Workspace whose directory the new conversation uses; it starts at Default workspace, or at the first Workspace when the default Workspace is not registered. When no Workspace exists, Linked workspace shows Ungrouped and the Host project directory is used. The new conversation uses the default preset. Create creates the task and selects it in the list, and Cancel closes the dialog without creating one. If the Host refuses the task, the dialog keeps your input and states the problem, for example that the date and time must be in the future. +To create a reminder without the model, open Automation tasks from the sidebar, open Choose a creation method beside New, and choose Create manually; when no task exists, New automation task opens the same editor. This editor also opens existing tasks and asks for an Automation task name, an Automation task instruction, Run in, Linked workspace, and Run time. It starts with Once at one hour from now in the browser's time zone and with Create a conversation and reuse it. Run in offers Create a conversation and reuse it, which creates one conversation at the first run and keeps using it, and New conversation for every run, which creates a separate conversation at each run and shows the Host's minimum run interval. Linked workspace lists each registered Workspace with its directory and selects the Workspace whose directory the new conversation uses; it starts at Default workspace, or at the first Workspace when the default Workspace is not registered. When no Workspace exists, Linked workspace shows Ungrouped and the Host project directory is used. The new conversation uses the default preset. Create creates the task and selects it in the list, and Discard closes the editor without creating one. If the Host refuses the task, the editor keeps your input and states the problem, for example that the date and time must be in the future. New, and Create in a conversation in the same menu, open a new conversation in the current or most recent Workspace; ask the model there to create the task. @@ -50,7 +50,7 @@ When retention removed earlier records, the last page states that earlier delive Active tasks participate in scheduling. Paused and ended tasks do not schedule another delivery. A one-time task ends after its reminder is durably written to the conversation inbox, and a repeating task ends after the last occurrence of its rule; choose All or Ended in Task status to find an ended task. The Session header clock, the row mark, and the hover card show only active tasks, so disappearance from those views does not mean that the task was deleted. -Pause in the details stops the future deliveries of an active task, and Resume restarts a paused task. Run now, in the Automation task actions menu, asks the Host to deliver the instruction once now and keeps the next scheduled run, also for a paused or ended task. While a delivery of a task that delivers to an existing conversation or to one new conversation is queued or running, Edit, Pause, Resume, Run now, and Delete task are disabled, and the details state that the task is queued or running and that you manage it in its conversation, which the button at the bottom of the details opens. A task that creates a new conversation for every run is not disabled this way; its runs continue beside one another. After a delivery, Run now stays disabled until the task's minimum delivery interval ends; while only the interval holds it, hold the pointer on Run now or move the keyboard to it to see when it becomes available. If the Host refuses a run for one of these reasons, the details state that the task is queued, running, or was just delivered. Run now is also disabled while the conversation that the task delivers to is archived. If you stop the conversation while the task's instruction waits there, the instruction stays queued until your next message, the task's next scheduled run, or Run now starts it. +Pause in the details stops the future deliveries of an active task, and Resume restarts a paused task. Run now, in the Automation task actions menu, asks the Host to deliver the instruction once now and keeps the next scheduled run, also for a paused or ended task. While a delivery of a task that delivers to an existing conversation or to one new conversation is queued or running, the fields, Save changes, Pause, Resume, Run now, and Delete task are disabled, and the details state that the task is queued or running and that you manage it in its conversation, which the button at the bottom of the details opens. A task that creates a new conversation for every run is not disabled this way; its runs continue beside one another. After a delivery, Run now stays disabled until the task's minimum delivery interval ends; while only the interval holds it, hold the pointer on Run now or move the keyboard to it to see when it becomes available. If the Host refuses a run for one of these reasons, the details state that the task is queued, running, or was just delivered. Run now is also disabled while the conversation that the task delivers to is archived. If you stop the conversation while the task's instruction waits there, the instruction stays queued until your next message, the task's next scheduled run, or Run now starts it. Choose Delete task from the Automation task actions menu and confirm to stop future delivery and remove the task from the list, including for a paused or ended task. Deletion removes the task together with its saved delivery records, so the task's saved history is gone. Confirming the deletion reports the outcome in one app-wide notice — deleted, which also closes the details, or a failure that leaves the task in place to retry; it does not remove the conversations or retract a message already queued there. The row stays visible until a successful query confirms the remaining tasks. If a query fails, Retry reloads the catalog; the error is not an empty successful result. @@ -59,7 +59,7 @@ Choose Delete task from the Automation task actions menu and confirm to stop fut Ask the model to change a reminder's name, instruction, repeat rule, or delivery target, or to pause or resume it; the change keeps the reminder id and saved delivery records. -In Automation tasks, select a task and choose Edit to change its Automation task name, Automation task instruction, Run in, Linked workspace, and Run time, including for a paused or ended task. Edits stay in a local draft until you choose Save changes. Editing keeps the task id, latest delivery receipt, and all saved delivery records. A changed rule makes an ended task active again and keeps a paused task paused. For a task that delivers to an existing conversation, Run in shows Current linked conversation; choosing another option makes later runs create their conversations and sets Linked workspace to its starting value, as in Create manually. A task keeps its stored preset while its Workspace stays the same; a changed Workspace uses the default preset. +In Automation tasks, select a task to change its Automation task name, Automation task instruction, Run in, Linked workspace, and Run time, including for a paused or ended task. Edits stay in a local draft until you choose Save changes. Editing keeps the task id, latest delivery receipt, and all saved delivery records. A changed rule makes an ended task active again and keeps a paused task paused. For a task that delivers to an existing conversation, Run in shows Current linked conversation; choosing another option makes later runs create their conversations and sets Linked workspace to its starting value, as in Create manually. A task keeps its stored preset while its Workspace stays the same; a changed Workspace uses the default preset. - Once: Date opens a month calendar, Time opens Hour, Minute, and Second columns, and Time zone interprets them. The task runs once and then ends. - Every N minutes and Every N hours: Interval sets the number of minutes or hours, at least 1. The interval counts from the minute at which you save the task. @@ -70,9 +70,9 @@ In Automation tasks, select a task and choose Edit to change its Automation task Time zone lists recently selected zones first, marks the system zone, and searches by UTC offset, IANA ID, or city. It appears for guided choices and Custom expressions that use a separate zone; otherwise the Custom rule text supplies its zone. -The fields change only a local draft. While you edit, the bottom of the details shows Unsaved changes with Cancel and Save changes; Save changes sends only the changed fields in one update, and Cancel restores the stored values. A save sends a new rule only when the timing changed. Save changes stays disabled while the name or instruction is empty or a guided choice is incomplete. The Host validates a supplied Custom rule on save; invalid text keeps the draft and shows an error. The fields are disabled while a save is in flight. Leaving the details does not roll back a Host write already begun. +The fields change only a local draft. If the task targets an existing conversation, Current linked session remains available so you can undo a destination change without discarding other edits. While you edit, the bottom of the details shows Unsaved changes with Discard and Save changes; Save changes sends only the changed fields in one update, and Discard restores the stored values. A save sends a new rule only when the timing changed. Save changes stays disabled until a meaningful change has valid fields and timing. Invalid Custom text keeps the draft and shows a correction message; the Host also validates the rule on save. Pause, resume, Run now, and deletion wait until you save or discard unsaved changes. The fields are disabled while a save is in flight. Leaving the details does not roll back a Host write already begun. -A catalog refresh does not change the draft. If delivery or another edit changed the task before the save arrives, the Host refuses the save and changes nothing; the details state that the task changed elsewhere, Load latest version replaces the draft with the latest version, and Save changes stays disabled until then. If another save fails, the draft stays and the details ask you to try again; check the task before retrying an update whose outcome is unknown. An accepted update refreshes the catalog. Late responses after closing details or switching tasks do not replace the current view. +Switching tasks or leaving the page keeps unsaved drafts. A catalog refresh updates a pristine form; an edited form keeps all fields and its original version. If delivery or another edit changes the task, the details state that the task changed elsewhere and disable Save changes when the newer version arrives. The Host also checks the version on save. Review with my edits keeps the title, prompt, timing, and target groups you changed and loads the latest values for the other groups. Review the whole form before saving; this action alone does not save. If the same group changed elsewhere, your edits take precedence in the form. Discard replaces the whole draft with the latest stored values. If another save fails, the draft stays and the details ask you to try again; check the task before retrying an update whose outcome is unknown. An accepted update refreshes the catalog. Late responses after closing details or switching tasks do not replace the current view. ## Timing and delivery reference diff --git a/docs/user/guide/schedule.zh.md b/docs/user/guide/schedule.zh.md index 1e52ae1f60..ebf7b24946 100644 --- a/docs/user/guide/schedule.zh.md +++ b/docs/user/guide/schedule.zh.md @@ -27,7 +27,7 @@ 例如,请求:“每天 Asia/Shanghai 时区 23:00 提醒我查看天气。”回复下方的卡片显示刚创建的提醒的名称和频率;“打开”在右侧栏显示其详情。检查频率是每天 23:00 重复,而非“仅一次”。一次性提醒不会在投递后变成重复任务。提醒删除后,卡片显示“已删除”,且不再打开该提醒。 -不经模型创建提醒时,从侧栏打开自动化任务页面,打开“新建”旁的“选择创建方式”,再选择“手动创建”;尚无任务时,“新建自动化任务”打开同一对话框。“手动创建”对话框要求填写“自动化任务名称”“自动化任务内容”“执行位置”“关联工作区”和“运行时间”。初始运行时间为“仅一次”,时间为浏览器时区下的一小时后,初始执行位置为“新建对话并复用”。“执行位置”提供“新建对话并复用”和“每次新建对话”:前者在首次运行时创建一个对话并在之后沿用,后者在每次运行时创建独立对话,并显示宿主的最小运行间隔。“关联工作区”列出每个已注册的 Workspace 及其目录,并选择新对话使用其目录的 Workspace;初始值为“默认工作区”,未注册默认 Workspace 时为第一个 Workspace。尚无任何 Workspace 时,“关联工作区”显示“未分组”,并使用宿主项目目录。新对话使用默认 preset。“创建”创建该任务并在列表中选中它,“取消”关闭对话框而不创建任务。若宿主拒绝该任务,对话框保留你的输入并说明问题,例如日期和时间必须在未来。 +不经模型创建提醒时,从侧栏打开自动化任务页面,打开“新建”旁的“选择创建方式”,再选择“手动创建”;尚无任务时,“新建自动化任务”打开同一编辑页。已有任务也使用此页面,填写“自动化任务名称”“自动化任务内容”“执行位置”“关联工作区”和“运行时间”。初始运行时间为“仅一次”,时间为浏览器时区下的一小时后,初始执行位置为“新建对话并复用”。“执行位置”提供“新建对话并复用”和“每次新建对话”:前者在首次运行时创建一个对话并在之后沿用,后者在每次运行时创建独立对话,并显示宿主的最小运行间隔。“关联工作区”列出每个已注册的 Workspace 及其目录,并选择新对话使用其目录的 Workspace;初始值为“默认工作区”,未注册默认 Workspace 时为第一个 Workspace。尚无任何 Workspace 时,“关联工作区”显示“未分组”,并使用宿主项目目录。新对话使用默认 preset。“创建”创建该任务并在列表中选中它,“放弃”关闭编辑页而不创建任务。若宿主拒绝该任务,编辑页保留你的输入并说明问题,例如日期和时间必须在未来。 “新建”以及同一菜单中的“在对话中创建”会在当前或最近使用的 Workspace 中打开一个新对话;在其中请模型创建该任务。 @@ -50,7 +50,7 @@ 活动任务参与调度;已暂停和已结束的任务不会再次安排投递。一次性任务在提醒持久写入对话收件箱后结束,重复任务在其规则的最后一个发生时点后结束;在“任务状态”中选择“全部”或“已结束”即可找到已结束的任务。Session 顶部的提醒时钟、Session 行的时钟标识和悬浮卡片只显示活动任务,因此从这些视图消失并不表示任务已被删除。 -详情中的“暂停任务”停止活动任务之后的投递,“恢复任务”重新启动已暂停的任务。“自动化任务操作”菜单中的“立即执行”请宿主现在投递一次指令,并保留下次计划运行;已暂停或已结束的任务也可以这样运行。投递到现有对话或一个新对话的任务的一次投递已排队或正在运行时,“编辑”“暂停任务”“恢复任务”“立即执行”和“删除任务”均禁用,详情说明此任务已排队或执行中,请在会话中管理任务;详情底部的按钮打开该会话。每次运行都创建新对话的任务不会这样禁用;它的各次运行并行继续。一次投递之后,“立即执行”保持禁用,直到该任务的最小投递间隔结束;仅因间隔禁用时,将指针停在“立即执行”上或用键盘移到该项,即可查看它恢复可用的时间。若宿主因上述原因之一拒绝运行,详情会说明该任务已排队、执行中或刚执行过。任务投递到的对话已归档时,“立即执行”同样禁用。若在任务指令仍在对话中等待时停止该对话,指令会保持排队,直到你的下一条消息、该任务的下一次计划运行或“立即执行”使其开始。 +详情中的“暂停任务”停止活动任务之后的投递,“恢复任务”重新启动已暂停的任务。“自动化任务操作”菜单中的“立即执行”请宿主现在投递一次指令,并保留下次计划运行;已暂停或已结束的任务也可以这样运行。投递到现有对话或一个新对话的任务的一次投递已排队或正在运行时,字段、“保存修改”“暂停任务”“恢复任务”“立即执行”和“删除任务”均禁用,详情说明此任务已排队或执行中,请在会话中管理任务;详情底部的按钮打开该会话。每次运行都创建新对话的任务不会这样禁用;它的各次运行并行继续。一次投递之后,“立即执行”保持禁用,直到该任务的最小投递间隔结束;仅因间隔禁用时,将指针停在“立即执行”上或用键盘移到该项,即可查看它恢复可用的时间。若宿主因上述原因之一拒绝运行,详情会说明该任务已排队、执行中或刚执行过。任务投递到的对话已归档时,“立即执行”同样禁用。若在任务指令仍在对话中等待时停止该对话,指令会保持排队,直到你的下一条消息、该任务的下一次计划运行或“立即执行”使其开始。 从“自动化任务操作”菜单选择“删除任务”并确认,可停止未来投递并将该任务从列表中移除,包括已暂停或已结束的任务。删除会移除该任务及其已保存的发送记录,因此该任务的已保存历史随之消失。确认删除会在同一个应用级提示里报告结果——已删除(同时关闭详情),或失败(失败时任务保留、可重试);删除不会删除相关对话,也不会撤回已进入其队列的消息。该行会保留到成功查询确认剩余任务为止。查询失败时,“重试”重新加载目录;错误不会被视为查询成功的空结果。 @@ -59,7 +59,7 @@ 请模型修改提醒的名称、指令、重复规则或投递目标,或暂停、恢复提醒;修改保留提醒 id 与已保存的投递记录。 -在自动化任务页面选择任务并选择“编辑”,即可修改其“自动化任务名称”“自动化任务内容”“执行位置”“关联工作区”和“运行时间”,包括已暂停或已结束的任务。修改先保存在本地草稿中,直到选择“保存修改”。编辑保留任务 id、最近一次投递回执以及全部已保存的发送记录。规则改变后,已结束的任务重新变为活动,已暂停的任务保持暂停。对于投递到现有对话的任务,“执行位置”显示“当前关联对话”;选择其他选项后,之后的运行会创建各自的对话,“关联工作区”也设为其初始值,与“手动创建”相同。Workspace 保持不变时,任务保留已存储的 preset;改换 Workspace 后使用默认 preset。 +在自动化任务页面选择任务,即可直接修改其“自动化任务名称”“自动化任务内容”“执行位置”“关联工作区”和“运行时间”,包括已暂停或已结束的任务。修改先保存在本地草稿中,直到选择“保存修改”。编辑保留任务 id、最近一次投递回执以及全部已保存的发送记录。规则改变后,已结束的任务重新变为活动,已暂停的任务保持暂停。对于投递到现有对话的任务,“执行位置”显示“当前关联对话”;选择其他选项后,之后的运行会创建各自的对话,“关联工作区”也设为其初始值,与“手动创建”相同。Workspace 保持不变时,任务保留已存储的 preset;改换 Workspace 后使用默认 preset。 - “仅一次”:“日期”打开月历,“时间”打开“时”“分”“秒”三列,“时区”决定如何解释它们。任务运行一次后结束。 - “每 N 分钟”和“每 N 小时”:“重复间隔”设置分钟数或小时数,至少为 1。间隔从你保存任务时所在的分钟起算。 @@ -70,9 +70,9 @@ “时区”先列出最近选择的时区,标出系统时区,并可按 UTC 偏移、IANA 标识或城市搜索。引导式选项和使用独立时区的“自定义”表达式显示此字段;其他“自定义”规则由文本指定时区。 -各字段只修改本地草稿。编辑期间,详情底部显示“有未保存的修改”以及“取消”和“保存修改”;“保存修改”以一次更新只提交已修改的字段,“取消”恢复已存储的值。只有时间规则改变时,保存才会提交新规则。名称或指令为空,或引导式选择不完整时,“保存修改”保持禁用。宿主在保存时验证所填的“自定义”规则;文本无效时保留草稿并显示错误。保存进行期间,各字段禁用。离开详情不会回滚宿主已开始的写入。 +各字段只修改本地草稿。若任务指向已有会话,执行位置仍提供“当前关联会话”,可撤销目标变更而保留其他修改。编辑期间,详情底部显示“有未保存的修改”以及“放弃”和“保存修改”;“保存修改”以一次更新只提交已修改的字段,“放弃”恢复已存储的值。只有时间规则改变时,保存才会提交新规则。只有实际修改且字段和时间规则有效时,才能“保存修改”。无效的自定义文本保留在草稿中,并显示纠正提示;宿主也会在保存时校验规则。有未保存修改时,先保存或放弃更改,才能暂停、恢复、立即执行或删除。保存进行期间,各字段禁用。离开详情不会回滚宿主已开始的写入。 -刷新目录不会改变草稿。若投递或其他编辑在保存到达前改变了任务,宿主拒绝保存且不做任何修改;详情会说明此任务已在其他位置更新,“载入最新版本”用最新版本替换草稿,在此之前“保存修改”保持禁用。保存因其他原因失败时,草稿保留,详情提示你重试;对于结果未知的更新,请在重试前检查任务。更新被接受后刷新目录。关闭详情或切换任务后才到达的响应不会替换当前视图。 +切换任务或离开页面时保留未保存的草稿。刷新目录会更新未修改的表单;已修改的表单保留全部字段和原版本。若投递或其他编辑改变了任务,详情会在收到新版本后说明任务已在其他位置更新,并禁用“保存修改”。宿主也会在保存时检查版本。“保留修改并复核”保留你改过的名称、内容、时间和目标字段组,其余字段组载入最新值。保存前请复核整份表单;此操作本身不保存。若同一字段组在其他位置也有更改,表单优先保留你的修改。“放弃”用最新存储值替换整份草稿。保存因其他原因失败时,草稿保留,详情提示你重试;对于结果未知的更新,请在重试前检查任务。更新被接受后刷新目录。关闭详情或切换任务后才到达的响应不会替换当前视图。 ## 时间与投递参考 diff --git a/packages/README.i18n.yaml b/packages/README.i18n.yaml index 92c89f5ff2..d96e805d23 100644 --- a/packages/README.i18n.yaml +++ b/packages/README.i18n.yaml @@ -15,8 +15,8 @@ en: ffc55792d6e922e1 zh: 83bd759638857e2d /packages/package-groups: - en: c397fc0c1885058a - zh: c720d0e91dd1d15e + en: 60a6056460b8d4c1 + zh: fce319504ae9b6ef /packages/release-expectations: en: 236081c1659b25ab zh: 809110adf003802e diff --git a/packages/README.md b/packages/README.md index ed7c55ec5a..85a63643d0 100644 --- a/packages/README.md +++ b/packages/README.md @@ -52,6 +52,7 @@ Every package lives in exactly one group; new packages join existing groups, and | [`compaction/`](compaction/README.md) | Compaction capability family: Service Definition + basic provider + command Consumer | | [`context/`](context/README.md) | Model-visible request context: workspace instructions, time context, references | | [`subagent/`](subagent/README.md) | Subagent capability family: provider-registry contract and model-facing delegation tools | +| [`team/`](team/README.md) | Team coordination: shared task boards and model-facing task tools | | [`jobs/`](jobs/README.md) | Generic background-job runtime and model-facing job control tools | | [`experimental/`](experimental/README.md) | Capabilities under evaluation; [status and support policy](experimental/README.md#status) | | [`workflow/`](workflow/README.md) | Workflow seam, PTC process engine, and the model-facing `workflow` tool | diff --git a/packages/README.zh.md b/packages/README.zh.md index ae3a18bbc1..f293666d2f 100644 --- a/packages/README.zh.md +++ b/packages/README.zh.md @@ -52,6 +52,7 @@ harness 由 `packages/` 下的 npm 包组装而成,按能力系列分组:会 | [`compaction/`](compaction/README.zh.md) | 压缩(compaction)能力系列:Service Definition + 基础提供方 + 命令 Consumer | | [`context/`](context/README.zh.md) | 模型可见请求上下文:workspace 指令、时间上下文、引用 | | [`subagent/`](subagent/README.zh.md) | subagent 能力系列:提供方注册表约定和面向模型的委托工具 | +| [`team/`](team/README.zh.md) | Team 协调:共享任务板与面向模型的任务工具 | | [`jobs/`](jobs/README.zh.md) | 通用后台任务运行时和面向模型的作业控制工具 | | [`experimental/`](experimental/README.zh.md) | 仍在评估中的能力;[状态与支持策略](experimental/README.zh.md#status) | | [`workflow/`](workflow/README.zh.md) | 工作流 seam、PTC 进程引擎、面向模型的 `workflow` 工具 | diff --git a/packages/acp/acp/src/session.ts b/packages/acp/acp/src/session.ts index 2af289b3a2..e1abf0b2f0 100644 --- a/packages/acp/acp/src/session.ts +++ b/packages/acp/acp/src/session.ts @@ -24,8 +24,8 @@ import { assistantUpdates, toolCallUpdate, toolResultUpdate } from './updates.ts interface SubagentLifecycle { /** Dispose subagent descendants below exact host-owned parents child-first. */ drainDescendants(parents: readonly Agent[]): Promise - /** Observe currently managed descendants without closing future admission. */ - waitForChildren(parent: Agent): Promise + /** Join progressing work in the root's subagent tree without closing future admission. */ + waitForTreeWork(root: Agent): Promise } /** Inputs shared by fresh and resumed ACP session construction. */ @@ -489,7 +489,7 @@ export class AcpSession { await Promise.race([this.agent.whenIdle(), cancelled]) if (isCancelled()) break const idleSeq = this.agent.session.seq - const children = await Promise.race([subagents?.waitForChildren(this.agent), cancelled]) + const children = await Promise.race([subagents?.waitForTreeWork(this.agent), cancelled]) if (isCancelled()) break if (children || !idleAt(idleSeq)) continue await this.outputTail diff --git a/packages/acp/acp/tests/turns.spec.ts b/packages/acp/acp/tests/turns.spec.ts index e85c2553d9..39eb9f784e 100644 --- a/packages/acp/acp/tests/turns.spec.ts +++ b/packages/acp/acp/tests/turns.spec.ts @@ -50,7 +50,7 @@ async function installDelegationTool(ctx: BridgeHarness['ctx']): Promise<() => b if (exec.agent === undefined) throw new Error('delegation fixture requires an Agent') const child = await ctx.subagents.startActivation({ provider: 'local-test', label: 'later child', delivery: 'caller', signal: exec.signal, - request: { parent: exec.agent, prompt: [{ type: 'text', text: 'later child task' }] }, + parent: exec.agent, prompt: [{ type: 'text', text: 'later child task' }], }) const result = await child.result delegated = result.stopReason === 'completed' @@ -215,7 +215,7 @@ describe('ACP prompt lifecycle', () => { const appendSpy = vi.spyOn(parent.session, 'append') const failures: number[] = [] harness.ctx.on('agent/error', ({ turn }) => { failures.push(turn) }) - const waitForChildren = vi.fn().mockImplementationOnce(() => { + const waitForTreeWork = vi.fn().mockImplementationOnce(() => { appendSpy.mockImplementation(((type: string, ...rest: never[]) => { if (type === failedEvent) throw new Error(`summary ${failedEvent} unavailable`) return (append as (...args: never[]) => unknown)(type as never, ...rest) @@ -223,7 +223,7 @@ describe('ACP prompt lifecycle', () => { parent.followup(createUserMessage({ content: [{ type: 'text', text: 'child result' }], source: { kind: 'test' } })) return Promise.resolve(true) }).mockResolvedValue(false) - harness.ctx.provide('subagents', { waitForChildren, drainDescendants: vi.fn() } as never) + harness.ctx.provide('subagents', { waitForTreeWork, drainDescendants: vi.fn() } as never) try { await expect(harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'delegate' }] })) .rejects.toThrow(`summary ${failedEvent} unavailable`) @@ -594,10 +594,10 @@ describe('ACP prompt lifecycle', () => { const parent = harness.ctx.agents.get(SessionId(sessionId))! const waiting = Promise.withResolvers() const children = Promise.withResolvers() - const waitForChildren = vi.fn() + const waitForTreeWork = vi.fn() .mockImplementationOnce(() => { waiting.resolve(undefined); return children.promise }) .mockResolvedValue(false) - harness.ctx.provide('subagents', { waitForChildren, drainDescendants: vi.fn() } as never) + harness.ctx.provide('subagents', { waitForTreeWork, drainDescendants: vi.fn() } as never) let settled = false const prompt = harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'delegate' }] }) const observed = prompt.then(() => { settled = true }, () => { settled = true }) @@ -619,11 +619,11 @@ describe('ACP prompt lifecycle', () => { const sessionId = await newSession(harness) const waiting = Promise.withResolvers() const children = Promise.withResolvers() - const waitForChildren = vi.fn() + const waitForTreeWork = vi.fn() .mockImplementationOnce(() => { waiting.resolve(undefined); return children.promise }) .mockResolvedValue(false) const drainDescendants = vi.fn() - harness.ctx.provide('subagents', { waitForChildren, drainDescendants } as never) + harness.ctx.provide('subagents', { waitForTreeWork, drainDescendants } as never) const first = harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'delegate' }] }) await waiting.promise await harness.client.cancel({ sessionId }) @@ -647,7 +647,7 @@ describe('ACP prompt lifecycle', () => { const parent = ctx.agents.get(SessionId(sessionId))! const waiting = Promise.withResolvers() const children = Promise.withResolvers() - vi.spyOn(ctx.subagents, 'waitForChildren').mockImplementationOnce(() => { + vi.spyOn(ctx.subagents, 'waitForTreeWork').mockImplementationOnce(() => { waiting.resolve(undefined) return children.promise }) @@ -873,11 +873,11 @@ describe('ACP prompt lifecycle', () => { await delivery.promise if (fail) throw new Error('summary image unavailable') } - const waitForChildren = vi.fn().mockImplementationOnce(() => { + const waitForTreeWork = vi.fn().mockImplementationOnce(() => { parent.followup(createUserMessage({ content: [{ type: 'text', text: 'child result' }], source: { kind: 'test' } })) return Promise.resolve(true) }).mockResolvedValue(false) - harness.ctx.provide('subagents', { waitForChildren, drainDescendants: vi.fn() } as never) + harness.ctx.provide('subagents', { waitForTreeWork, drainDescendants: vi.fn() } as never) let settled = false const prompt = harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'delegate' }] }) const observed = prompt.then(() => { settled = true }, () => { settled = true }) @@ -910,7 +910,7 @@ describe('ACP prompt lifecycle', () => { const sessionId = await newSession(harness) const parent = ctx.agents.get(SessionId(sessionId))! const noChildren = Promise.withResolvers() - vi.spyOn(ctx.subagents, 'waitForChildren').mockImplementationOnce(() => { + vi.spyOn(ctx.subagents, 'waitForTreeWork').mockImplementationOnce(() => { noChildren.resolve(undefined) return Promise.resolve(false) }) diff --git a/packages/api/remotes/README.i18n.yaml b/packages/api/remotes/README.i18n.yaml index f3b9eab6fc..64b8496be6 100644 --- a/packages/api/remotes/README.i18n.yaml +++ b/packages/api/remotes/README.i18n.yaml @@ -15,8 +15,8 @@ en: a7a78c6d1aeebeb3 zh: 4bb69f32d8b05ba3 /deepseek-ai-dsh-api-remotes/use-this-package: - en: a80d44e0683768a5 - zh: 379833ac180e5418 + en: 6e3448339c646a7e + zh: 5dc00ff4cceaa88c /deepseek-ai-dsh-api-remotes/forwarded-host-events: en: 31bb9a2f50094b84 zh: 226c055739d5def7 diff --git a/packages/api/remotes/README.md b/packages/api/remotes/README.md index 8d49cb49a5..a05773a4af 100644 --- a/packages/api/remotes/README.md +++ b/packages/api/remotes/README.md @@ -29,7 +29,7 @@ Two-sided BFF for Host Remote capabilities selected by this application. The Hos [`@deepseek-ai/dsh-api-session-controller`](../session-controller/README.md) owns Agent and Session identity policy, including the Typert lookup resolvers used by other namespaces. This package selects and mounts that generated Session contribution without a direct runtime dependency on `@deepseek-ai/dsh-session`; activation policy stays with Session Controller. -The Client assembly mounts Office conversion, Commands, credentials, settings, Goal, Schedule, dynamic Cordis, file and Session references, read-only Host plugin inventory, plugin-installation registry response comparison, message feedback, permission presets, Session Controller, subagents, and Workspace Controller contributions. The `permissionPresets` namespace returns the complete process-level catalog used by current-session controls. Cordis effect ownership withdraws every contribution when this assembly unloads, while `@deepseek-ai/dsh-api-gateway/client` owns descriptor validation, traced namespace Services, direct and scoped methods, invocation, streams, and cancellation. The Client entry consumes the shared `TypertClientRemote` interface through Cordis and does not import the concrete Gateway. It re-exports the Gateway Client face's declaration merges type-only, so a consumer reaching the forwarded-event vocabulary through this facade gains no runtime edge to the Gateway implementation. +The Client assembly mounts Office conversion, Commands, Agent Teams, credentials, settings, Goal, Schedule, dynamic Cordis, file and Session references, read-only Host plugin inventory, plugin-installation registry response comparison, message feedback, permission presets, Session Controller, subagents, and Workspace Controller contributions. The `permissionPresets` namespace returns the complete process-level catalog used by current-session controls. Cordis effect ownership withdraws every contribution when this assembly unloads, while `@deepseek-ai/dsh-api-gateway/client` owns descriptor validation, traced namespace Services, direct and scoped methods, invocation, streams, and cancellation. The Client entry consumes the shared `TypertClientRemote` interface through Cordis and does not import the concrete Gateway. It re-exports the Gateway Client face's declaration merges type-only, so a consumer reaching the forwarded-event vocabulary through this facade gains no runtime edge to the Gateway implementation. This facade is also the front door for the wire type vocabulary a Client package names. It re-exports, type-only, the Remote failure vocabulary (`RemoteResult`, `RemoteFailure`, `RemoteErrorCode`, `RemoteErrorDetailsMap`), the Host facts (`RemoteHostFacts`), and each selected domain's client-safe payload types, so a Client feature package imports one specifier instead of reaching into `dsh-typert-protocol`, the Gateway, or an owner's Host entry. Two kinds of package deliberately skip this door: the API-layer packages this assembly itself selects — importing it back would close a dependency cycle — and their tests, which take the failure vocabulary from `dsh-typert-protocol` directly. A UI package's tests instead take the `RemoteError` constructor from [`dsh-client-test-runtime`](../../test-support/client-runtime/README.md). diff --git a/packages/api/remotes/README.zh.md b/packages/api/remotes/README.zh.md index 4f9772403d..bc616b9588 100644 --- a/packages/api/remotes/README.zh.md +++ b/packages/api/remotes/README.zh.md @@ -29,7 +29,7 @@ kind: "package-reference" [`@deepseek-ai/dsh-api-session-controller`](../session-controller/README.zh.md) 拥有 agent(智能体)与会话身份策略,包括供其他 namespace 使用的 Typert lookup 解析器。本包选择并挂载生成的会话 contribution,无需直接依赖 `@deepseek-ai/dsh-session` 的运行时;激活策略由会话控制器负责。 -Client 组合挂载 Office 转换、Commands、凭据、settings、Goal、Schedule、动态 Cordis、文件与会话引用、只读 Host 插件清单、插件安装源响应比较、消息反馈、权限预设、会话控制器、subagents 和 Workspace 控制器 contribution。`permissionPresets` namespace 返回 current-session 控件使用的完整进程级目录。该组合卸载时,Cordis effect 的所有权机制会撤回所有贡献;`@deepseek-ai/dsh-api-gateway/client` 负责描述符校验、可追踪的 namespace 服务、直接与作用域方法、调用、流与取消。Client 入口通过 Cordis 消费共享的 `TypertClientRemote` 接口,不导入具体 Gateway;它只以 type-only 形式重新导出 Gateway Client face 的声明合并,因此消费端经由本外观取到转发事件词汇时,运行时不会多出一条通往 Gateway 实现的边。 +Client 组合挂载 Office 转换、Commands、Agent Teams、凭据、settings、Goal、Schedule、动态 Cordis、文件与会话引用、只读 Host 插件清单、插件安装源响应比较、消息反馈、权限预设、会话控制器、subagents 和 Workspace 控制器 contribution。`permissionPresets` namespace 返回 current-session 控件使用的完整进程级目录。该组合卸载时,Cordis effect 的所有权机制会撤回所有贡献;`@deepseek-ai/dsh-api-gateway/client` 负责描述符校验、可追踪的 namespace 服务、直接与作用域方法、调用、流与取消。Client 入口通过 Cordis 消费共享的 `TypertClientRemote` 接口,不导入具体 Gateway;它只以 type-only 形式重新导出 Gateway Client face 的声明合并,因此消费端经由本外观取到转发事件词汇时,运行时不会多出一条通往 Gateway 实现的边。 本 facade 同时是 Client 包指称 wire 类型词汇的正门。它以 type-only 方式转出 Remote 失败词汇(`RemoteResult`、`RemoteFailure`、`RemoteErrorCode`、`RemoteErrorDetailsMap`)、Host 事实(`RemoteHostFacts`),以及各已选领域对 Client 安全的载荷类型,因此 Client 功能包只 import 一个 specifier,不必伸手进 `dsh-typert-protocol`、Gateway 或某个拥有方的 Host 入口。有两类包刻意不走这道门:本装配自己选中的 API 层包——反向 import 会形成依赖环——以及它们的测试,后者直接从 `dsh-typert-protocol` 取失败词汇。UI 包的测试则从 [`dsh-client-test-runtime`](../../test-support/client-runtime/README.zh.md) 取 `RemoteError` 构造器。 diff --git a/packages/api/remotes/package.json b/packages/api/remotes/package.json index 8735bc4339..1fe16a4a87 100644 --- a/packages/api/remotes/package.json +++ b/packages/api/remotes/package.json @@ -99,6 +99,7 @@ "@deepseek-ai/dsh-api-account-controller": "workspace:*", "@deepseek-ai/dsh-deepseek-account": "workspace:*", "@deepseek-ai/dsh-client-product-analytics": "workspace:*", + "@deepseek-ai/dsh-agent-team": "workspace:*", "@deepseek-ai/dsh-client-ui-trajectory": "workspace:*" } } diff --git a/packages/api/remotes/src/client/index.ts b/packages/api/remotes/src/client/index.ts index 8a4b7dea8a..86112b7cc2 100644 --- a/packages/api/remotes/src/client/index.ts +++ b/packages/api/remotes/src/client/index.ts @@ -6,6 +6,8 @@ export type {} from '@deepseek-ai/dsh-client-product-analytics/remote' import agentPresetsRemote from '@deepseek-ai/dsh-agent-preset-registry/remote' import userQuestionsRemote from '@deepseek-ai/dsh-user-questions/remote' import commandsRemote from '@deepseek-ai/dsh-commands/remote' +import agentTeamsRemote from '@deepseek-ai/dsh-agent-team/remote' +export type {} from '@deepseek-ai/dsh-agent-team/remote' import accountRemote from '@deepseek-ai/dsh-api-account-controller/remote' import settingsControllerRemote from '@deepseek-ai/dsh-api-settings-controller/remote' import officeToPdfRemote from '@deepseek-ai/dsh-office-to-pdf/remote' @@ -189,7 +191,7 @@ export async function apply(ctx: Context): Promise<() => Promise> { const disposers: Array<() => Promise> = [] try { for (const contribution of [ - productAnalyticsRemote, agentPresetsRemote, commandsRemote, settingsControllerRemote, accountRemote, + productAnalyticsRemote, agentPresetsRemote, commandsRemote, agentTeamsRemote, settingsControllerRemote, accountRemote, goalsRemote, llmRemote, dynamicRemote, scheduleRemote, pluginInventoryRemote, pluginManagerRemote, pluginMarketRemote, pluginRegistryProbeRemote, toolResultsRemote, messageFeedbackRemote, sessionFeedbackRemote, diff --git a/packages/api/remotes/tsconfig.client.json b/packages/api/remotes/tsconfig.client.json index 70181b08c5..49b3180c43 100644 --- a/packages/api/remotes/tsconfig.client.json +++ b/packages/api/remotes/tsconfig.client.json @@ -11,6 +11,7 @@ "src/types.ts" ], "references": [ + { "path": "../../team/agent-team" }, { "path": "../../client/product-analytics/tsconfig.client.json" }, { "path": "../../../vendor/cordis" diff --git a/packages/api/session-controller/README.i18n.yaml b/packages/api/session-controller/README.i18n.yaml index 010c41bf94..fb96bac533 100644 --- a/packages/api/session-controller/README.i18n.yaml +++ b/packages/api/session-controller/README.i18n.yaml @@ -15,11 +15,11 @@ en: 1fdf707322fb6996 zh: 0204f7e9d4aa3cce /session-controller/use-this-package: - en: d3d53e899960d96e - zh: 16b2bc7fe74215d8 + en: 25c9c54037dc7971 + zh: 7fa87a380b046afb /session-controller/client-references: - en: 41bdda1ac32df558 - zh: aa5d0e325924066b + en: e8979a803ec3e194 + zh: c567efcd971728ba /session-controller/session-media-references: en: 954923654eebe2dc zh: 6bd9a7d2ea965aed diff --git a/packages/api/session-controller/README.md b/packages/api/session-controller/README.md index 6fd7b24561..4ce2afd480 100644 --- a/packages/api/session-controller/README.md +++ b/packages/api/session-controller/README.md @@ -33,7 +33,7 @@ History pages and follow opening snapshots carry one `{ type: 'event', event: Se The Client journal validates current Session event envelopes before publishing follow snapshots, live entries, or history pages. It reuses the browser-safe Session validators for required surface markers, exact replacement endpoints, earlier unique source seqs, embedded Assistant provider metadata, request-header omissions, and tool-error consistency. Invalid records fail without field stripping or normalization; range membership and source existence remain durable-log checks on the Host. -Each endpoint states its activation policy. List reads only stored headers and projection-cache rows: it never calls per-session stat or opens a cold Session body. A current-format cache identity may supply every list hint; a lifecycle-matching predecessor cache may supply only its version-compatible title as a stale display fact, never as an authoritative fold seed. Search, history pages, log following, skill discovery, and workspace-path opening can inspect persistence without activating an Agent; `canOpenWorkspacePath()` reports native-opening availability without addressing a Session. Cancellation requires live state; queue mutation, model, rename, prompt, and file-reference operations may resolve or resume an ordinary Session. Prompt rejects content with neither non-whitespace text nor an attachment before resolving the Agent or appending Session events; queue edits accept only non-empty text content. Prompt admission consumes opaque receipts from the injected [`fileUploads`](../../client/file-upload/README.md) Host service and resolves every same-Agent receipt before sending the complete ordered content list through `ctx.attachments`. A receipt names a staged file or a staged image, and an image receipt subjects the prompt to the same image-input check as an encoded image. Prompt retries whose `requestId` is already queued or logged return the original acceptance without inserting another message. Create and fork are the only operations that create a new Agent directly. The service applies one preset-aware resume policy and subagent ownership fence to its own methods and to the Typert Agent and Session lookups used by other Remote namespaces. Queue mutation has one narrow exception: a live child whose current projected identity is continuable and comes from its own non-seed suffix accepts the ordinary Edit, Remove, and QueueDock Steer actions across both inbox destinations. One-shot, missing, unknown, corrupt, seed-only, or cold children remain rejected without resume. The skill catalog uses a live Agent's effective working directory and scoped registry when present. Cold listing uses the projected current directory, falling back to the recorded origin and then the configured directory default. It retains the preset standing scope without activating an Agent. The authenticated delivery routes use `workspaceDesktop()` for the serving Host name and file-manager behavior. `openWorkspacePath({ path, action: "reveal" })` delegates file-manager navigation to the native adapter; omitting `action` opens the file-type association, including HTML and SVG. Both operations require the composed filesystem to map the requested Host path to the same canonical process path; unmapped remote paths are refused before a native command runs. `session.projections` normally reads an exact baseline, including current-format cold logs, and returns `kind: sequenced`. Only a read requiring migration returns `kind: migration-required` hints without `asOfSeq`; missing keys remain absent. It returns null for a missing Session. Fork callers may set `allowMigration: false` to receive `session/migration-required` instead of starting source migration; omission retains automatic migration. The Client exposes shared values and explicit-read state through `projectionsBySession`; domains select their own keys. Session-list summaries carry `agentAvailable`, updated through existing summary and status events independently of durable projections. Sequenced reads and live projection frames use the same sequence ordering; cached reads fill hints without replacing sequenced values. +Each endpoint states its activation policy. List reads only stored headers and projection-cache rows: it never calls per-session stat or opens a cold Session body. A current-format cache identity may supply every list hint; a lifecycle-matching predecessor cache may supply only its version-compatible title as a stale display fact, never as an authoritative fold seed. Search, history pages, log following, skill discovery, and workspace-path opening can inspect persistence without activating an Agent; `canOpenWorkspacePath()` reports native-opening availability without addressing a Session. Cancellation requires live state; queue mutation, model, rename, prompt, and file-reference operations may resolve or resume an ordinary Session. Prompt rejects content with neither non-whitespace text nor an attachment before resolving the Agent or appending Session events; queue edits accept only non-empty text content. Prompt admission consumes opaque receipts from the injected [`fileUploads`](../../client/file-upload/README.md) Host service and resolves every same-Agent receipt before sending the complete ordered content list through `ctx.attachments`. A receipt names a staged file or a staged image, and an image receipt subjects the prompt to the same image-input check as an encoded image. Prompt retries whose `requestId` is already queued or logged return the original acceptance without inserting another message. Create and fork are the only operations that create a new Agent directly. The service applies one preset-aware resume policy and the subagent ownership fence determined by `origin: 'subagent'` to its own methods and to the Typert Agent and Session lookups used by other Remote namespaces. Queue mutation has one narrow exception: a live child whose current projected identity is continuable and comes from its own non-seed suffix accepts the ordinary Edit, Remove, and QueueDock Steer actions across both inbox destinations. One-shot, missing, unknown, corrupt, seed-only, or cold children remain rejected without resume. The skill catalog uses a live Agent's effective working directory and scoped registry when present. Cold listing uses the projected current directory, falling back to the recorded origin and then the configured directory default. It retains the preset standing scope without activating an Agent. The authenticated delivery routes use `workspaceDesktop()` for the serving Host name and file-manager behavior. `openWorkspacePath({ path, action: "reveal" })` delegates file-manager navigation to the native adapter; omitting `action` opens the file-type association, including HTML and SVG. Both operations require the composed filesystem to map the requested Host path to the same canonical process path; unmapped remote paths are refused before a native command runs. `session.projections` normally reads an exact baseline, including current-format cold logs, and returns `kind: sequenced`. Only a read requiring migration returns `kind: migration-required` hints without `asOfSeq`; missing keys remain absent. It returns null for a missing Session. Fork callers may set `allowMigration: false` to receive `session/migration-required` instead of starting source migration; omission retains automatic migration. The Client exposes shared values and explicit-read state through `projectionsBySession`; domains select their own keys. Session-list summaries carry `agentAvailable`, updated through existing summary and status events independently of durable projections. Sequenced reads and live projection frames use the same sequence ordering; cached reads fill hints without replacing sequenced values. Every Remote endpoint is access-controlled: a Peer whose principal is not the owner reaches an endpoint only when, on every resource the call touches, a binding of that principal kind's policy in [`dsh-api-remote-access`](../remote-access/README.md) grants a role listing the action ([Peer principals Agent Note](../../../.agents/notes/implemented/architecture/2026-10-12-peer-principal-and-remote-access.md)). The controller injects `remoteAccess` and registers a default resolver for the `session` resource kind through `typert.resources`. It reads a named `sessionId` parameter, otherwise the first object argument's `sessionId` or `address`; unrelated string parameters name no Session. An object containing both `sessionId` and `address` is invalid, even when they name the same Session. `session/events`, the deprecated `session/page`, `session/observeEvent`, and `session/follow` use this resolver to read `address.sessionId` for an ordinary Session address and `address.childSessionId` for a subagent address; whether access to the parent Session covers its children is the policy's rule. `session/projections`, `session/observeProjections`, `session/state`, `session/observeState`, and `session/wait` read the Session resource from `request.sessionId`, so each call and subscription names only the Sessions it may read. `session/list`, `session/search`, and `session/control` read no resource, so their calls touch the `global` resource and need a `global` role; for a non-owner caller, `session/list` returns only the rows, `session/search` only the hits, and `session/control` only the baseline projections and projection frames, whose Session `ctx.remoteAccess.allows()` admits for the same action, so that action also belongs to a `session` role; owner results are unfiltered. `session/prompt` builds the durable `user-rpc` message source in its Remote method and records `principal` only for a non-owner caller, read through `principalOf(this.ctx.invocation)`; a source without `principal` belongs to the owner. @@ -76,14 +76,14 @@ Queue edits replace pending content with non-empty text only. `session.attachment` receives `{ sessionId, attachment: ImageAttachmentRef }` and passes the complete reference to `AttachmentStore.readImage()`, without reading or traversing Session history. The Client supplies the message's reference through `session.readAttachment(ref)`. Connection authentication and Gateway permission checks still apply; `sessionId` selects the Session resource for the endpoint's permission check. After authorization, the handler does not check whether that Session exists and does not return `session/not-found` merely because it is absent. An authorized caller may read any image in the same store, including an image referenced only by another Session or by no Session. The response preserves the supplied reference after verifying its ID, media type, byte length, width, and height against the stored bytes. Optional `name` and `originalDimensions` pass through as caller-supplied display information and are not verified against the file. Missing or invalid objects return a terminal attachment failure. The Client's historical-image cache retains one result and browser URL per Session and attachment ID. -The controller composes `ArchivedSessionGate` through `ctx.plugin`: it loads once the Agent registry, Session store, and Workspace registry exist and unwinds with the controller. Its `agent/pre-step` listener rejects a step proposed for an archived Session or for a subagent descendant of one — read from the Session header's lineage fields, never a fork — so a late waking delivery ends its turn as `blocked` without a model request; unarchiving lifts the gate for the whole lineage. The work an archived Session still runs is reported and stopped by its owners through the Workspace registry's archive admission ([seam](../../workspace/workspace/README.md)): the running turn by the [Agent registry](../../core/agent/README.md), owned jobs by the [job registry seam](../../jobs/jobs/README.md), subagent descendants by the [Subagent](../../subagent/subagent/README.md) runtime, reminders by the [Schedule](../../schedule/schedule/README.md) plugin; this controller reports nothing itself. +The controller composes `ArchivedSessionGate` through `ctx.plugin`: it loads once the Agent registry, Session store, and Workspace registry exist and unwinds with the controller. Its `agent/pre-step` listener rejects a step proposed for an archived Session or for a subagent descendant of one — read through available Session headers, never a fork — so a late waking delivery ends its turn as `blocked` without a model request; unarchiving lifts the gate for the whole lineage. The Subagent runtime separately checks retained creation ancestors, covering managed descendants whose intermediate creators are offline. The work an archived Session still runs is reported and stopped by its owners through the Workspace registry's archive admission ([seam](../../workspace/workspace/README.md)): the running turn by the [Agent registry](../../core/agent/README.md), owned jobs by the [job registry seam](../../jobs/jobs/README.md), subagent descendants by the [Subagent](../../subagent/subagent/README.md) runtime, reminders by the [Schedule](../../schedule/schedule/README.md) plugin; this controller reports nothing itself. ## Client references `sessions.retain(target, { source, signal? })` immediately acquires one exact Client generation and starts its shared initial history opening. The target is a known Session id or a durable direct-parent subagent address; the Host validates an explicit address when history opens. The returned reference supports idempotent `release()` and `Symbol.dispose`; its `ready` Promise follows the shared `Session.open()` result and resolves to the exact binding when that attempt settles, including when a Remote failure is represented by `openState: 'error'`. It rejects when `Session.open()` rejects, its waiter is cancelled, or the reference is released early. Cancelling one waiter does not cancel another owner's opening. `sessions.using(target, options, operation)` waits for that settlement, holds its reference until the callback settles, and propagates rejected readiness and callback failures. -References keep local Session data, scoped Contexts, and history streams alive, not Host Agents. Ordinary Session retention never adds a catalog row. Retained subagents with a known direct-parent address keep a fallback row and notify list readers even before the parent catalog arrives; these rows do not join Host-list membership. Fork-title assignment sends rename directly and applies the returned title projection without retaining the child or opening its history. Final release withdraws the generation before teardown; later acquisition can create a new generation with the same id. `binding(id)` and `scope(id)` only borrow an existing generation. `retainInfo(id)` observes stable read-only source counts independently of catalog membership and performs no history I/O. Consumer source keys are declaration-merge extensible; navigation and completion acknowledgement belong to UI consumers, not this Controller. See [Client Session references](../../../.agents/notes/implemented/architecture/2026-09-15-client-session-references.md) for ownership and teardown rules. An unknown-mode subagent address permits history reads with direct-parent validation. Successful descriptor restoration determines the displayed mode; failures remain local to the child, and control requests still require a confirmed continuable identity. +References keep local Session data, scoped Contexts, and history streams alive, not Host Agents. Ordinary Session retention never adds a catalog row. Retained subagents with a known direct-parent address keep a fallback row and notify list readers even before the parent catalog arrives; these rows do not join Host-list membership. Fork-title assignment sends rename directly and applies the returned title projection without retaining the child or opening its history. Final release withdraws the generation before teardown; later acquisition can create a new generation with the same id. `binding(id)` and `scope(id)` only borrow an existing generation. `retainInfo(id)` observes stable read-only source counts independently of catalog membership and performs no history I/O. Consumer source keys are declaration-merge extensible; navigation and completion acknowledgement belong to UI consumers, not this Controller. See [Client Session references](../../../.agents/notes/implemented/architecture/2026-09-15-client-session-references.md) for ownership and teardown rules. An unknown-mode subagent address permits history reads with direct-parent consistency validation; access is checked separately on the child Session. Successful descriptor restoration determines the displayed mode; failures remain local to the child. Control requests carry only the target id, and the Host checks continuation availability. Client snapshots retain only the subagent address; runtime admission owns continuation availability. ## Session media references diff --git a/packages/api/session-controller/README.zh.md b/packages/api/session-controller/README.zh.md index 8729f555ba..0aecad0564 100644 --- a/packages/api/session-controller/README.zh.md +++ b/packages/api/session-controller/README.zh.md @@ -33,7 +33,7 @@ kind: "package-reference" Client journal 在发布 follow 快照、live entry 或历史页之前验证当前 Session 事件 envelope。它复用浏览器安全的 Session validator,检查必需的 surface marker、精确的 replacement endpoint、更早且唯一的 source seq、内嵌 Assistant 提供方元数据、request header 可选字段的省略规则以及工具错误一致性。无效 record 直接失败,不删除字段或归一化;范围成员与来源存在性仍由 Host 的持久日志检查。 -每个 endpoint 都声明自己的激活策略。列表只读取持久化 header 与 projection cache row,绝不调用逐 Session stat 或打开冷 Session body。当前格式 cache identity 可以提供全部列表 hint;生命周期匹配的 predecessor cache 只能提供版本兼容的 title,作为可能过时的展示事实,绝不能作为权威 fold seed。搜索、历史页、日志跟随、skill 发现和工作区路径打开可以在不激活 Agent 的情况下检查 persistence;`canOpenWorkspacePath()` 无需指定 Session 即可报告原生打开能力。取消要求 live 状态;queue 变更、模型、重命名、prompt 和文件引用操作可以解析或恢复普通 Session。提示词会在解析 Agent 或追加 Session 事件前,拒绝既没有非空白文本也没有附件的 content;queue edit 只接受非空文本 content。prompt 准入从注入的 [`fileUploads`](../../client/file-upload/README.zh.md) Host 服务取得不透明凭证,在把完整有序内容列表交给 `ctx.attachments` 前解析每个属于同一 Agent 的凭证。凭证指向一个暂存文件或一张暂存图片;图片凭证与编码图片一样要经过图片输入检查。`requestId` 已进入 queue 或日志时,prompt 重试直接返回原来的接受结果,不会重复插入消息。只有 create 与 fork 会直接创建新 Agent。该服务把同一套感知 preset 的恢复策略和 subagent ownership fence 同时用于自身方法,以及其他 Remote namespace 使用的 Typert Agent 与 Session lookup。Queue 变更只有一个狭窄例外:当前 projection identity 为 continuable 且来自自身非 seed suffix 的在线 child,可以在两个 inbox 目标上使用普通 Edit、Remove 与 QueueDock Steer action。One-shot、缺失、未知、损坏、仅含 seed identity 或冷 child 继续被拒绝,且不会恢复。skill 目录优先使用已有 live Agent 的有效工作目录与限定范围内的注册表。冷态列表使用投影中的当前目录,依次回退到记录的起始目录与配置的默认目录。它保留 preset 常驻 scope,不会激活 Agent。经过鉴权的文件交付路由通过 `workspaceDesktop()` 获取提供服务的 Host 名称和文件管理器行为。`openWorkspacePath({ path, action: "reveal" })` 将文件管理器导航委托给原生适配器;省略 `action` 时按文件类型关联打开,包括 HTML 和 SVG。两种操作都要求当前文件系统将请求的 Host 路径映射到同一个规范进程路径;无法映射的远端路径会在执行原生命令之前被拒绝。 `session.projections` 正常读取准确基线,包括当前格式的冷日志,返回 `kind: sequenced`。只有需要迁移时才返回不带 `asOfSeq` 的 `kind: migration-required` 提示,缺少的键保持缺席。Session 不存在时返回 null。Fork 调用者可设置 `allowMigration: false`,在源历史需要迁移时收到 `session/migration-required`;省略则保持自动迁移。Client 通过 `projectionsBySession` 暴露共享值和显式读取状态,由领域选择自身的 key。Session 列表摘要携带 `agentAvailable`,通过已有摘要与状态事件更新,与持久化 projection 相互独立。有序读取与实时 projection 帧使用相同的序号排序规则;缓存读取只补充提示,不替换有序值。 +每个 endpoint 都声明自己的激活策略。列表只读取持久化 header 与 projection cache row,绝不调用逐 Session stat 或打开冷 Session body。当前格式 cache identity 可以提供全部列表 hint;生命周期匹配的 predecessor cache 只能提供版本兼容的 title,作为可能过时的展示事实,绝不能作为权威 fold seed。搜索、历史页、日志跟随、skill 发现和工作区路径打开可以在不激活 Agent 的情况下检查 persistence;`canOpenWorkspacePath()` 无需指定 Session 即可报告原生打开能力。取消要求 live 状态;queue 变更、模型、重命名、prompt 和文件引用操作可以解析或恢复普通 Session。提示词会在解析 Agent 或追加 Session 事件前,拒绝既没有非空白文本也没有附件的 content;queue edit 只接受非空文本 content。prompt 准入从注入的 [`fileUploads`](../../client/file-upload/README.zh.md) Host 服务取得不透明凭证,在把完整有序内容列表交给 `ctx.attachments` 前解析每个属于同一 Agent 的凭证。凭证指向一个暂存文件或一张暂存图片;图片凭证与编码图片一样要经过图片输入检查。`requestId` 已进入 queue 或日志时,prompt 重试直接返回原来的接受结果,不会重复插入消息。只有 create 与 fork 会直接创建新 Agent。该服务把同一套感知 preset 的恢复策略和由 `origin: 'subagent'` 确定的 subagent ownership fence 同时用于自身方法,以及其他 Remote namespace 使用的 Typert Agent 与 Session lookup。Queue 变更只有一个狭窄例外:当前 projection identity 为 continuable 且来自自身非 seed suffix 的在线 child,可以在两个 inbox 目标上使用普通 Edit、Remove 与 QueueDock Steer action。One-shot、缺失、未知、损坏、仅含 seed identity 或冷 child 继续被拒绝,且不会恢复。skill 目录优先使用已有 live Agent 的有效工作目录与限定范围内的注册表。冷态列表使用投影中的当前目录,依次回退到记录的起始目录与配置的默认目录。它保留 preset 常驻 scope,不会激活 Agent。经过鉴权的文件交付路由通过 `workspaceDesktop()` 获取提供服务的 Host 名称和文件管理器行为。`openWorkspacePath({ path, action: "reveal" })` 将文件管理器导航委托给原生适配器;省略 `action` 时按文件类型关联打开,包括 HTML 和 SVG。两种操作都要求当前文件系统将请求的 Host 路径映射到同一个规范进程路径;无法映射的远端路径会在执行原生命令之前被拒绝。 `session.projections` 正常读取准确基线,包括当前格式的冷日志,返回 `kind: sequenced`。只有需要迁移时才返回不带 `asOfSeq` 的 `kind: migration-required` 提示,缺少的键保持缺席。Session 不存在时返回 null。Fork 调用者可设置 `allowMigration: false`,在源历史需要迁移时收到 `session/migration-required`;省略则保持自动迁移。Client 通过 `projectionsBySession` 暴露共享值和显式读取状态,由领域选择自身的 key。Session 列表摘要携带 `agentAvailable`,通过已有摘要与状态事件更新,与持久化 projection 相互独立。有序读取与实时 projection 帧使用相同的序号排序规则;缓存读取只补充提示,不替换有序值。 每个 Remote 端点都受访问控制:principal 不是 owner 的 Peer 只有在本次调用访问的每个资源上,该 principal kind 在 [`dsh-api-remote-access`](../remote-access/README.zh.md) 中的 policy 都有某个 binding 授予列出该 action 的 role 时,才能调用该端点([Peer 身份属性 Agent Note](../../../.agents/notes/implemented/architecture/2026-10-12-peer-principal-and-remote-access.zh.md))。控制器注入 `remoteAccess`,并经 `typert.resources` 为 `session` 资源种类登记默认 resolver。它读取具名 `sessionId` 参数,否则读取第一个对象参数的 `sessionId` 或 `address`;无关字符串参数不指明 Session。对象同时包含 `sessionId` 和 `address` 时无效,即使两者指向同一 Session。`session/events`、已弃用的 `session/page`、`session/observeEvent` 与 `session/follow` 使用该 resolver:对普通 Session 地址读 `address.sessionId`,对 subagent 地址读 `address.childSessionId`;父会话的访问权是否覆盖其子会话由 policy 决定。`session/projections`、`session/observeProjections`、`session/state`、`session/observeState` 与 `session/wait` 从 `request.sessionId` 读取会话资源,因此每次调用与订阅都只点名它可读的会话。`session/list`、`session/search` 与 `session/control` 不读取资源,其调用访问 `global` 资源,需要 `global` role;对非 owner 调用方,`session/list` 只返回其 Session 被 `ctx.remoteAccess.allows()` 按同一 action 允许的行,`session/search` 只返回这类 Session 的命中,`session/control` 也只返回这类 Session 的 baseline projection 与 projection 帧,因此该 action 也要列在 `session` role 中;owner 的结果不过滤。`session/prompt` 在其 Remote 方法内构造持久的 `user-rpc` 消息 source,且只为非 owner 调用方记录经 `principalOf(this.ctx.invocation)` 读取的 `principal`;不带 `principal` 的 source 属于 owner。 @@ -76,14 +76,14 @@ Client 的首次 `follow`、重连首屏与 `loadOlder()` 至少请求 50 条以 `session.attachment` 接收 `{ sessionId, attachment: ImageAttachmentRef }`,并把完整引用交给 `AttachmentStore.readImage()`,不读取或遍历 Session 历史。Client 通过 `session.readAttachment(ref)` 传入消息中的引用。连接身份认证和 Gateway 权限检查仍然适用;`sessionId` 为该接口的权限检查指定 Session 资源。授权通过后,处理函数不检查该 Session 是否存在,也不会仅因其不存在而返回 `session/not-found`。获准调用的身份可以读取同一存储中的任意图片,包括仅被其他 Session 引用或未被任何 Session 引用的图片。响应在把 ID、媒体类型、字节数、宽度和高度与存储字节校验后,保留传入的引用。可选的 `name` 和 `originalDimensions` 作为调用方提交的显示信息原样返回,不根据文件校验。对象缺失或无效时返回明确的附件失败。Client 的历史图片缓存按 Session 和附件 ID 保留一份结果与浏览器 URL。 -本控制器通过 `ctx.plugin` 组合 `ArchivedSessionGate`:在 Agent 注册表、Session store 与 Workspace 注册表就绪后加载,随控制器一起释放。它的 `agent/pre-step` 监听器会拒绝为已归档会话或其子代理子孙——从 Session header 的血缘字段读出,从不包括 fork——提出的步骤,因此迟到的唤醒投递会让该回合以 `blocked` 收口而不发出模型请求;取消归档即为整条血缘解除门禁。已归档会话仍在跑的工作由各自的 owner 通过 Workspace 注册表的归档准入([接缝](../../workspace/workspace/README.zh.md))报告与停止:运行中的回合由 [Agent 注册表](../../core/agent/README.zh.md)负责,所属任务由[任务注册表接缝](../../jobs/jobs/README.zh.md)负责,子代理子孙由 [Subagent](../../subagent/subagent/README.zh.md) runtime 负责,提醒由 [Schedule](../../schedule/schedule/README.zh.md) 插件负责;本控制器自己不报告任何内容。 +本控制器通过 `ctx.plugin` 组合 `ArchivedSessionGate`:在 Agent 注册表、Session store 与 Workspace 注册表就绪后加载,随控制器一起释放。它的 `agent/pre-step` 监听器会拒绝为已归档会话或其子代理子孙——沿可用的 Session header 读取,从不包括 fork——提出的步骤,因此迟到的唤醒投递会让该回合以 `blocked` 收口而不发出模型请求;取消归档即为整条血缘解除门禁。Subagent runtime 另行检查保留的创建祖先,覆盖中间创建者已离线的托管后代。已归档会话仍在跑的工作由各自的 owner 通过 Workspace 注册表的归档准入([接缝](../../workspace/workspace/README.zh.md))报告与停止:运行中的回合由 [Agent 注册表](../../core/agent/README.zh.md)负责,所属任务由[任务注册表接缝](../../jobs/jobs/README.zh.md)负责,子代理子孙由 [Subagent](../../subagent/subagent/README.zh.md) runtime 负责,提醒由 [Schedule](../../schedule/schedule/README.zh.md) 插件负责;本控制器自己不报告任何内容。 ## Client 引用 `sessions.retain(target, { source, signal? })` 立即获取一个精确 Client generation 的引用,并启动其共享的首次历史打开。目标是已知 Session id 或持久的直接父子 subagent 地址;Host 在打开历史时校验显式地址。返回引用支持幂等的 `release()` 和 `Symbol.dispose`;其 `ready` Promise 跟随共享的 `Session.open()` 结果,并在该次尝试结算时解析为确切 binding,包括 Remote failure 以 `openState: 'error'` 表示的情况。仅当 `Session.open()` 拒绝、等待方取消或引用提前释放时,`ready` 才拒绝。取消一个等待方不会取消其他 owner 的打开。`sessions.using(target, options, operation)` 等待该次结算,持有引用直到回调结束,并传播被拒绝的就绪与回调失败。 -引用保活本地会话数据、作用域 Context 和历史流,不保活 Host Agent。普通 Session 不因保留引用而添加目录行。已保留且具有明确直接父子地址的 subagent 即使尚未收到父目录,也会保留兜底行并通知列表读取方;这些行不加入 Host 列表成员集合。Fork 标题设置直接发送 rename 并应用返回的标题投影,不 retain 子会话,也不打开其历史。最后一个引用释放时,generation 先退出可访问映射,再执行清理;后续获取可以为同一 id 创建新 generation。`binding(id)` 和 `scope(id)` 只借用已有 generation。`retainInfo(id)` 独立于目录成员关系观察稳定的只读来源计数,不执行历史 I/O。消费方来源键可通过声明合并扩展;导航和完成确认属于 UI 消费方,不属于本控制器。所有权与清理规则见 [Client 会话引用](../../../.agents/notes/implemented/architecture/2026-09-15-client-session-references.zh.md)。 模式未知的子代理地址允许读取历史,但仍校验直接父级。成功恢复 descriptor 后确定展示模式;失败只影响该子会话,控制请求仍要求已确认的 continuable 身份。 +引用保活本地会话数据、作用域 Context 和历史流,不保活 Host Agent。普通 Session 不因保留引用而添加目录行。已保留且具有明确直接父子地址的 subagent 即使尚未收到父目录,也会保留兜底行并通知列表读取方;这些行不加入 Host 列表成员集合。Fork 标题设置直接发送 rename 并应用返回的标题投影,不 retain 子会话,也不打开其历史。最后一个引用释放时,generation 先退出可访问映射,再执行清理;后续获取可以为同一 id 创建新 generation。`binding(id)` 和 `scope(id)` 只借用已有 generation。`retainInfo(id)` 独立于目录成员关系观察稳定的只读来源计数,不执行历史 I/O。消费方来源键可通过声明合并扩展;导航和完成确认属于 UI 消费方,不属于本控制器。所有权与清理规则见 [Client 会话引用](../../../.agents/notes/implemented/architecture/2026-09-15-client-session-references.zh.md)。 模式未知的子代理地址允许读取历史,但仍校验直接父级的一致性;访问权单独按子 Session 检查。成功恢复 descriptor 后确定展示模式;失败只影响该子会话。控制请求仅携带目标 id,由 Host 检查是否支持继续执行。Client 快照仅保留子代理地址;是否能接收续接输入由运行时校验。 ## 会话媒体引用 diff --git a/packages/api/session-controller/src/agent.ts b/packages/api/session-controller/src/agent.ts index d16e041773..343ad9ba5f 100644 --- a/packages/api/session-controller/src/agent.ts +++ b/packages/api/session-controller/src/agent.ts @@ -92,21 +92,11 @@ export type ApiSessionAgentResult = /** * Test whether generic Session routing must leave an identity to subagent routing. - * @param ctx - Host context carrying the Agent ownership registry. - * @param session - attached or live Session whose ownership is tested. - * @param agent - live Agent when one exists for the Session. + * @param session - Attached or live Session whose origin is tested. * @returns whether subagent routing owns the Session identity. */ -export function hasApiSessionSubagentOwner( - ctx: Context, - session: Pick, - agent: Agent | undefined, -): boolean { - if (session.header.origin === 'subagent') return true - const parentId = session.header.parentSession - if (parentId === undefined || agent === undefined) return false - const parent = ctx.agents.get(parentId) - return parent !== undefined && ctx.agents.isOwnedBy(agent.id, parent) +export function hasApiSessionSubagentOwner(session: Pick): boolean { + return session.header.origin === 'subagent' } /** @@ -178,7 +168,7 @@ export class ApiSessionAgentController { const live = this.liveAgent(sessionId) if (live !== undefined) return live const attached = this.ctx.sessions.get(sessionId) - if (attached !== undefined && hasApiSessionSubagentOwner(this.ctx, attached, undefined)) { + if (attached !== undefined && hasApiSessionSubagentOwner(attached)) { return { error: apiSessionSubagentOwnershipError(sessionId) } } @@ -203,7 +193,7 @@ export class ApiSessionAgentController { const raced = this.liveAgent(sessionId) if (raced !== undefined) return raced const racedSession = this.ctx.sessions.get(sessionId) - if (racedSession !== undefined && hasApiSessionSubagentOwner(this.ctx, racedSession, undefined)) { + if (racedSession !== undefined && hasApiSessionSubagentOwner(racedSession)) { return { error: apiSessionSubagentOwnershipError(sessionId) } } if (error instanceof Error && error.name === 'SessionAlreadyOwnedError') { @@ -286,7 +276,7 @@ export class ApiSessionAgentController { private liveAgent(sessionId: SessionId): ApiSessionAgentResult | undefined { const agent = this.ctx.agents.get(sessionId) if (agent === undefined) return undefined - return hasApiSessionSubagentOwner(this.ctx, agent.session, agent) + return hasApiSessionSubagentOwner(agent.session) ? { error: apiSessionSubagentOwnershipError(sessionId) } : { agent } } @@ -312,13 +302,12 @@ export class ApiSessionAgentController { if (observation.header.id !== sessionId || observation.header.cwd === undefined) { throw new ApiSessionNotFound(`session "${sessionId}" not found`) } - if (hasApiSessionSubagentOwner(this.ctx, { header: observation.header }, undefined)) { + if (hasApiSessionSubagentOwner({ header: observation.header })) { throw new ApiSessionSubagentOwnership(sessionId) } const composition = await this.composeAgent(this.presetForObservation(observation)) const published = this.ctx.sessions.get(sessionId) - const live = this.ctx.agents.get(sessionId) - if (published !== undefined && hasApiSessionSubagentOwner(this.ctx, published, live)) { + if (published !== undefined && hasApiSessionSubagentOwner(published)) { throw new ApiSessionSubagentOwnership(sessionId) } return (await this.ctx.agents.resume({ @@ -343,7 +332,7 @@ export class ApiSessionAgentController { private async claim(sessionId: SessionId, request: ApiSessionCreateRequest): Promise { const attached = this.ctx.sessions.get(sessionId) const live = this.ctx.agents.get(sessionId) - if (attached !== undefined && hasApiSessionSubagentOwner(this.ctx, attached, live)) { + if (attached !== undefined && hasApiSessionSubagentOwner(attached)) { throw new ApiSessionSubagentOwnership(sessionId) } if (live !== undefined) { @@ -363,7 +352,7 @@ export class ApiSessionAgentController { let reusable: boolean { using observed = observation - if (hasApiSessionSubagentOwner(this.ctx, { header: observed.header }, undefined)) { + if (hasApiSessionSubagentOwner({ header: observed.header })) { throw new ApiSessionSubagentOwnership(sessionId) } if (observed.header.cwd !== request.cwd) { diff --git a/packages/api/session-controller/src/archived-session-gate.ts b/packages/api/session-controller/src/archived-session-gate.ts index 06c6ee5f02..b1f556b397 100644 --- a/packages/api/session-controller/src/archived-session-gate.ts +++ b/packages/api/session-controller/src/archived-session-gate.ts @@ -4,7 +4,9 @@ * it is restored. The work a Session still runs is reported and stopped by * its owners — the Agent registry (`turn`), the job registry seam (`job`), * the Subagent runtime (`subagent`), and the Schedule plugin (`schedule`) — - * through the Workspace registry's archive-admission events. + * through the Workspace registry's archive-admission events. The Subagent + * runtime also checks its retained ancestry before each step, so a released + * intermediate Session cannot hide an archived ancestor. */ import type { Context, Plugin } from '@deepseek-ai/cordis' @@ -33,9 +35,10 @@ export const ArchivedSessionGate: Plugin.Object = { /** * Whether the Agent's Session, or a Session above it in its subagent lineage, - * is archived. Lineage follows the durable header fields through + * is archived. This header-only check follows the durable fields through * subagent-origin Sessions only: a fork of an archived Session is an - * independent conversation. + * independent conversation. The Subagent runtime separately checks managed + * ancestry when an intermediate Session is unavailable here. * @param ctx - Host context. * @param agent - the Agent proposing a step. * @returns whether an archived Session owns the step. diff --git a/packages/api/session-controller/src/client/contract/snapshot.ts b/packages/api/session-controller/src/client/contract/snapshot.ts index 78e0c8c8a3..4912d19b16 100644 --- a/packages/api/session-controller/src/client/contract/snapshot.ts +++ b/packages/api/session-controller/src/client/contract/snapshot.ts @@ -73,8 +73,6 @@ export interface SessionSnapshot { readonly running: boolean readonly subagent: { readonly address: SubagentAddress - /** Absent until the direct-parent catalog resolves. */ - readonly parentAvailable?: boolean } | null readonly removed: boolean readonly openState: OpenState diff --git a/packages/api/session-controller/src/client/sessions/manager.ts b/packages/api/session-controller/src/client/sessions/manager.ts index ecb6a0e127..e7450ca973 100644 --- a/packages/api/session-controller/src/client/sessions/manager.ts +++ b/packages/api/session-controller/src/client/sessions/manager.ts @@ -147,12 +147,7 @@ export class SessionManager { throw new Error(`sessions.retain: unknown session ${id}`) } if (address !== undefined) this.addresses.set(id, address) - this.sessions.get(id)?.configureSubagent( - address, - address === undefined - ? undefined - : this.agentAvailable(address.parentSessionId), - ) + this.sessions.get(id)?.configureSubagent(address) return id } @@ -276,14 +271,8 @@ export class SessionManager { private createSession(sessionId: SessionId): Session { const address = this.addresses.get(sessionId) - const parentAvailable = address === undefined - ? undefined - : this.agentAvailable(address.parentSessionId) return new Session(sessionId, this.remote, { - ...(address === undefined ? {} : { - address, - ...parentAvailable === undefined ? {} : { parentAvailable }, - }), + ...(address === undefined ? {} : { address }), // The sender's local first-send flip mirrors into the list row so the // session surfaces (lists filter on blank) before any host frame lands. onEngaged: (engaged) => { @@ -400,18 +389,6 @@ export class SessionManager { return operation } - private agentAvailable(sessionId: SessionId): boolean | undefined { - return this.summaries.find(summary => summary.sessionId === sessionId)?.agentAvailable - ?? (this.listPhase === 'ready' ? false : undefined) - } - - private updateParentAvailability(): void { - for (const [childId, address] of this.addresses) { - const available = this.agentAvailable(address.parentSessionId) - if (available !== undefined) this.sessions.get(childId)?.handleSubagentParentAvailable(available) - } - } - // ---- List API ---- /** Full refresh via session.list (single-flight within one Host generation). */ @@ -446,7 +423,7 @@ export class SessionManager { for (const sessionId of this.engagedSessions) this.pruneEngagement(sessionId, retained) this.listState = 'idle' this.listPhase = 'ready' - this.updateParentAvailability() + // Resident Sessions and list rows share one display blank, reconciled with the metadata projection. for (const s of this.summaries) { const session = this.sessions.get(s.sessionId) @@ -599,7 +576,6 @@ export class SessionManager { */ private mergeSummary(summary: SessionSummary): void { this.recordMutation({ kind: 'upsert', summary }) - this.updateParentAvailability() } /** Apply immediately and retain for replay when a list response is in flight. */ @@ -721,11 +697,6 @@ export class SessionManager { this.projectionInflight.get(sessionId)?.controller.abort() this.projectionInflight.delete(sessionId) this.projectionLoads.delete(sessionId) - for (const [childId, address] of this.addresses) { - if (address.parentSessionId === sessionId) { - this.sessions.get(childId)?.handleSubagentParentAvailable(false) - } - } } /** @@ -736,7 +707,6 @@ export class SessionManager { handleSessionStatus(sessionId: SessionId, running: boolean): void { if (!this.disposed && running) this.engagedSessions.add(sessionId) this.recordMutation({ kind: 'status', sessionId, running, agentAvailable: true }) - this.updateParentAvailability() this.sessions.get(sessionId)?.handleRunning(running) } diff --git a/packages/api/session-controller/src/client/sessions/remotes.ts b/packages/api/session-controller/src/client/sessions/remotes.ts index 8e084384d4..bbe026bf4e 100644 --- a/packages/api/session-controller/src/client/sessions/remotes.ts +++ b/packages/api/session-controller/src/client/sessions/remotes.ts @@ -30,10 +30,8 @@ export interface SessionSubagentsRemote { request: SubagentPromptRequest, signal?: AbortSignal, ): Promise> - interruptByParent( + interruptByUser( childSessionId: SessionId, - parentSessionId: SessionId, - mode: 'continuable', ): Promise> } diff --git a/packages/api/session-controller/src/client/sessions/session.ts b/packages/api/session-controller/src/client/sessions/session.ts index 253da9c463..e74dd84422 100644 --- a/packages/api/session-controller/src/client/sessions/session.ts +++ b/packages/api/session-controller/src/client/sessions/session.ts @@ -71,8 +71,6 @@ interface PendingHistory { export interface SessionOptions { /** Catalog-discovered address selecting non-activating subagent transport. */ address?: SubagentAddress - /** Whether the exact direct parent Agent is available in Host summaries; absent until known. */ - parentAvailable?: boolean /** * Publish each accepted prompt to the Manager, including after this Session * object is replaced. Acceptance converts display state, but does not @@ -113,7 +111,6 @@ export class Session implements SessionFace { private readonly assistantStream = new ClientAssistantStream() private running = false private address: SubagentAddress | undefined - private parentAvailable: boolean | undefined /** * Sticky send marker, private input of the composerPhase derivation: set * when the Host accepts a prompt, never reset — the blank → engaging edge of @@ -193,7 +190,6 @@ export class Session implements SessionFace { ) { this.projections = options.projections ?? new ProjectionValueStore() this.address = options.address - this.parentAvailable = options.parentAvailable this.notifier = new Notifier(() => { this.snapshotCache = this.buildSnapshot() }) @@ -294,9 +290,7 @@ export class Session implements SessionFace { const routedContent = content as Exclude[] const routed = await this.remote.subagents.prompt({ requestId: requestId ?? randomUUID() as SessionRequestId, - parentSessionId: this.address.parentSessionId, childSessionId: this.address.childSessionId, - mode: 'continuable', delivery: mode, content: routedContent, clientTimeZone: resolvedClientTimeZone(), @@ -363,18 +357,14 @@ export class Session implements SessionFace { /** * Stop the active turn while the Host preserves pending inbox work; failures * land in promptError (same error-strip display slot). A subagent address - * routes through `subagents.interruptByParent`, whose durable parent-address - * authority works without a live parent Agent. + * routes through `subagents.interruptByUser`, authorized on the target Session + * without requiring a live parent Agent. * @returns the cancel result. */ async cancel(): Promise> { const address = this.address const result = address !== undefined - ? await this.remote.subagents.interruptByParent( - address.childSessionId, - address.parentSessionId, - 'continuable', - ) + ? await this.remote.subagents.interruptByUser(address.childSessionId) : await this.remote.session.cancel({ sessionId: this.sessionId }) if (!result.ok) { this.promptError = { op: 'stop', error: result.error } @@ -562,28 +552,16 @@ export class Session implements SessionFace { * Install or clear the catalog-discovered transport address. A changed * address rebuilds an already-open window through its new history route. * @param address - direct parent/child address, or undefined for ordinary transport. - * @param parentAvailable - latest exact-parent availability hint, or undefined before a catalog read. */ - configureSubagent(address: SubagentAddress | undefined, parentAvailable?: boolean): void { + configureSubagent(address: SubagentAddress | undefined): void { const same = this.address?.parentSessionId === address?.parentSessionId && this.address?.childSessionId === address?.childSessionId && this.address?.mode === address?.mode this.address = address - this.parentAvailable = parentAvailable if (!same && this.openState !== 'cold') void this.resync() else this.notifier.markDirty() } - /** - * Update only the parent availability hint from a catalog refresh. - * @param available - whether the exact direct parent is live. - */ - handleSubagentParentAvailable(available: boolean): void { - if (this.parentAvailable === available) return - this.parentAvailable = available - this.notifier.markDirty() - } - /** * Apply the Manager's effective display blank, further reconciled with the * current `sessionListMetadata` projection. Local send attempts and current @@ -934,7 +912,6 @@ export class Session implements SessionFace { address: this.address.mode === 'unknown' && identity != null ? { ...this.address, mode: identity.mode } : this.address, - ...(this.parentAvailable === undefined ? {} : { parentAvailable: this.parentAvailable }), }, removed: this.removed, openState: this.openState, diff --git a/packages/api/session-controller/src/commands.ts b/packages/api/session-controller/src/commands.ts index 5309872331..d306953c8c 100644 --- a/packages/api/session-controller/src/commands.ts +++ b/packages/api/session-controller/src/commands.ts @@ -409,7 +409,7 @@ export class SessionCommandController { } agent = found.agent } - if (hasApiSessionSubagentOwner(this.ctx, agent.session, agent)) { + if (hasApiSessionSubagentOwner(agent.session)) { const identity = this.ctx.sessionProjections .snapshot(agent.session, ['subagent']) .values.subagent @@ -470,7 +470,7 @@ export class SessionCommandController { { sessionId: request.sessionId }, ) } - if (hasApiSessionSubagentOwner(this.ctx, agent.session, agent)) { + if (hasApiSessionSubagentOwner(agent.session)) { throw apiSessionSubagentOwnershipError(request.sessionId) } agent.cancel({ kind: 'user' }, { keepInbox: true }) diff --git a/packages/api/session-controller/tests/list-scheduling.host.spec.ts b/packages/api/session-controller/tests/list-scheduling.host.spec.ts index d589dfd5ca..0cba6939d7 100644 --- a/packages/api/session-controller/tests/list-scheduling.host.spec.ts +++ b/packages/api/session-controller/tests/list-scheduling.host.spec.ts @@ -70,7 +70,7 @@ async function harness(rows: readonly Row[], summaryMs = 8, workSliceMs = 8) { ctx.sessions.announce(session) const agent = { id: session.id, session, ctx, status: 'idle' as Agent['status'] } // Only registry lookup and status are consumed; no Agent execution occurs in this fixture. - const detachAgent = ctx.agents.enter(agent as Agent, undefined) + const detachAgent = ctx.agents.enter(agent as Agent) ctx.effect(() => detachAgent, 'list-scheduling.agent') agents.set(id, agent) detach.set(id, () => { detachAgent(); detachSession() }) diff --git a/packages/api/session-controller/tests/manager.client.spec.ts b/packages/api/session-controller/tests/manager.client.spec.ts index 1946dfdeca..4e64470548 100644 --- a/packages/api/session-controller/tests/manager.client.spec.ts +++ b/packages/api/session-controller/tests/manager.client.spec.ts @@ -862,7 +862,6 @@ describe('subagent catalogs', () => { address: { parentSessionId: S1, childSessionId: S2, mode: 'continuable', }, - parentAvailable: true, }) // Clicking the same child through an ordinary list-selection path must not // erase the catalog-derived address and fall back to session.* transport. @@ -874,7 +873,6 @@ describe('subagent catalogs', () => { address: { parentSessionId: S1, childSessionId: S2, mode: 'continuable', }, - parentAvailable: true, }) await manager.get(S2).open() await manager.get(S2).prompt([{ type: 'text', text: 'continue' }], 'queue') @@ -895,8 +893,7 @@ describe('subagent catalogs', () => { expect(remote.subagents.prompt.mock.calls.map(([request]) => request)).toEqual([ { requestId: expect.any(String) as unknown as string, - parentSessionId: S1, childSessionId: S2, - mode: 'continuable', + childSessionId: S2, delivery: 'queue', content: [{ type: 'text', text: 'continue' }], clientTimeZone: new Intl.DateTimeFormat().resolvedOptions().timeZone, @@ -1061,36 +1058,6 @@ describe('subagent catalogs', () => { expect(remote.session.projections).toHaveBeenCalledTimes(2) }) - it.for([false, true])('keeps live parent availability when an earlier initial read settles (failure: %s)', async (failure, { mock, remote }) => { - const response = Promise.withResolvers>>() - remote.session.projections.mockImplementation(() => response.promise) - const manager = makeManager(mock, remote) - manager.resolveTarget({ parentSessionId: S1, childSessionId: S2, mode: 'continuable' }) - const child = manager.get(S2) - const read = manager.refreshProjections(S1) - manager.handleSessionAdded(summary(S1)) - manager.handleSessionStatus(S1, false) - response.resolve(failure - ? err(new RemoteError('gateway/internal', 'offline', {})) - : ok({ kind: 'sequenced', asOfSeq: 0, values: { subagentCatalog: [] } })) - await read - expect(child.getSnapshot().subagent?.parentAvailable).toBe(true) - expect(remote.session.projections.mock.calls.map(([request]) => request)).toHaveLength(1) - }) - - it('keeps parent availability unknown after list failure and accepts a later Host summary', async ({ mock, remote }) => { - const manager = makeManager(mock, remote) - manager.resolveTarget({ parentSessionId: S1, childSessionId: S2, mode: 'continuable' }) - const child = manager.get(S2) - remote.session.list.mockResolvedValue(err(new RemoteError('gateway/internal', 'offline', {}))) - await manager.refreshList() - expect(manager.getListSnapshot().state).toBe('error') - expect(child.getSnapshot().subagent?.parentAvailable).toBeUndefined() - manager.handleSessionAdded(summary(S1)) - expect(child.getSnapshot().subagent?.parentAvailable).toBe(true) - await manager.dispose() - }) - it('keeps a pushed catalog when an earlier initial read reports the parent missing', async ({ mock, remote }) => { const response = Promise.withResolvers>>() remote.session.projections.mockImplementation(() => response.promise) @@ -1260,23 +1227,6 @@ describe('remaining branches', () => { await manager.dispose() }) - it('preserves Host availability when a local create echo follows Agent disposal', async ({ mock, remote }) => { - const response = Promise.withResolvers>>() - remote.session.create.mockImplementation(() => response.promise) - const manager = makeManager(mock, remote) - manager.resolveTarget({ - parentSessionId: S1, childSessionId: S2, mode: 'continuable', - }) - const child = manager.get(S2) - const request = manager.create() - manager.handleSessionAdded(summary(S1, { agentAvailable: false })) - response.resolve(ok({ sessionId: S1 })) - await request - manager.resolveTarget(S2) - expect(child.getSnapshot().subagent?.parentAvailable).toBe(false) - await manager.dispose() - }) - it('publishes a real Ungrouped summary from workspace-attach-failed', async ({ mock, remote }) => { remote.session.create.mockResolvedValue(err(new RemoteError('session/workspace-attach-failed', 'published but unattached', { sessionId: S1, workspaceId: 'w1', @@ -1631,7 +1581,6 @@ describe('connected generation', () => { }) expect(manager.get(S2).getSnapshot().subagent).toEqual({ address, - parentAvailable: true, }) expect(manager.subagentAddress(S2)).toEqual(address) }) diff --git a/packages/api/session-controller/tests/remote/bench.client.ts b/packages/api/session-controller/tests/remote/bench.client.ts index 6e0f10d569..a696fd0c28 100644 --- a/packages/api/session-controller/tests/remote/bench.client.ts +++ b/packages/api/session-controller/tests/remote/bench.client.ts @@ -11,7 +11,7 @@ import { sessionWorld } from './session.client.ts' * @param mock - this case's Remote responses. * @param start - the fixture's lazy client startup. * @param sessionId - Session identity. - * @param options - addressed child and parent availability, when applicable. + * @param options - addressed child and other Session options, when applicable. * @returns the Session, disposed at the end of the case. */ export async function sessionBench( diff --git a/packages/api/session-controller/tests/remote/session.client.ts b/packages/api/session-controller/tests/remote/session.client.ts index f920d1592e..c4d8049177 100644 --- a/packages/api/session-controller/tests/remote/session.client.ts +++ b/packages/api/session-controller/tests/remote/session.client.ts @@ -147,7 +147,7 @@ export const sessionWorld: RemoteTable = { 'session/page': pageRule(ok({ cursor: -1, records: [], hasMore: false })), 'session/projections': ok({ kind: 'sequenced', asOfSeq: -1, values: { subagentCatalog: [] } }), 'subagents/prompt': ok({ messageId: 'fake-message' }), - 'subagents/interruptByParent': ok({ accepted: true }), + 'subagents/interruptByUser': ok({ accepted: true }), }, stream: { 'session/follow': followScript(ok({ cursor: -1, records: [], hasMore: false })), diff --git a/packages/api/session-controller/tests/session-cold.host.spec.ts b/packages/api/session-controller/tests/session-cold.host.spec.ts index 560c05514f..e5f721d37d 100644 --- a/packages/api/session-controller/tests/session-cold.host.spec.ts +++ b/packages/api/session-controller/tests/session-cold.host.spec.ts @@ -672,10 +672,10 @@ describe('subagent ownership fence', () => { await ctx.agents.register(originChild) const startingSession = ctx.sessions.create(sid('session-starting-child'), { - meta: { cwd: '/proj', parentSession: parent.id }, + meta: { cwd: '/proj', parentSession: parent.id, origin: 'subagent' }, }) const startingChild = { id: startingSession.id, session: startingSession, status: 'idle', ctx } as Agent - ctx.agents.enter(startingChild, parent) + ctx.agents.enter(startingChild) const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) const stopped = await remote.cancel(request({ sessionId: originChild.id })) diff --git a/packages/api/session-controller/tests/session-remote-rejection.client.spec.ts b/packages/api/session-controller/tests/session-remote-rejection.client.spec.ts index a6e7a274aa..2fb870f910 100644 --- a/packages/api/session-controller/tests/session-remote-rejection.client.spec.ts +++ b/packages/api/session-controller/tests/session-remote-rejection.client.spec.ts @@ -7,21 +7,28 @@ */ import { describe, expect, it } from 'vitest' import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client' +import { RemoteMock } from '@deepseek-ai/dsh-remote-mock' import { Session } from '../src/client/sessions/session.ts' import type { SessionRemotes } from '../src/client/sessions/remotes.ts' const SID = 'fk-s1' as SessionId const PARENT = 'fk-parent' as SessionId -/** Every command rejects with `message`; nothing else on the face is reachable from prompt or cancel. */ +/** Calls used by Session prompt and cancel reject with `message`. */ function rejecting(message: string): SessionRemotes { const reject = (): Promise => Promise.reject(new Error(message)) + const { remote } = RemoteMock.create() + remote.commands.execute.mockImplementation(reject) + remote.session.prompt.mockImplementation(reject) + remote.session.cancel.mockImplementation(reject) + remote.subagents.prompt.mockImplementation(reject) + remote.subagents.interruptByUser.mockImplementation(reject) return { $stream: () => { throw new Error('unused') }, - commands: { execute: reject }, - session: { prompt: reject, cancel: reject }, - subagents: { list: reject, prompt: reject, interruptByParent: reject }, - } as unknown as SessionRemotes + commands: remote.commands, + session: remote.session, + subagents: remote.subagents, + } } describe('Session over a rejecting Remote face', () => { @@ -35,7 +42,6 @@ describe('Session over a rejecting Remote face', () => { it('propagates the rejection from a subagent continuation prompt and interrupt', async () => { const session = new Session(SID, rejecting('assembly fault'), { address: { parentSessionId: PARENT, childSessionId: SID, mode: 'continuable' }, - parentAvailable: true, }) await expect(session.prompt([{ type: 'text', text: 'x' }], 'queue')).rejects.toThrow('assembly fault') await expect(session.cancel()).rejects.toThrow('assembly fault') diff --git a/packages/api/session-controller/tests/session.client.spec.ts b/packages/api/session-controller/tests/session.client.spec.ts index 70c69ca033..e118ef29e5 100644 --- a/packages/api/session-controller/tests/session.client.spec.ts +++ b/packages/api/session-controller/tests/session.client.spec.ts @@ -474,7 +474,7 @@ describe('prompt and cancel errors', () => { const CHILD = { parentSessionId: PARENT, childSessionId: SID, mode: 'continuable' } as const it('routes an addressed child through non-activating history, continuation prompt, and interrupt only', async ({ mock, start }) => { - const session = await sessionBench(mock, start, SID, { address: CHILD, parentAvailable: true }) + const session = await sessionBench(mock, start, SID, { address: CHILD }) await session.open() const prompted = await session.prompt([{ type: 'text', text: '继续' }], 'queue') const steered = await session.prompt([{ type: 'text', text: '现在处理' }], 'steer') @@ -491,34 +491,34 @@ describe('prompt and cancel errors', () => { expect(mock.log.requests('subagents/prompt')).toEqual([ { requestId: expect.any(String) as unknown as string, - ...CHILD, + childSessionId: SID, delivery: 'queue', content: [{ type: 'text', text: '继续' }], clientTimeZone: TIME_ZONE, }, { requestId: expect.any(String) as unknown as string, - ...CHILD, + childSessionId: SID, delivery: 'steer', content: [{ type: 'text', text: '现在处理' }], clientTimeZone: TIME_ZONE, }, ]) // Positional parameters cross the wire as positional args. - expect(mock.log.calls('subagents/interruptByParent').map(call => call.args)).toEqual([[SID, PARENT, 'continuable']]) + expect(mock.log.calls('subagents/interruptByUser').map(call => call.args)).toEqual([[SID]]) expect(mock.log.requests('session/prompt')).toEqual([]) expect(mock.log.requests('session/cancel')).toEqual([]) // A successful interrupt leaves no stop error behind. expect(session.getSnapshot().promptError).toBeNull() - expect(session.getSnapshot().subagent).toEqual({ address: CHILD, parentAvailable: true }) + expect(session.getSnapshot().subagent).toEqual({ address: CHILD }) }) it('sends an addressed child continuation under its echo request id and retires a refused echo', async ({ mock, start }) => { - const session = await sessionBench(mock, start, SID, { address: CHILD, parentAvailable: true }) + const session = await sessionBench(mock, start, SID, { address: CHILD }) await session.open() const accepted = session.beginSubmission({ mode: 'queue', text: '继续', attachments: [] }) await session.prompt([{ type: 'text', text: '继续' }], 'queue', undefined, accepted.requestId) - expect(mock.log.requests('subagents/prompt')).toMatchObject([{ requestId: accepted.requestId, ...CHILD }]) + expect(mock.log.requests('subagents/prompt')).toMatchObject([{ requestId: accepted.requestId, childSessionId: SID }]) expect(session.getSnapshot().pendingSubmissions.map(echo => echo.requestId)).toEqual([accepted.requestId]) const refused = session.beginSubmission({ mode: 'queue', text: '附件', attachments: [] }) @@ -527,7 +527,7 @@ describe('prompt and cancel errors', () => { }) it('forwards continuation image parts to the subagent prompt Remote unstripped', async ({ mock, start }) => { - const session = await sessionBench(mock, start, SID, { address: CHILD, parentAvailable: true }) + const session = await sessionBench(mock, start, SID, { address: CHILD }) await session.open() const content = [ { type: 'text' as const, text: '看这张图' }, @@ -539,7 +539,7 @@ describe('prompt and cancel errors', () => { expect(mock.log.requests('subagents/prompt')).toEqual([ { requestId: expect.any(String) as unknown as string, - ...CHILD, + childSessionId: SID, delivery: 'queue', content, clientTimeZone: TIME_ZONE, @@ -549,18 +549,18 @@ describe('prompt and cancel errors', () => { }) it('lands an interrupt business failure in promptError with op=stop', async ({ mock, start }) => { - const session = await sessionBench(mock, start, SID, { address: CHILD, parentAvailable: true }) - mock.remote.subagents.interruptByParent.mockResolvedValue(err(new RemoteError('subagent/unauthorized', 'nope', { childSessionId: SID }))) + const session = await sessionBench(mock, start, SID, { address: CHILD }) + mock.remote.subagents.interruptByUser.mockResolvedValue(err(new RemoteError('remote-access/denied', 'nope', { action: 'subagents/interruptByUser', reason: 'no-allow' }))) await session.open() const cancelled = await session.cancel() - expect(cancelled).toMatchObject({ ok: false, error: { code: 'subagent/unauthorized' } }) + expect(cancelled).toMatchObject({ ok: false, error: { code: 'remote-access/denied' } }) expect(session.getSnapshot().promptError).toMatchObject({ - op: 'stop', error: { code: 'subagent/unauthorized' }, + op: 'stop', error: { code: 'remote-access/denied' }, }) }) it('rejects staged files instead of dropping them from subagent continuations', async ({ mock, start }) => { - const session = await sessionBench(mock, start, SID, { address: CHILD, parentAvailable: true }) + const session = await sessionBench(mock, start, SID, { address: CHILD }) await session.open() const prompted = await session.prompt([ @@ -578,7 +578,7 @@ describe('prompt and cancel errors', () => { expect(mock.log.requests('subagents/prompt')).toEqual([]) }) - it('sends a one-shot address to the Host under the continuable marker', async ({ mock, start }) => { + it('lets the Host validate the target continuation state', async ({ mock, start }) => { const session = await sessionBench(mock, start, SID, { address: { ...CHILD, mode: 'one-shot' } }) mock.remote.subagents.prompt.mockResolvedValue(err(new RemoteError( 'subagent/not-resumable', 'subagent cannot be resumed', { childSessionId: SID }, @@ -587,11 +587,10 @@ describe('prompt and cancel errors', () => { const prompted = await session.prompt([{ type: 'text', text: '继续' }], 'queue') const cancelled = await session.cancel() - // The Host reads the durable descriptor; the wire marker stays 'continuable'. expect(prompted).toMatchObject({ ok: false, error: { code: 'subagent/not-resumable' } }) expect(cancelled).toEqual({ ok: true, value: { accepted: true } }) - expect(mock.log.requests('subagents/prompt')).toMatchObject([CHILD]) - expect(mock.log.calls('subagents/interruptByParent').map(call => call.args)).toEqual([[SID, PARENT, 'continuable']]) + expect(mock.log.requests('subagents/prompt')).toMatchObject([{ childSessionId: SID }]) + expect(mock.log.calls('subagents/interruptByUser').map(call => call.args)).toEqual([[SID]]) expect(mock.log.requests(FOLLOW)).toEqual([ { address: { kind: 'subagent', ...CHILD, mode: 'one-shot' }, activate: true, assistantStream: true, maxMessages: 500, turnWindow: { minMessages: 50, minTurns: 2 } }, ]) diff --git a/packages/api/session-controller/tests/subagent-access.host.spec.ts b/packages/api/session-controller/tests/subagent-access.host.spec.ts new file mode 100644 index 0000000000..ad077c695b --- /dev/null +++ b/packages/api/session-controller/tests/subagent-access.host.spec.ts @@ -0,0 +1,128 @@ +/** Target Session permissions are enforced before subagent input or cancellation is admitted. */ +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { Context } from '@deepseek-ai/cordis' +import AgentLoop from '@deepseek-ai/dsh-agent-loop' +import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' +import TypertGatewayService from '@deepseek-ai/dsh-api-gateway' +import type { RemoteAccessBinding } from '@deepseek-ai/dsh-api-remote-access' +import { SessionId } from '@deepseek-ai/dsh-session' +import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' +import SubagentRuntime from '@deepseek-ai/dsh-subagent' +import type { PeerId, PeerScope, Principal } from '@deepseek-ai/dsh-typert-protocol' +import TypertRegistry from '@deepseek-ai/dsh-typert-registry' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { MockAdapter } from '../../../core/agent-loop/tests/mock-adapter.ts' +import { mountWorkingDirectoryFixture } from '../../../subagent/subagent/tests/working-directory-fixture.ts' +import { createSessionTestController } from './test-remote.ts' + +declare module '@deepseek-ai/dsh-typert-protocol' { + interface PrincipalMap { + /** Test-only non-owner caller shared by the access specs. */ + 'test-guest': { readonly kind: 'test-guest'; readonly guest: string } + } +} + +const GUEST: Principal = { kind: 'test-guest', guest: 'subagent-controls' } +const ACTIONS = ['subagents/prompt', 'subagents/interruptByUser'] as const +const cleanups: Array<() => Promise> = [] +afterEach(async () => { + for (const cleanup of cleanups.splice(0).reverse()) await cleanup() +}) + +async function harness() { + const root = await mkdtemp(join(tmpdir(), 'dsh-subagent-access-')) + const ctx = new Context() + cleanups.push(async () => { + try { await ctx.fiber.dispose() } finally { await rm(root, { recursive: true, force: true }) } + }) + await ctx.plugin(TypertRegistry) + await mountAgentLoopTestDependencies(ctx) + await ctx.plugin(JsonlSessionPersistence, { root }) + await ctx.plugin(AgentLoop, { agents: [] }) + await mountWorkingDirectoryFixture(ctx) + await createSessionTestController(ctx, { defaultModelSelection: () => ({ provider: 'mock', model: 'mock' }), cwd: root }) + await ctx.plugin(SubagentRuntime) + ctx.subagents.registerProvider({ + name: 'test', inheritsParentContext: false, + capabilities: { agentOptions: true, agentPreset: false, outputSchema: false, depthLimit: false }, + prepareContinuable: async () => ({}), + }) + const adapter = new MockAdapter(['hang']) + ctx.llm.registerAdapter(['mock'], adapter) + const parent = await ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' }) + const run = await ctx.subagents.startActivation({ + provider: 'test', label: 'access target', delivery: 'caller', + parent, prompt: [{ type: 'text', text: 'hold' }], + signal: new AbortController().signal, + }) + await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) }) + const child = ctx.agents.get(run.childId)! + await ctx.plugin(TypertGatewayService) + const bindings: RemoteAccessBinding[] = [] + ctx.remoteAccess.registerPolicy('test-guest', { bindings: () => bindings }) + const call = (method: 'prompt' | 'interruptByUser', principal: Principal = GUEST, image = false) => { + const peer: PeerScope = { + id: 'subagent-access-peer' as PeerId, ctx, principal, interactions: 'answer', + signal: new AbortController().signal, dispose: async () => {}, + } + return ctx.typertGateway.invoke({ + peer, namespace: 'subagents', method, + args: method === 'prompt' ? { request: { + requestId: 'access-follow-up', childSessionId: child.id, delivery: 'queue', + content: image ? [{ type: 'image', mediaType: 'image/png', data: 'aGk=' }] : [{ type: 'text', text: 'follow-up' }], + } } : { childSessionId: child.id }, + }) + } + return { ctx, parent, child, bindings, call } +} + +describe('subagent target Session access', () => { + it.each(['prompt', 'interruptByUser'] as const)('requires the target permission for %s', async (method) => { + const { ctx, parent, child, bindings, call } = await harness() + const attachments = vi.spyOn(ctx.attachments, 'admitPromptContent') + const observe = vi.spyOn(ctx.sessionQuery, 'observeSession') + const cancel = vi.spyOn(child, 'cancel') + const grants: RemoteAccessBinding[][] = [ + [], + [{ role: { name: 'global controls', resource: 'global', actions: ACTIONS }, resource: { kind: 'global' } }], + [{ role: { name: 'parent controls', resource: 'session', actions: ACTIONS }, resource: { kind: 'session', id: parent.id } }], + [{ role: { name: 'other control', resource: 'session', actions: [method === 'prompt' ? ACTIONS[1] : ACTIONS[0]] }, resource: { kind: 'session', id: child.id } }], + ] + for (const grant of grants) { + bindings.splice(0, bindings.length, ...grant) + await expect(call(method, GUEST, true)).rejects.toMatchObject({ + code: 'remote-access/denied', details: { action: `subagents/${method}`, reason: 'no-allow' }, + }) + } + expect(attachments).not.toHaveBeenCalled() + expect(observe).not.toHaveBeenCalled() + expect(cancel).not.toHaveBeenCalled() + expect(child.inbox.nextTurn).toHaveLength(0) + + bindings.splice(0, bindings.length, { + role: { name: 'target control', resource: 'session', actions: [`subagents/${method}`] }, + resource: { kind: 'session', id: child.id }, + }) + const accepted = await call(method) + if (method === 'prompt') { + expect(child.inbox.nextTurn).toHaveLength(1) + expect(accepted).toEqual({ messageId: child.inbox.nextTurn[0]?.id }) + expect(child.inbox.nextTurn[0]?.source).toMatchObject({ kind: 'user', principal: GUEST }) + } else { + expect(accepted).toEqual({ accepted: true }) + expect(cancel).toHaveBeenCalledWith({ kind: 'user' }, { keepInbox: true }) + await child.whenIdle() + } + }) + + it('allows the owner without target bindings', async () => { + const { child, call } = await harness() + const accepted = await call('prompt', { kind: 'owner' }) + expect(child.inbox.nextTurn).toHaveLength(1) + expect(accepted).toEqual({ messageId: child.inbox.nextTurn[0]?.id }) + await expect(call('interruptByUser', { kind: 'owner' })).resolves.toEqual({ accepted: true }) + await child.whenIdle() + }) +}) diff --git a/packages/api/session-controller/tests/transport.client.spec.ts b/packages/api/session-controller/tests/transport.client.spec.ts index 2568f3f807..db0857e5b7 100644 --- a/packages/api/session-controller/tests/transport.client.spec.ts +++ b/packages/api/session-controller/tests/transport.client.spec.ts @@ -83,7 +83,7 @@ function sessionClient(remote: SessionTransportRemote): SessionRemotes { commands: { execute: () => Promise.reject(new Error('stream tests never run commands')) }, subagents: { prompt: () => Promise.reject(new Error('stream tests never prompt a subagent')), - interruptByParent: () => Promise.reject(new Error('stream tests never interrupt a subagent')), + interruptByUser: () => Promise.reject(new Error('stream tests never interrupt a subagent')), }, } } diff --git a/packages/boot/app-boot/README.i18n.yaml b/packages/boot/app-boot/README.i18n.yaml index 9ce0beae8e..98cb511540 100644 --- a/packages/boot/app-boot/README.i18n.yaml +++ b/packages/boot/app-boot/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-app-boot/use-this-package: - en: f6fa378225009c8f - zh: 324c0e4640bb1358 + en: 8e42ac55d685e0f1 + zh: e9e8804af1b2df2b /deepseek-ai-dsh-app-boot/use-this-package/when-to-use-it: en: 2fdfe02f2ed4a258 zh: d748228ece579c4a diff --git a/packages/boot/app-boot/README.md b/packages/boot/app-boot/README.md index 4d30457f81..46fc9be865 100644 --- a/packages/boot/app-boot/README.md +++ b/packages/boot/app-boot/README.md @@ -27,6 +27,8 @@ English | [中文](README.zh.md) Starting an app with this package is a small, explicit entry point: you give it a config file and it runs the whole boot. This section covers what you can do and what you get; the helper calls behind each outcome are documented in the folded implementation section. +Profile loading removes retired installation-bundle selections from `dsh.profile.bundles` and rewrites the manifest only when needed, preserving other fields. [Team migration](../../../docs/upgrade-guide/v0.2.1-rc.2/session-team-mode/guide.md) describes the default composition and retained row overrides. + ### When to use it Use it when implementing the shared `dsh` launcher or embedding its lower-level boot helpers. Product features belong in profile bundles instead of new application bins; code that only adds plugins to an already-running app mounts those plugins directly. diff --git a/packages/boot/app-boot/README.zh.md b/packages/boot/app-boot/README.zh.md index a4d8554ca5..1bc7234f55 100644 --- a/packages/boot/app-boot/README.zh.md +++ b/packages/boot/app-boot/README.zh.md @@ -27,6 +27,8 @@ kind: "package-library" 用此包启动应用是一个小而显式的入口:你给它一个配置文件,它运行整个启动过程。本节说明你能做什么、能得到什么;每个结果背后的 helper 调用记录在下方可折叠的实现章节中。 +profile 加载时会从 `dsh.profile.bundles` 移除已停用的安装方 bundle 选项,仅在需要时重写 manifest,保留其他字段。[Team 迁移](../../../docs/upgrade-guide/v0.2.1-rc.2/session-team-mode/guide.zh.md)说明默认组合与保留的行覆盖。 + ### 何时使用 在实现共享 `dsh` launcher 或嵌入其低层启动 helper 时使用它。产品功能应放入 profile 组合包,而不是新增应用 bin;只向已运行应用添加插件的代码直接挂载插件即可。 diff --git a/packages/boot/app-boot/tests/profile.spec.ts b/packages/boot/app-boot/tests/profile.spec.ts index 0a67d476e1..238f316dc9 100644 --- a/packages/boot/app-boot/tests/profile.spec.ts +++ b/packages/boot/app-boot/tests/profile.spec.ts @@ -394,13 +394,13 @@ describe('loadProfile', () => { ]) }) - it('removes a retired bundle from an application-owned profile and keeps the rest of the manifest', () => { + it.each(['@deepseek-ai/dsh-experimental-schedule-bundle', '@deepseek-ai/dsh-experimental-agent-team-profile'])('removes retired %s and keeps the rest of an application-owned manifest', (retired) => { const anchor = stageInstallation({ '@deepseek-ai/dsh-base': { patch: '[]\n' }, 'custom-bundle': { patch: '[]\n' }, }) const dir = join(tmp(), 'app-profile') - initProfile(dir, ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-experimental-schedule-bundle', 'custom-bundle']) + initProfile(dir, ['@deepseek-ai/dsh-base', retired, 'custom-bundle']) writeProfileManifest(dir, { ...readProfileManifest('t', dir), dependencies: { 'custom-bundle': '^1.0.0' } }) const profile = loadProfileDirectory('t', dir, anchor) expect(profile.skippedBundles).toEqual([]) diff --git a/packages/boot/plugin-market/tests/repository-cache.spec.ts b/packages/boot/plugin-market/tests/repository-cache.spec.ts index bb57767bc3..9b9e77803c 100644 --- a/packages/boot/plugin-market/tests/repository-cache.spec.ts +++ b/packages/boot/plugin-market/tests/repository-cache.spec.ts @@ -1,4 +1,4 @@ -/** Partial clones of real local repositories: reads, README lookup, limits, eviction, and Git failures. */ +/** Partial clones of real local repositories: reads, README lookup, limits, eviction, and Git failures, including a stalled Git fixture. */ import { execFileSync } from 'node:child_process' import { createHash, randomBytes } from 'node:crypto' import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, rmSync, symlinkSync, utimesSync, writeFileSync } from 'node:fs' @@ -17,6 +17,7 @@ async function expectRejection(promise: Promise, fields: object, messag let root: string +const caches: RepositoryCache[] = [] const lifetime = new AbortController() const signal = new AbortController().signal const text = (bytes: Uint8Array | undefined): string => new TextDecoder().decode(bytes) @@ -28,16 +29,20 @@ beforeEach(() => { vi.stubEnv('HOME', join(root, 'home')) vi.stubEnv('XDG_CONFIG_HOME', join(root, 'home')) }) -afterEach(() => { +afterEach(async () => { + await Promise.all(caches.splice(0).map(repositories => repositories.settled())) vi.unstubAllEnvs() vi.unstubAllGlobals() rmSync(root, { recursive: true, force: true }) }) function git(cwd: string, ...args: string[]): string { - // Fixture repositories ignore inherited Git overrides, such as a hook's `GIT_DIR`, and every user or system configuration. + // Fixture Git ignores inherited overrides and user configuration. Background maintenance stays disabled. const env = Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith('GIT_'))) - return execFileSync('git', ['-c', 'user.name=t', '-c', 'user.email=t@example.com', '-c', 'commit.gpgsign=false', ...args], { + return execFileSync('git', [ + '-c', 'user.name=t', '-c', 'user.email=t@example.com', '-c', 'commit.gpgsign=false', + '-c', 'gc.auto=0', '-c', 'maintenance.auto=false', ...args, + ], { cwd, encoding: 'utf8', env: { ...env, HOME: join(root, 'home'), XDG_CONFIG_HOME: join(root, 'home'), GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: '/dev/null' }, }).trim() @@ -73,10 +78,12 @@ function repository(name: string, files: Record): { } function cache(options: Partial = {}): RepositoryCache { - return new RepositoryCache({ + const repositories = new RepositoryCache({ dir: join(root, 'cache'), maxRepositoryBytes: 10 * 1024 * 1024, maxCacheBytes: 100 * 1024 * 1024, timeoutMs: 30_000, ttlMs: 60_000, transports: ['file', 'ssh'], lifetime: lifetime.signal, sizePollMs: 1, ...options, }) + caches.push(repositories) + return repositories } it('reads files at a pinned commit and the default branch, fetching contents only when read', async () => { @@ -132,7 +139,7 @@ it('asks the smart HTTP endpoint without credentials whether a repository is pub expect(fetch).toHaveBeenCalledTimes(5) }) -it('reports an unreachable host as a network failure, a failed local command as a plain error, and a Git that cannot run', async () => { +it('reports an unreachable host as a network failure, a failed local command as a plain error, and a missing Git', async () => { vi.stubEnv('GIT_SSH_COMMAND', 'ssh -F /dev/null -o BatchMode=yes -o ConnectTimeout=5') await expectRejection(cache().read('ssh://127.0.0.1:1/owner/repo.git', undefined, 'README.md', 100, signal), { reason: 'network' }, 'git fetch') const source = repository('t', { 'README.md': 'x' }) @@ -149,15 +156,31 @@ it('reports an unreachable host as a network failure, a failed local command as await expect(broken).rejects.toThrow('git update-ref') await expect(broken).rejects.not.toBeInstanceOf(DetailFailure) expect(readdirSync(join(root, 'cache'))).toEqual([]) - // Git that cannot answer `git --version` within the deadline cannot run. const other = repository('t2', { 'README.md': 'x' }) - await expectRejection(cache({ timeoutMs: 1 }).read(other.url, other.commit, 'README.md', 100, signal), { reason: 'git-unavailable' }, 'timed out') vi.stubEnv('PATH', join(root, 'no-git')) const missing = cache().read(other.url, other.commit, 'README.md', 100, signal) await expectRejection(missing, { reason: 'git-unavailable' }, 'git --version') expect(readdirSync(join(root, 'cache'))).toEqual([]) }) +it('reports a stalled Git version check as unavailable before initializing a clone', async () => { + const bin = join(root, 'stalled-git') + const script = join(bin, 'stall.cjs') + mkdirSync(bin) + writeFileSync(script, 'setInterval(() => {}, 1000)\n') + vi.stubEnv('PLUGIN_MARKET_TEST_NODE', process.execPath) + vi.stubEnv('PLUGIN_MARKET_TEST_GIT_SCRIPT', script) + writeFileSync(join(bin, process.platform === 'win32' ? 'git.cmd' : 'git'), process.platform === 'win32' + ? '@"%PLUGIN_MARKET_TEST_NODE%" "%PLUGIN_MARKET_TEST_GIT_SCRIPT%"\r\n' + : '#!/bin/sh\nexec "$PLUGIN_MARKET_TEST_NODE" "$PLUGIN_MARKET_TEST_GIT_SCRIPT"\n', { mode: 0o755 }) + vi.stubEnv('PATH', bin) + const repositories = cache({ timeoutMs: 100 }) + const stalled = repositories.read('https://git.example/stalled.git', undefined, 'README.md', 100, signal) + await expectRejection(stalled, { reason: 'git-unavailable' }, 'timed out') + await expect(stalled).rejects.toThrow('git --version') + expect(readdirSync(join(root, 'cache'))).toEqual([]) +}) + it('reports a repository Git cannot fetch as denied', async () => { const repositories = cache() const missing = pathToFileURL(join(root, 'missing')).href diff --git a/packages/bundle/base/README.i18n.yaml b/packages/bundle/base/README.i18n.yaml index c6f661f22d..8c6ebd500c 100644 --- a/packages/bundle/base/README.i18n.yaml +++ b/packages/bundle/base/README.i18n.yaml @@ -21,8 +21,8 @@ en: 0f971f7ac7c09139 zh: ceb866da681c709c /deepseek-ai-dsh-base/use-this-package/what-you-get: - en: 2ef2653d55107eec - zh: 4936c0fff94b1984 + en: 94430c891006526d + zh: 94ba771960ec6f71 /deepseek-ai-dsh-base/use-this-package/shell-tools-per-platform: en: 2dce12f82d6ee955 zh: d4599eb899a3cd88 diff --git a/packages/bundle/base/README.md b/packages/bundle/base/README.md index f928d433d6..db77c345ec 100644 --- a/packages/bundle/base/README.md +++ b/packages/bundle/base/README.md @@ -51,6 +51,8 @@ Out of the box, every profile built on this core provides: a DeepSeek model conn Both `subagent` and `subagent_fork` create managed children and return a child id immediately. Use `send_message` to steer active children or continue their conversations after completion. [Delegation-tool behavior](../../subagent/tool-subagent/README.md) describes result delivery and backend capabilities. +The bundle also mounts [Agent Teams](../../team/agent-team/README.md) and its task tools. Team mode defaults off per root Session; the root conversation’s **Use Agent Teams** menu switch enables collaboration for that tree without changing the profile. + The built-in `working_directory` tool changes the directory for subsequent operations while existing processes and write permissions remain independent. Current directory information appears in user context. Default file editing uses `read`, `write`, and `edit`. The `str_replace_editor` tool remains available as an explicit opt-in. To add it to a base-backed profile, put this entry in the profile, home, or invocation patch: ```yaml diff --git a/packages/bundle/base/README.zh.md b/packages/bundle/base/README.zh.md index 132a40bab3..53c04f4b52 100644 --- a/packages/bundle/base/README.zh.md +++ b/packages/bundle/base/README.zh.md @@ -51,6 +51,8 @@ kind: "package-bundle" `subagent` 与 `subagent_fork` 都创建受管理的子任务,并立即返回 child id。使用 `send_message` 可引导活跃子任务,或在完成后继续其对话。[委派工具行为](../../subagent/tool-subagent/README.zh.md)说明了结果交付和后端能力。 +此 bundle 还挂载 [Agent Teams](../../team/agent-team/README.zh.md) 及其任务工具。每个根 Session 的 Team 模式默认关闭;根会话菜单中的**使用智能体团队**开关为当前树开启协作,无需修改 profile。 + 内置 `working_directory` 工具改变后续操作使用的目录,已有进程与写权限保持独立。当前目录信息出现在用户上下文中。默认文件编辑使用 `read`、`write` 和 `edit`。`str_replace_editor` 工具仍可显式启用。要将它加入基于 base 的 profile,请在 profile、home 或逐次调用 patch 中添加以下条目: ```yaml diff --git a/packages/bundle/base/cordis.patch.yml b/packages/bundle/base/cordis.patch.yml index ef931753d9..6740234ade 100644 --- a/packages/bundle/base/cordis.patch.yml +++ b/packages/bundle/base/cordis.patch.yml @@ -363,6 +363,14 @@ - id: subagent name: '@deepseek-ai/dsh-subagent' + - id: agent-team + name: '@deepseek-ai/dsh-agent-team' + config: + maxTasks: 256 + + - id: tool-agent-team + name: '@deepseek-ai/dsh-tool-agent-team' + - id: subagent-spawn-in-process name: '@deepseek-ai/dsh-subagent-spawn-in-process' config: diff --git a/packages/bundle/base/package.json b/packages/bundle/base/package.json index 6c6e508732..68a395af92 100644 --- a/packages/bundle/base/package.json +++ b/packages/bundle/base/package.json @@ -34,13 +34,16 @@ } }, "dependencies": { - "@deepseek-ai/dsh-hmr": "workspace:*", "@deepseek-ai/cordis-plugin-timer": "workspace:~", "@deepseek-ai/dsh-agent": "workspace:*", "@deepseek-ai/dsh-agent-default-model": "workspace:*", + "@deepseek-ai/dsh-agent-instructions": "workspace:*", "@deepseek-ai/dsh-agent-loop": "workspace:*", + "@deepseek-ai/dsh-agent-team": "workspace:*", + "@deepseek-ai/dsh-api-gateway": "workspace:*", + "@deepseek-ai/dsh-api-remote-access": "workspace:*", "@deepseek-ai/dsh-attachment-local": "workspace:*", - "@deepseek-ai/dsh-shell-env": "workspace:*", + "@deepseek-ai/dsh-authorization": "workspace:*", "@deepseek-ai/dsh-bash-sandbox": "workspace:*", "@deepseek-ai/dsh-command-compact": "workspace:*", "@deepseek-ai/dsh-command-feedback": "workspace:*", @@ -49,24 +52,29 @@ "@deepseek-ai/dsh-compaction-basic": "workspace:*", "@deepseek-ai/dsh-compaction-image-offload": "workspace:*", "@deepseek-ai/dsh-compaction-tool-result-pruner": "workspace:*", + "@deepseek-ai/dsh-config-editor": "workspace:*", "@deepseek-ai/dsh-credentials-local": "workspace:*", + "@deepseek-ai/dsh-deepseek-account-platform": "workspace:*", "@deepseek-ai/dsh-deepseek-llm-api-extensions": "workspace:*", - "@deepseek-ai/dsh-plugin-package-inventory-deepseek": "workspace:*", "@deepseek-ai/dsh-fs-local": "workspace:*", "@deepseek-ai/dsh-fs-observation-policy": "workspace:*", "@deepseek-ai/dsh-fs-sandbox": "workspace:*", "@deepseek-ai/dsh-goal": "workspace:*", "@deepseek-ai/dsh-goal-round-driver": "workspace:*", - "@deepseek-ai/dsh-api-gateway": "workspace:*", - "@deepseek-ai/dsh-api-remote-access": "workspace:*", + "@deepseek-ai/dsh-hmr": "workspace:*", + "@deepseek-ai/dsh-jobs-local": "workspace:*", "@deepseek-ai/dsh-llm": "workspace:*", - "@deepseek-ai/dsh-llm-deepseek-api-key": "workspace:*", "@deepseek-ai/dsh-llm-deepseek-account": "workspace:*", + "@deepseek-ai/dsh-llm-deepseek-api-key": "workspace:*", "@deepseek-ai/dsh-llm-pi-ai": "workspace:*", "@deepseek-ai/dsh-llm-retry": "workspace:*", "@deepseek-ai/dsh-mcp-resources": "workspace:*", + "@deepseek-ai/dsh-otel": "workspace:*", "@deepseek-ai/dsh-permission-presets": "workspace:*", "@deepseek-ai/dsh-plan-mode": "workspace:*", + "@deepseek-ai/dsh-plugin-manager": "workspace:*", + "@deepseek-ai/dsh-plugin-package-inventory-deepseek": "workspace:*", + "@deepseek-ai/dsh-ptc-runtime-node": "workspace:*", "@deepseek-ai/dsh-pwsh-sandbox": "workspace:*", "@deepseek-ai/dsh-repeat-tool-reminder": "workspace:*", "@deepseek-ai/dsh-sandbox-local": "workspace:*", @@ -81,6 +89,8 @@ "@deepseek-ai/dsh-session-telemetry-otel": "workspace:*", "@deepseek-ai/dsh-session-title": "workspace:*", "@deepseek-ai/dsh-session-title-first-prompt-llm": "workspace:*", + "@deepseek-ai/dsh-settings": "workspace:*", + "@deepseek-ai/dsh-shell-env": "workspace:*", "@deepseek-ai/dsh-skill": "workspace:*", "@deepseek-ai/dsh-skill-filesystem": "workspace:*", "@deepseek-ai/dsh-spill-local": "workspace:*", @@ -93,44 +103,36 @@ "@deepseek-ai/dsh-subagent-spawn-in-process": "workspace:*", "@deepseek-ai/dsh-subprocess-local": "workspace:*", "@deepseek-ai/dsh-system-prompt": "workspace:*", - "@deepseek-ai/dsh-jobs-local": "workspace:*", - "@deepseek-ai/dsh-tool-call-timeout-policy": "workspace:*", "@deepseek-ai/dsh-token-meter": "workspace:*", + "@deepseek-ai/dsh-tool-agent-team": "workspace:*", "@deepseek-ai/dsh-tool-bash": "workspace:*", - "@deepseek-ai/dsh-tool-present": "workspace:*", + "@deepseek-ai/dsh-tool-call-timeout-policy": "workspace:*", "@deepseek-ai/dsh-tool-executor-native": "workspace:*", "@deepseek-ai/dsh-tool-executor-ptc": "workspace:*", "@deepseek-ai/dsh-tool-fs": "workspace:*", "@deepseek-ai/dsh-tool-fs-search": "workspace:*", "@deepseek-ai/dsh-tool-goal": "workspace:*", + "@deepseek-ai/dsh-tool-jobs": "workspace:*", + "@deepseek-ai/dsh-tool-present": "workspace:*", "@deepseek-ai/dsh-tool-pwsh": "workspace:*", "@deepseek-ai/dsh-tool-skill": "workspace:*", "@deepseek-ai/dsh-tool-subagent": "workspace:*", "@deepseek-ai/dsh-tool-subagent-control": "workspace:*", - "@deepseek-ai/dsh-tool-jobs": "workspace:*", "@deepseek-ai/dsh-tool-todo": "workspace:*", "@deepseek-ai/dsh-tool-web": "workspace:*", "@deepseek-ai/dsh-tool-workflow": "workspace:*", + "@deepseek-ai/dsh-tool-working-directory": "workspace:*", "@deepseek-ai/dsh-tools": "workspace:*", "@deepseek-ai/dsh-typert-loader": "workspace:*", "@deepseek-ai/dsh-typert-registry": "workspace:*", "@deepseek-ai/dsh-user-approval": "workspace:*", "@deepseek-ai/dsh-user-questions": "workspace:*", "@deepseek-ai/dsh-web": "workspace:*", + "@deepseek-ai/dsh-web-deepseek-api": "workspace:*", "@deepseek-ai/dsh-web-fetch-http": "workspace:*", "@deepseek-ai/dsh-web-search-deepseek": "workspace:*", "@deepseek-ai/dsh-workflow-ptc": "workspace:*", - "@deepseek-ai/dsh-agent-instructions": "workspace:*", - "@deepseek-ai/dsh-ptc-runtime-node": "workspace:*", - "@deepseek-ai/dsh-plugin-manager": "workspace:*", - "@deepseek-ai/dsh-authorization": "workspace:*", - "@deepseek-ai/dsh-deepseek-account-platform": "workspace:*", - "@deepseek-ai/dsh-config-editor": "workspace:*", - "@deepseek-ai/dsh-settings": "workspace:*", - "@deepseek-ai/dsh-otel": "workspace:*", - "@deepseek-ai/dsh-working-directory": "workspace:*", - "@deepseek-ai/dsh-tool-working-directory": "workspace:*", - "@deepseek-ai/dsh-web-deepseek-api": "workspace:*" + "@deepseek-ai/dsh-working-directory": "workspace:*" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~" diff --git a/packages/bundle/headless/src/index.ts b/packages/bundle/headless/src/index.ts index d02729c097..bc98d7ae6b 100644 --- a/packages/bundle/headless/src/index.ts +++ b/packages/bundle/headless/src/index.ts @@ -426,7 +426,7 @@ async function run(ctx: Context, config: Config, io: HeadlessIo): Promise await agent.whenIdle() const idleSeq = agent.session.seq // Child settlement can wake the parent before the child wait observes it. - if (!await subagents?.waitForChildren(agent) + if (!await subagents?.waitForTreeWork(agent) && agent.status === 'idle' && agent.session.seq === idleSeq) break } } finally { diff --git a/packages/bundle/headless/tests/headless.spec.ts b/packages/bundle/headless/tests/headless.spec.ts index 0d00029409..7a76b0b3e5 100644 --- a/packages/bundle/headless/tests/headless.spec.ts +++ b/packages/bundle/headless/tests/headless.spec.ts @@ -381,7 +381,7 @@ describe('headless runner', () => { if (turns === 1) { await test.ctx.subagents.startActivation({ provider: 'external', label: 'work', delivery: 'parent', signal: new AbortController().signal, - request: { parent, prompt: [] }, + parent, prompt: [], }) await cleaning.promise appendTurn(session, 1, message, 'waiting for child', true) @@ -429,7 +429,7 @@ describe('headless runner', () => { }, }) test.ctx.provide('subagents', { - async waitForChildren(parent: Agent) { + async waitForTreeWork(parent: Agent) { expect(test.ctx.agents.get(parent.id)).toBe(parent) const batch = checks++ if (batch === 2) return false @@ -467,7 +467,7 @@ describe('headless runner', () => { }, }) test.ctx.provide('subagents', { - async waitForChildren(parent: Agent) { + async waitForTreeWork(parent: Agent) { const check = checks++ if (check === 2) return false parent.followup(createUserMessage({ content: [{ type: 'text', text: 'batch complete' }], source: { kind: 'user' } })) diff --git a/packages/bundle/headless/tests/terminal-failures.spec.ts b/packages/bundle/headless/tests/terminal-failures.spec.ts index 5c20881ea9..c92f095334 100644 --- a/packages/bundle/headless/tests/terminal-failures.spec.ts +++ b/packages/bundle/headless/tests/terminal-failures.spec.ts @@ -60,7 +60,7 @@ describe('headless terminal failures', () => { const test = await harness() const failures: unknown[] = [] test.ctx.on('agent/error', ({ error }) => { failures.push(error) }) - const waitForChildren = vi.fn().mockImplementationOnce((parent: Agent) => { + const waitForTreeWork = vi.fn().mockImplementationOnce((parent: Agent) => { const append = parent.session.append.bind(parent.session) const failAppend: typeof parent.session.append = (type, data, ...intent) => { if (type === failedEvent) throw new Error(`summary ${failedEvent} unavailable`) @@ -70,7 +70,7 @@ describe('headless terminal failures', () => { followup(parent) return Promise.resolve(true) }).mockResolvedValue(false) - test.ctx.provide('subagents', { waitForChildren } as never) + test.ctx.provide('subagents', { waitForTreeWork } as never) const result = await test.run(json) expect(failures).toHaveLength(1) @@ -87,7 +87,7 @@ describe('headless terminal failures', () => { it.each([false, true])('uses the durable outcome after a summary hook fails (recover=%s)', async (recover) => { const test = await harness() - const waitForChildren = vi.fn().mockImplementationOnce((parent: Agent) => { + const waitForTreeWork = vi.fn().mockImplementationOnce((parent: Agent) => { const stop = test.ctx.on('agent/pre-step', () => { stop() throw new Error('summary hook failed') @@ -98,7 +98,7 @@ describe('headless terminal failures', () => { if (recover) followup(parent) return Promise.resolve(recover) }).mockResolvedValue(false) - test.ctx.provide('subagents', { waitForChildren } as never) + test.ctx.provide('subagents', { waitForTreeWork } as never) await expect(test.run()).resolves.toEqual(recover ? { code: 0, out: 'summary\n', err: '' } @@ -107,7 +107,7 @@ describe('headless terminal failures', () => { it.each(['turn/start', 'turn/end'] as const)('accepts a recovered summary after a missing %s', async (failedEvent) => { const test = await harness() - const waitForChildren = vi.fn().mockImplementationOnce((parent: Agent) => { + const waitForTreeWork = vi.fn().mockImplementationOnce((parent: Agent) => { const append = parent.session.append.bind(parent.session) let failed = false const failAppend: typeof parent.session.append = (type, data, ...intent) => { @@ -124,7 +124,7 @@ describe('headless terminal failures', () => { followup(parent) return Promise.resolve(true) }).mockResolvedValue(false) - test.ctx.provide('subagents', { waitForChildren } as never) + test.ctx.provide('subagents', { waitForTreeWork } as never) await expect(test.run()).resolves.toEqual({ code: 0, @@ -142,12 +142,12 @@ describe('headless terminal failures', () => { if (agent === other) throw new Error('another Agent failed') return next() }) - const waitForChildren = vi.fn().mockImplementationOnce(async () => { + const waitForTreeWork = vi.fn().mockImplementationOnce(async () => { followup(other) await other.whenIdle() return false }).mockResolvedValue(false) - test.ctx.provide('subagents', { waitForChildren } as never) + test.ctx.provide('subagents', { waitForTreeWork } as never) await expect(test.run()).resolves.toEqual({ code: 0, out: 'waiting\n', err: '' }) }) diff --git a/packages/bundle/web-app/README.i18n.yaml b/packages/bundle/web-app/README.i18n.yaml index ac78994ab7..adb17d9e6d 100644 --- a/packages/bundle/web-app/README.i18n.yaml +++ b/packages/bundle/web-app/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-web-app/use-this-package: - en: 3bd37b85139d2fa7 - zh: ba86c807eb83866e + en: 1f69a4c52a27e7e5 + zh: e4431a252fb86f7c /deepseek-ai-dsh-web-app/use-this-package/starting-the-web-gui: en: e3cac911f080d8ef zh: 60749e9f5c39c1bc diff --git a/packages/bundle/web-app/README.md b/packages/bundle/web-app/README.md index cb974443e5..b621d1e8c2 100644 --- a/packages/bundle/web-app/README.md +++ b/packages/bundle/web-app/README.md @@ -31,6 +31,8 @@ Run `dsh --profile web` for browser chat, model and settings management, and ses Start the GUI, open your browser, and start talking to the agent. The flags fine-tune the invocation. +[Agent Teams UI](../../client/ui-agent-team/README.md) loads with this bundle. Enable it per Session in the model and reasoning menu; shared tasks open below the member tree from the Subagent control. + ### Starting the Web GUI ```sh diff --git a/packages/bundle/web-app/README.zh.md b/packages/bundle/web-app/README.zh.md index 88bbae54a5..9df9111bf3 100644 --- a/packages/bundle/web-app/README.zh.md +++ b/packages/bundle/web-app/README.zh.md @@ -31,6 +31,8 @@ kind: "package-bundle" 启动 GUI、打开浏览器,然后开始与 agent(智能体)对话。flag 用于微调本次调用。 +[Agent Teams UI](../../client/ui-agent-team/README.zh.md) 随本 bundle 加载。在模型与推理等级菜单中按 Session 开启;通过 Subagent 控件打开成员树,下方显示共享任务。 + ### 启动 Web GUI ```sh diff --git a/packages/bundle/web-app/cordis.patch.yml b/packages/bundle/web-app/cordis.patch.yml index d751fcc462..dfaccf6812 100644 --- a/packages/bundle/web-app/cordis.patch.yml +++ b/packages/bundle/web-app/cordis.patch.yml @@ -382,6 +382,9 @@ - id: ui-skill name: '@deepseek-ai/dsh-client-ui-skill' + - id: ui-agent-team + name: '@deepseek-ai/dsh-client-ui-agent-team' + - id: ui-subagent name: '@deepseek-ai/dsh-client-ui-subagent' @@ -552,12 +555,9 @@ - id: tool-result-pruner disabled: true -# The subagent registry and its backends STAY in the host plane. `subagents` is -# a process singleton with a cross-session query surface (`listChildren`, -# `followup`) that the host api-proxy serves to the browser, and a provider -# registers under a globally unique name, so a per-session copy would both -# starve that host row and collide on the second session. What a preset -# chooses is which delegation TOOLS its agent sees, below. +# The subagent registry and its backends belong in the host composition: browser +# prompt and interrupt requests share its runtime, and provider names are unique +# within the registry. Presets select the delegation tools visible to each Agent. - id: tool-subagent-control disabled: true diff --git a/packages/bundle/web-app/package.json b/packages/bundle/web-app/package.json index 9ecc295724..89cc800d9f 100644 --- a/packages/bundle/web-app/package.json +++ b/packages/bundle/web-app/package.json @@ -58,6 +58,7 @@ "@deepseek-ai/dsh-agent-instructions": "workspace:*", "@deepseek-ai/dsh-agent-preset": "workspace:*", "@deepseek-ai/dsh-agent-preset-registry": "workspace:*", + "@deepseek-ai/dsh-agent-tool-presentation": "workspace:*", "@deepseek-ai/dsh-api-job-controller": "workspace:*", "@deepseek-ai/dsh-api-remotes": "workspace:*", "@deepseek-ai/dsh-api-session-controller": "workspace:*", @@ -71,8 +72,11 @@ "@deepseek-ai/dsh-client-hmr": "workspace:*", "@deepseek-ai/dsh-client-locale": "workspace:*", "@deepseek-ai/dsh-client-modules": "workspace:*", + "@deepseek-ai/dsh-client-product-analytics": "workspace:*", "@deepseek-ai/dsh-client-resources": "workspace:*", + "@deepseek-ai/dsh-client-shortcuts": "workspace:*", "@deepseek-ai/dsh-client-ui-agent-preset": "workspace:*", + "@deepseek-ai/dsh-client-ui-agent-team": "workspace:*", "@deepseek-ai/dsh-client-ui-approval": "workspace:*", "@deepseek-ai/dsh-client-ui-attachment": "workspace:*", "@deepseek-ai/dsh-client-ui-brand-official": "workspace:*", @@ -97,18 +101,21 @@ "@deepseek-ai/dsh-client-ui-plugin-manager": "workspace:*", "@deepseek-ai/dsh-client-ui-reference": "workspace:*", "@deepseek-ai/dsh-client-ui-renderer": "workspace:*", + "@deepseek-ai/dsh-client-ui-schedule": "workspace:*", "@deepseek-ai/dsh-client-ui-secret-mask": "workspace:*", "@deepseek-ai/dsh-client-ui-session": "workspace:*", - "@deepseek-ai/dsh-client-ui-schedule": "workspace:*", "@deepseek-ai/dsh-client-ui-settings": "workspace:*", + "@deepseek-ai/dsh-client-ui-settings-account": "workspace:*", + "@deepseek-ai/dsh-client-ui-settings-agent-loop": "workspace:*", "@deepseek-ai/dsh-client-ui-settings-general": "workspace:*", "@deepseek-ai/dsh-client-ui-settings-models": "workspace:*", "@deepseek-ai/dsh-client-ui-settings-plugin-inventory": "workspace:*", "@deepseek-ai/dsh-client-ui-settings-plugins": "workspace:*", + "@deepseek-ai/dsh-client-ui-settings-session-log": "workspace:*", "@deepseek-ai/dsh-client-ui-settings-shell": "workspace:*", - "@deepseek-ai/dsh-client-ui-settings-agent-loop": "workspace:*", "@deepseek-ai/dsh-client-ui-settings-subagent": "workspace:*", "@deepseek-ai/dsh-client-ui-settings-web-search": "workspace:*", + "@deepseek-ai/dsh-client-ui-shortcuts": "workspace:*", "@deepseek-ai/dsh-client-ui-sidebar": "workspace:*", "@deepseek-ai/dsh-client-ui-sidebar-browser": "workspace:*", "@deepseek-ai/dsh-client-ui-sidebar-documentpreview": "workspace:*", @@ -130,14 +137,17 @@ "@deepseek-ai/dsh-compaction-tool-result-pruner": "workspace:*", "@deepseek-ai/dsh-cordis-client-runner": "workspace:*", "@deepseek-ai/dsh-cordis-host-runner": "workspace:*", + "@deepseek-ai/dsh-deliverable-file-references": "workspace:*", "@deepseek-ai/dsh-file-reference": "workspace:*", "@deepseek-ai/dsh-file-reference-local": "workspace:*", + "@deepseek-ai/dsh-harness-dev-context": "workspace:*", "@deepseek-ai/dsh-host-directory-picker-auto": "workspace:*", "@deepseek-ai/dsh-host-directory-picker-browse": "workspace:*", "@deepseek-ai/dsh-host-directory-picker-native": "workspace:*", "@deepseek-ai/dsh-host-frontend-static": "workspace:*", "@deepseek-ai/dsh-host-open-in-app": "workspace:*", "@deepseek-ai/dsh-host-plugin-inventory": "workspace:*", + "@deepseek-ai/dsh-host-product-telemetry-otel": "workspace:*", "@deepseek-ai/dsh-host-webserver": "workspace:*", "@deepseek-ai/dsh-launch-environment": "workspace:*", "@deepseek-ai/dsh-message-feedback": "workspace:*", diff --git a/packages/client/README.i18n.yaml b/packages/client/README.i18n.yaml index 503d65a1c8..1c3494a2f5 100644 --- a/packages/client/README.i18n.yaml +++ b/packages/client/README.i18n.yaml @@ -15,8 +15,8 @@ en: 1eb35b015a6c423f zh: a8a3dceee2c9e943 /client-web-gui-browser-half/packages: - en: b6fae58a79112e2b - zh: c95394e541d421ba + en: d7adae6baeed495b + zh: 9e49fb00adc8eec1 /client-web-gui-browser-half/related-documentation: en: d1d506e68fab7333 zh: 1a90fe6f3f8f24a9 diff --git a/packages/client/README.md b/packages/client/README.md index fe33ee2a20..a61299c2d2 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -64,6 +64,7 @@ The kernel packages boot and serve the page; the UI feature packages present it. | [`ui-skill/`](ui-skill/README.md) | Adds skill references to inline suggestions | — | | [`ui-reference/`](ui-reference/README.md) | Unified Web `@file` / `@session` reference source | — | | [`ui-secret-mask/`](ui-secret-mask/README.md) | Masks recognized credentials in sent user text | — | +| [`ui-agent-team/`](ui-agent-team/README.md) | Session Team switch and shared tasks below the Subagent tree | — | | [`ui-subagent/`](ui-subagent/README.md) | Provides subagent navigation, child transcript states, and inline references | — | | [`ui-jobs/`](ui-jobs/README.md) | Lists this session's background jobs with on-demand streaming record panels | — | | [`ui-schedule/`](ui-schedule/README.md) | Provides the sidebar Automation tasks page and right Sidebar task tab, which create, edit, run, pause, resume, and delete Host scheduled tasks, plus the Session header clock and row mark | — | diff --git a/packages/client/README.zh.md b/packages/client/README.zh.md index de0479c889..1e865a79c2 100644 --- a/packages/client/README.zh.md +++ b/packages/client/README.zh.md @@ -64,6 +64,7 @@ kind: "package-group" | [`ui-skill/`](ui-skill/README.zh.md) | 向内联建议添加 skill(技能)引用 | — | | [`ui-reference/`](ui-reference/README.zh.md) | 统一的 Web `@file` / `@session` 引用 source | — | | [`ui-secret-mask/`](ui-secret-mask/README.zh.md) | 遮挡已发送用户文本中可识别的凭据 | — | +| [`ui-agent-team/`](ui-agent-team/README.zh.md) | Session Team 开关与 Subagent 树下方的共享任务 | — | | [`ui-subagent/`](ui-subagent/README.zh.md) | 提供 subagent 导航、子级 transcript(文本记录)状态与内联引用 | — | | [`ui-jobs/`](ui-jobs/README.zh.md) | 列出本会话的后台任务,并提供按需展开的流式输出面板 | — | | [`ui-schedule/`](ui-schedule/README.zh.md) | 提供侧栏“自动化任务”页面与右侧栏任务页签,用于创建、编辑、立即运行、暂停、恢复和删除宿主的计划任务,并提供 Session 顶部时钟与行标记 | — | diff --git a/packages/client/locale/src/locales/de.ts b/packages/client/locale/src/locales/de.ts index dd88cf6fc4..cb0bb45579 100644 --- a/packages/client/locale/src/locales/de.ts +++ b/packages/client/locale/src/locales/de.ts @@ -33,6 +33,8 @@ export const de = { 'more': 'Mehr', 'collapse': 'Einklappen', 'expand': 'Ausklappen', + 'subtitle.expand': 'Beschreibung für {name} ausklappen', + 'subtitle.collapse': 'Beschreibung für {name} einklappen', 'back': 'Zurück', 'brand.localBuild': 'Lokaler DSH-Build', 'workspace.defaultName': 'Standardarbeitsbereich', diff --git a/packages/client/locale/src/locales/en.ts b/packages/client/locale/src/locales/en.ts index b649bbc3dc..47a03d69ef 100644 --- a/packages/client/locale/src/locales/en.ts +++ b/packages/client/locale/src/locales/en.ts @@ -32,6 +32,8 @@ export const en = { 'more': 'More', 'collapse': 'Collapse', 'expand': 'Expand', + 'subtitle.expand': 'Expand description for {name}', + 'subtitle.collapse': 'Collapse description for {name}', 'back': 'Back', 'brand.localBuild': 'DSH Local Build', 'workspace.defaultName': 'Default workspace', diff --git a/packages/client/locale/src/locales/es.ts b/packages/client/locale/src/locales/es.ts index 7eb95374e1..4fba984885 100644 --- a/packages/client/locale/src/locales/es.ts +++ b/packages/client/locale/src/locales/es.ts @@ -33,6 +33,8 @@ export const es = { 'more': 'Más', 'collapse': 'Contraer', 'expand': 'Expandir', + 'subtitle.expand': 'Expandir la descripción de {name}', + 'subtitle.collapse': 'Contraer la descripción de {name}', 'back': 'Atrás', 'brand.localBuild': 'Compilación local de DSH', 'workspace.defaultName': 'Espacio de trabajo predeterminado', diff --git a/packages/client/locale/src/locales/fr.ts b/packages/client/locale/src/locales/fr.ts index c57f1a3b11..f0e5030f41 100644 --- a/packages/client/locale/src/locales/fr.ts +++ b/packages/client/locale/src/locales/fr.ts @@ -33,6 +33,8 @@ export const fr = { 'more': 'Plus', 'collapse': 'Réduire', 'expand': 'Développer', + 'subtitle.expand': 'Développer la description de {name}', + 'subtitle.collapse': 'Réduire la description de {name}', 'back': 'Retour', 'brand.localBuild': 'Version locale de DSH', 'workspace.defaultName': 'Espace de travail par défaut', diff --git a/packages/client/locale/src/locales/ja.ts b/packages/client/locale/src/locales/ja.ts index 1186838c31..2b70cbb156 100644 --- a/packages/client/locale/src/locales/ja.ts +++ b/packages/client/locale/src/locales/ja.ts @@ -33,6 +33,8 @@ export const ja = { 'more': 'その他', 'collapse': '折りたたむ', 'expand': '展開', + 'subtitle.expand': '{name} の説明を展開', + 'subtitle.collapse': '{name} の説明を折りたたむ', 'back': '戻る', 'brand.localBuild': 'DSHローカルビルド', 'workspace.defaultName': 'デフォルトワークスペース', diff --git a/packages/client/locale/src/locales/pt.ts b/packages/client/locale/src/locales/pt.ts index 75cdb19c29..adf711152d 100644 --- a/packages/client/locale/src/locales/pt.ts +++ b/packages/client/locale/src/locales/pt.ts @@ -33,6 +33,8 @@ export const pt = { 'more': 'Mais', 'collapse': 'Recolher', 'expand': 'Expandir', + 'subtitle.expand': 'Expandir descrição de {name}', + 'subtitle.collapse': 'Recolher descrição de {name}', 'back': 'Voltar', 'brand.localBuild': 'Versão local do DSH', 'workspace.defaultName': 'Espaço de trabalho padrão', diff --git a/packages/client/locale/src/locales/ru.ts b/packages/client/locale/src/locales/ru.ts index a8a6d829da..e75620fe21 100644 --- a/packages/client/locale/src/locales/ru.ts +++ b/packages/client/locale/src/locales/ru.ts @@ -33,6 +33,8 @@ export const ru = { 'more': 'Ещё', 'collapse': 'Свернуть', 'expand': 'Развернуть', + 'subtitle.expand': 'Развернуть описание {name}', + 'subtitle.collapse': 'Свернуть описание {name}', 'back': 'Назад', 'brand.localBuild': 'Локальная сборка DSH', 'workspace.defaultName': 'Рабочая область по умолчанию', diff --git a/packages/client/locale/src/locales/zh.ts b/packages/client/locale/src/locales/zh.ts index 11ff789b8a..6350caae3c 100644 --- a/packages/client/locale/src/locales/zh.ts +++ b/packages/client/locale/src/locales/zh.ts @@ -30,6 +30,8 @@ export const zh = { 'more': '更多', 'collapse': '收起', 'expand': '展开', + 'subtitle.expand': '展开{name}的描述', + 'subtitle.collapse': '收起{name}的描述', 'back': '返回', 'brand.localBuild': 'DSH 本地构建', 'workspace.defaultName': '默认工作区', diff --git a/packages/client/ui-agent-preset/src/client/AgentPresetLabel.module.css b/packages/client/ui-agent-preset/src/client/AgentPresetLabel.module.css index f828de9af1..50a1b2b181 100644 --- a/packages/client/ui-agent-preset/src/client/AgentPresetLabel.module.css +++ b/packages/client/ui-agent-preset/src/client/AgentPresetLabel.module.css @@ -19,7 +19,6 @@ .icon { flex: none; - opacity: 0.7; } /* First sacrifice when the title row (the enclosing inline-size container) diff --git a/packages/client/ui-agent-team/README.i18n.yaml b/packages/client/ui-agent-team/README.i18n.yaml new file mode 100644 index 0000000000..dfab72bf26 --- /dev/null +++ b/packages/client/ui-agent-team/README.i18n.yaml @@ -0,0 +1,40 @@ +# Bilingual-pair consistency record for README.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 packages/client/ui-agent-team/README.md +/: + en: cecfe90c9079279f + zh: 58062db454093088 +/deepseek-ai-dsh-client-ui-agent-team: + en: 39c47e6e926425bf + zh: 0fd155c454355e3a +/deepseek-ai-dsh-client-ui-agent-team/summary: + en: 88103cec4c91d755 + zh: 6536e4307a5ce122 +/deepseek-ai-dsh-client-ui-agent-team/table-of-contents: + en: d152484eb41ac6b4 + zh: 09388d293f9be9cb +/deepseek-ai-dsh-client-ui-agent-team/use-this-package: + en: d502ab38773eef34 + zh: e1f9c4edbc74c38f +/deepseek-ai-dsh-client-ui-agent-team/use-this-package/inspect-the-task-board: + en: 7e88fb00a3abd230 + zh: 08efcf80afecb0c0 +/deepseek-ai-dsh-client-ui-agent-team/understand-the-implementation: + en: 9b0b6a8505a9ebbf + zh: beb5d89f80463b3e +/deepseek-ai-dsh-client-ui-agent-team/further-exploration: + en: 4d9214370681b02e + zh: 4acb21b9ecf25e85 +/deepseek-ai-dsh-client-ui-agent-team/model-experience: + en: 47ebeba07d72759d + zh: 8fe8133f6dd845a9 +/deepseek-ai-dsh-client-ui-agent-team/model-experience/kv-cache-effect: + en: b51b320f5e4dacfc + zh: 6695947c3f9fed28 +/deepseek-ai-dsh-client-ui-agent-team/known-limitations-and-deferred-work: + en: 8be0a045f2f42e9e + zh: 36f98d5cc1189f7a +/deepseek-ai-dsh-client-ui-agent-team/known-limitations-and-deferred-work/dev-note: + en: e67f633dc6fe7773 + zh: 3bb0c20c93f16f98 diff --git a/packages/client/ui-agent-team/README.md b/packages/client/ui-agent-team/README.md new file mode 100644 index 0000000000..d156e7b7cb --- /dev/null +++ b/packages/client/ui-agent-team/README.md @@ -0,0 +1,100 @@ +--- +description: "Inspect the shared task board from the Web conversation header." +kind: "package-reference" +--- + +# @deepseek-ai/dsh-client-ui-agent-team + +English | [中文](README.zh.md) + +## Summary + +This package displays shared tasks below the Subagent tree in one header panel. Host projection frames keep task state current without a refresh control. Ordinary subagent navigation owns the member list and teammate conversations. The Web bundle loads this package; it adds no model-facing input. + +## Table of Contents + +- [Use this package](#use-this-package) +- [Understand the implementation](#understand-the-implementation) +- [Further Exploration](#further-exploration) +- [Model Experience](#model-experience) +- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work) +- [Dev Note](#dev-note) + +----- + + +## Use this package + +In the root conversation’s model and reasoning menu, turn on **Enable agent teamwork**. New Sessions default to off. While enabled, open the root conversation’s Subagent control to see members above shared tasks. Child conversations keep subagent navigation without a task board or Team switch, including before their subagent address is discovered. The entry is hidden when both the subagent catalog and shared tasks are empty; catalog and task-record errors remain accessible. Section headers appear only when Team mode is enabled, at least one nondeleted shared task exists, and the task footer is available. In that case, the noncollapsible Subagents section remains above shared tasks even when its catalog is empty. Otherwise, the menu shows only subagent navigation, without section headers, a shared-task module, empty-state copy, or a divider. Turning it off retains tasks and subagents. The entire Team row highlights on hover and toggles the selection when clicked, except for its independent information button. The switch calls `remote.agentTeams.setEnabled`, waits for the saved projection and reports failures through the menu host; the row cannot toggle while locked or saving. The adjacent information button explains shared-task coordination and additional token use on hover, keyboard focus, or click/tap without changing the selection. Escape or an outside press dismisses the explanation. + +Turning off the menu switch requires confirmation after the Session records a model request; a model choice alone does not require it. The switch waits while request history is unavailable. The dialog explains the tool, coordination, and possible cache effects without deleting task records. Cancel, Escape, or closing the dialog leaves Team mode unchanged. Dismissing the dialog returns focus to the model selector. Confirmation targets the originating Session, waits for the save, and keeps failures visible for retry; it does not depend on the model menu remaining open. Team mode has no slash command; menu changes create no `command/run` or `command/done` records. + +Chat hides historical `/team on` and `/team off` receipts with pending or successful outcomes. Failures remain visible with a Users icon, a localized “Agent teamwork” title, and a localized failure prefix followed by the first nonempty line of the recorded reason without translation. Expanding the receipt shows the full original error. Unknown or missing arguments retain a visible receipt. + +The [Web bundle](../../bundle/web-app/README.md) loads this package by default. The Web Client loader mounts the `/client` export; the root Host export is inert, and the package has no user configuration fields. + +### Inspect the task board + +Ready pending tasks use idle, blocked pending tasks use warning, in-progress tasks use ongoing, and completed tasks use done. + +The noncollapsible Shared tasks header uses a Checklist icon and completed, in-progress, and pending counts, with zero counts omitted and wording and separators matching the input task list. Compact read-only rows show the subject, status, and a chevron, with 13px primary titles and hover geometry aligned to subagent rows; expanded details use 12px text. Task rows use 7px vertical padding, 6px horizontal padding, and 7px gaps to align their clickable content with subagent rows. All task details start closed; opening one closes the previous task. Details show only the full description when present, followed by a single-line owner label and Session id in the same color with a 4px gap. Long owner ids are ellipsized and reveal their full text in a tooltip only when clipped. Task-text and owner tooltips appear after 500ms of hover. Every owner is identified by its Session id. The shared menu is 360px wide with 6px outer padding, constrained to fit the viewport. Shared tasks have a 4px top margin rather than a divider. Both section headers keep a 7px icon/title gap and an additional 4px before their counts. Only the menu scrolls; task lists and details have no nested scrolling. Agents mutate tasks through tools; the panel provides no mutation controls. A projection failure appears separately above any retained tasks and remains visible when no tasks exist. + +----- + + +## Understand the implementation + +
+Implementation internals — click to expand + +The Client export registers its locale dictionaries and subagent-catalog and model-menu slots through Cordis effects; it mounts no Remote namespace. Disposing the plugin fiber removes its registrations. + +The Subagent catalog owns the shared panel, scrolling, hover, keyboard, and dismissal interactions. This package contributes the task section only below the root conversation’s tree. The board reads the current Session’s mode and tasks through `useProjection('agentTeam')`. An available, enabled projection supplies the task section only when it contains nondeleted tasks; a projection failure can render independently without a task section. Task IDs, dependencies, readiness, write scopes, and write scope warnings remain in task data but are not displayed as detail fields; blocked pending tasks still use the warning glyph. + +The [Chat visibility policy](../ui-chat/src/client/contract/chat-visibility.ts) hides only historical commands named `team` whose trimmed recorded arguments are exactly `on` or `off` and whose outcome is pending or successful. The remaining receipts use a localized running summary or preserve raw successful output, with a localized completed fallback when that text is empty. Rendering neither reads the current Team projection nor parses English outcome text. Filtering and localization affect only Chat presentation: command records, logs, trajectory, and raw errors remain unchanged. + +| File | Role | +|---|---| +| [`src/client/mount.ts`](src/client/mount.ts) | Locale, catalog-section, and model-menu registrations | +| [`src/client/TeamBoard.tsx`](src/client/TeamBoard.tsx) | Projection-derived task section | +| [`src/client/locales.ts`](src/client/locales.ts) | Panel and command copy in eight languages | +| [`src/index.ts`](src/index.ts) | Inert Host entry | + +
+ +----- + + +## Further Exploration + +- [Web bundle](../../bundle/web-app/README.md) — mounts the Client plugin by default. +- [Agent Teams service](../../team/agent-team/README.md) — authoritative task and projection behavior. +- [Conversation UI](../ui-conversation/README.md) — the stable header slot and addressed-subagent navigation surface. + +----- + + +## Model Experience + +None, as this browser projection registers no model-facing input. + +#### KV Cache effect + +No direct effect; the Team tools and ordinary conversation submission own any later model-visible use. + +## Known Limitations and Deferred Work + + + +- **No mailbox timeline** — the panel displays tasks, not messages. +- **No lifecycle or workspace controls** — the panel cannot spawn, rename, delete, or interrupt teammates, and write scopes remain advisory metadata. + + +### Dev Note + +
+Working context for maintainers — click to expand + +None. + +
diff --git a/packages/client/ui-agent-team/README.zh.md b/packages/client/ui-agent-team/README.zh.md new file mode 100644 index 0000000000..6a370c6eeb --- /dev/null +++ b/packages/client/ui-agent-team/README.zh.md @@ -0,0 +1,100 @@ +--- +description: "从 Web 对话标题栏查看共享任务板。" +kind: "package-reference" +--- + +# @deepseek-ai/dsh-client-ui-agent-team + +[English](README.md) | 中文 + +## 概述 + +此包在同一个页头面板中,将共享任务显示在 Subagent 树下方。Host 投影帧持续更新任务状态,无需刷新控件。普通 subagent(子智能体)导航负责成员列表和队友对话。Web bundle 默认加载此包;它不添加模型可见输入。 + +## 目录 + +- [使用本包](#use-this-package) +- [理解实现](#understand-the-implementation) +- [进一步探索](#further-exploration) +- [模型体验](#model-experience) +- [已知限制与延期工作](#known-limitations-and-deferred-work) +- [开发备注](#dev-note) + +----- + + +## 使用本包 + +在根会话的模型与推理等级菜单中打开**启用智能体团队协作**。新 Session 默认关闭。开启后,通过根会话的 Subagent 入口同时查看上方的成员树和下方的共享任务。子会话保留子智能体导航,不显示任务板或 Team 开关,包括尚未发现子代理地址时。子智能体目录和共享任务都为空时隐藏入口;目录或任务记录错误仍保留查看入口。仅当 Team 模式已开启、存在至少一个未删除的共享任务且任务 footer 可用时,才显示区域标题。此时即使目录为空,共享任务上方仍保留不可折叠的“子智能体”区域。其他情况下,菜单仅显示 subagent 导航,不显示区域标题、共享任务模块、空状态文案或分隔线。关闭后保留任务和子代理。团队设置整行在悬停时高亮,点击可切换状态,但独立的信息按钮除外。开关调用 `remote.agentTeams.setEnabled`,等待保存后的投影更新,失败由菜单宿主提示;锁定或保存中不能通过整行切换。旁边的信息按钮可通过悬停、键盘聚焦或点击/轻触查看共享任务协作与额外 token 消耗的说明,不改变开关状态。按 Escape 或点击外部可关闭说明。 + +会话记录过模型请求后,从菜单开关停用团队需要确认;仅选择模型不会触发确认。请求历史尚未加载时,开关暂不可操作。弹窗说明工具、协作与可能的缓存影响,不会删除任务记录。取消、按 Escape 或关闭弹窗都保持团队状态不变。弹窗关闭后,焦点回到模型选择按钮。确认操作绑定到最初的会话,等待保存完成,失败时保留错误供重试,不依赖模型菜单保持打开。Team 模式没有斜杠命令;菜单切换不会生成 `command/run` 或 `command/done` 记录。 + +Chat 隐藏历史 `/team on` 和 `/team off` 记录中尚未完成或执行成功的回执。失败回执仍显示 Users 图标、本地化的“智能体团队协作”标题,以及本地化的失败前缀和已记录原因的首个非空行,不翻译原因原文。展开回执可查看完整原始错误。参数无法识别或缺失时仍显示回执。 + +[Web bundle](../../bundle/web-app/README.zh.md) 默认加载本包。Web Client loader 挂载 `/client` export;root Host export 不执行行为,本包也没有用户配置字段。 + +### 查看任务板 + +可开始的 pending 任务使用 idle 灰点,被依赖阻塞的 pending 任务使用 warning 橙点,in-progress 任务使用 ongoing loading,completed 任务使用 done 绿点。 + +不可折叠的“共享任务”标题使用 Checklist 图标,并显示已完成、进行中和待处理数量;省略零值,文案和分隔符与输入区任务列表一致。紧凑只读行显示主题、状态和展开箭头,13px 主标题与悬停区域对齐子智能体行;展开详情使用 12px 文字。任务行使用 7px 垂直内边距、6px 水平内边距和 7px 间距,使可点击内容与 subagent 行对齐。所有任务详情初始关闭,打开一项会关闭上一项。详情仅显示完整描述(如有),下方以单行显示负责人标签和 Session id,两者使用相同颜色,间距为 4px。过长的负责人 id以省略号截断,仅在被截断时通过工具提示显示完整内容。任务文字和负责人的工具提示在悬停 500ms 后显示。每个负责人均通过其 Session id 标识。共享菜单宽度为 360px,外层内边距为 6px,并受视口可用宽度约束。共享任务上方保留 4px 外边距,不显示分隔线。两个区域标题均保留 7px 图标与文字间距,并在统计数量前额外留出 4px。仅菜单本身滚动,任务列表与详情不单独滚动。Agent 通过工具修改任务;面板不提供修改控件。投影失败以独立警告显示在保留的任务上方,即使没有任务也保持可见。 + +----- + + +## 理解实现 + +
+实现细节——点击展开 + +Client export 通过 Cordis effect 注册语言字典、子代理目录与模型菜单 slot;它不挂载 Remote namespace。销毁插件 fiber 会移除这些注册。 + +Subagent 目录负责共享面板的滚动、悬停、键盘和关闭交互。本包仅在根会话的树下方提供任务区。任务板通过 `useProjection('agentTeam')` 读取当前 Session 的模式与任务;仅当投影可用、已开启且含有未删除的任务时才显示任务区;投影错误可独立显示,不需要任务区。任务 ID、依赖、准备状态、写入范围及其提示仍保留在任务数据中,但不作为详情字段显示;被依赖阻塞的 pending 任务仍使用 warning 图标。 + +[Chat 可见性策略](../ui-chat/src/client/contract/chat-visibility.ts) 仅隐藏名称为 `team`、已记录参数去除首尾空白后恰为 `on` 或 `off`,且尚未完成或执行成功的历史命令。其余回执在执行中显示本地化摘要,成功时保留原始结果文本,文本为空则使用本地化的完成提示。渲染既不读取当前 Team 投影,也不解析英文结果文案。过滤与本地化仅影响 Chat 展示:命令记录、日志、轨迹和原始错误均保持不变。 + +| 文件 | 职责 | +|---|---| +| [`src/client/mount.ts`](src/client/mount.ts) | 语言字典、目录任务区和模型菜单注册 | +| [`src/client/TeamBoard.tsx`](src/client/TeamBoard.tsx) | 从投影派生的任务区 | +| [`src/client/locales.ts`](src/client/locales.ts) | 八种语言的面板与命令文案 | +| [`src/index.ts`](src/index.ts) | 不执行行为的 Host entry | + +
+ +----- + + +## 进一步探索 + +- [Web bundle](../../bundle/web-app/README.zh.md)——默认挂载本 Client plugin。 +- [Agent Teams 服务](../../team/agent-team/README.zh.md) — 任务和投影行为的权威来源。 +- [会话 UI](../ui-conversation/README.zh.md)——稳定 header slot 与 addressed-subagent 导航表层。 + +----- + + +## 模型体验 + +无直接影响,因为该浏览器 projection 不注册面向模型的输入。 + +#### KV Cache 影响 + +无直接影响;Team 工具与普通会话提交负责后续任何模型可见用途。 + +## 已知限制与延期工作 + + + +- **无邮箱时间线** — 面板显示任务,不显示消息。 +- **没有 lifecycle 或 workspace control**——panel 不能 spawn、rename、delete 或 interrupt teammate,write scope 仍只是提示性 metadata。 + + +### 开发备注 + +
+维护者的工作上下文——点击展开 + +无。 + +
diff --git a/packages/experimental/client-ui-agent-team/locale/de.json b/packages/client/ui-agent-team/locale/de.json similarity index 100% rename from packages/experimental/client-ui-agent-team/locale/de.json rename to packages/client/ui-agent-team/locale/de.json diff --git a/packages/client/ui-agent-team/locale/en.json b/packages/client/ui-agent-team/locale/en.json new file mode 100644 index 0000000000..c6667a6afb --- /dev/null +++ b/packages/client/ui-agent-team/locale/en.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Task board", + "description": "View shared tasks for the subagent tree." + } +} diff --git a/packages/experimental/client-ui-agent-team/locale/es.json b/packages/client/ui-agent-team/locale/es.json similarity index 100% rename from packages/experimental/client-ui-agent-team/locale/es.json rename to packages/client/ui-agent-team/locale/es.json diff --git a/packages/experimental/client-ui-agent-team/locale/fr.json b/packages/client/ui-agent-team/locale/fr.json similarity index 100% rename from packages/experimental/client-ui-agent-team/locale/fr.json rename to packages/client/ui-agent-team/locale/fr.json diff --git a/packages/client/ui-agent-team/locale/ja.json b/packages/client/ui-agent-team/locale/ja.json new file mode 100644 index 0000000000..ae63e83f31 --- /dev/null +++ b/packages/client/ui-agent-team/locale/ja.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "タスクボード", + "description": "サブエージェントツリーの共有タスクを表示します。" + } +} diff --git a/packages/experimental/client-ui-agent-team/locale/pt.json b/packages/client/ui-agent-team/locale/pt.json similarity index 100% rename from packages/experimental/client-ui-agent-team/locale/pt.json rename to packages/client/ui-agent-team/locale/pt.json diff --git a/packages/experimental/client-ui-agent-team/locale/ru.json b/packages/client/ui-agent-team/locale/ru.json similarity index 100% rename from packages/experimental/client-ui-agent-team/locale/ru.json rename to packages/client/ui-agent-team/locale/ru.json diff --git a/packages/client/ui-agent-team/locale/zh.json b/packages/client/ui-agent-team/locale/zh.json new file mode 100644 index 0000000000..9d06145f0f --- /dev/null +++ b/packages/client/ui-agent-team/locale/zh.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "任务板", + "description": "查看子智能体树的共享任务。" + } +} diff --git a/packages/experimental/client-ui-agent-team/package.json b/packages/client/ui-agent-team/package.json similarity index 71% rename from packages/experimental/client-ui-agent-team/package.json rename to packages/client/ui-agent-team/package.json index 17963f6de8..271137193a 100644 --- a/packages/experimental/client-ui-agent-team/package.json +++ b/packages/client/ui-agent-team/package.json @@ -1,6 +1,6 @@ { - "name": "@deepseek-ai/dsh-experimental-client-ui-agent-team", - "description": "Web Agent Teams roster, task board, and teammate navigation", + "name": "@deepseek-ai/dsh-client-ui-agent-team", + "description": "Web shared task board for a subagent tree", "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" @@ -8,7 +8,7 @@ "repository": { "type": "git", "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", - "directory": "packages/experimental/client-ui-agent-team" + "directory": "packages/client/ui-agent-team" }, "type": "module", "main": "lib/index.js", @@ -32,8 +32,9 @@ "@deepseek-ai/dsh-api-session-controller", "@deepseek-ai/dsh-client-locale", "@deepseek-ai/dsh-client-ui-conversation", - "@deepseek-ai/dsh-client-ui-primitives", - "@deepseek-ai/dsh-client-ui-workspace" + "@deepseek-ai/dsh-client-ui-chat", + "@deepseek-ai/dsh-client-ui-layout", + "@deepseek-ai/dsh-client-ui-primitives" ], "platform": "web" } @@ -51,18 +52,23 @@ "@deepseek-ai/dsh-client-locale": "workspace:*", "@deepseek-ai/dsh-client-store": "workspace:*", "@deepseek-ai/dsh-client-test-runtime": "workspace:*", + "@deepseek-ai/dsh-commands": "workspace:*", "@deepseek-ai/dsh-client-ui-conversation": "workspace:*", + "@deepseek-ai/dsh-client-ui-chat": "workspace:*", + "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/dsh-client-ui-primitives": "workspace:*", "@deepseek-ai/dsh-client-ui-renderer": "workspace:*", - "@deepseek-ai/dsh-client-ui-session": "workspace:*", "@deepseek-ai/dsh-client-ui-slots": "workspace:*", - "@deepseek-ai/dsh-client-ui-workspace": "workspace:*", - "@deepseek-ai/dsh-experimental-agent-team": "workspace:*", + "@deepseek-ai/dsh-agent-team": "workspace:*", "@deepseek-ai/dsh-session": "workspace:*", "@testing-library/react": "^16.3.3", "@deepseek-ai/cordis": "workspace:~", "react": "^18.2.0", "react-dom": "^18.2.0", + "@deepseek-ai/dsh-client-ui-model-selection": "workspace:*", + "@deepseek-ai/dsh-client-ui-layout": "workspace:*", + "@deepseek-ai/dsh-api-remotes": "workspace:*", + "@deepseek-ai/dsh-client-ui-subagent": "workspace:*", "@deepseek-ai/dsh-agent": "workspace:*" }, "files": [ diff --git a/packages/client/ui-agent-team/src/client/TeamBoard.module.css b/packages/client/ui-agent-team/src/client/TeamBoard.module.css new file mode 100644 index 0000000000..3d10436a16 --- /dev/null +++ b/packages/client/ui-agent-team/src/client/TeamBoard.module.css @@ -0,0 +1,165 @@ +.board { + min-width: 0; + padding: 8px 7px; +} + +.board[data-with-members='true'] { + margin-top: 4px; +} + +.header { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 4px 7px; + margin: 0 0 4px; + padding: 0; +} + +.lead, +.glyph, +.chevron { + display: grid; + flex: none; + place-items: center; + color: var(--dsw-alias-label-tertiary); +} + +.title { + flex: none; + color: var(--dsw-alias-label-primary); + font-size: 13px; + font-weight: 500; + line-height: 24px; +} + +.progress { + margin-inline-start: 4px; + color: var(--dsw-alias-label-tertiary); + font-size: 13px; + font-weight: 400; + font-variant-numeric: tabular-nums; + line-height: 20px; + overflow-wrap: anywhere; +} + +.tasks { + display: flex; + flex-direction: column; + margin: 0; + padding: 0; + list-style: none; +} + +.task { + min-width: 0; +} + +.taskRow { + box-sizing: border-box; + display: flex; + align-items: center; + gap: 7px; + width: calc(100% + 12px); + min-width: 0; + margin: 0 -6px; + padding: 7px 6px; + border: 0; + border-radius: var(--dsw-radius-md); + background: transparent; + color: var(--dsw-alias-label-primary); + font: inherit; + font-size: 13px; + line-height: 17px; + text-align: left; + cursor: pointer; +} + +.taskRow:focus-visible { + outline: none; + background: var(--dsw-alias-interactive-bg-hover); +} + +@media (hover: hover) { + .taskRow:hover { + background: var(--dsw-alias-interactive-bg-hover); + } +} + +.glyph { + width: 14px; + height: 17px; +} + +.subject { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.task[data-expanded='true'] .taskRow { + align-items: flex-start; +} + +.task[data-expanded='true'] .subject { + overflow: visible; + overflow-wrap: anywhere; + white-space: pre-wrap; +} + +.task[data-expanded='true'] .chevron { + transform: rotate(180deg); +} + +.chevron { + width: 28px; + height: 17px; +} + +.details { + padding: 0 0 8px 21px; + color: var(--dsw-alias-label-secondary); + font-size: 12px; + line-height: 18px; + overflow-wrap: anywhere; +} + +.description { + margin: 0; + white-space: pre-wrap; +} + +.owner { + display: flex; + gap: 4px; + min-width: 0; + margin: 0; + white-space: nowrap; +} + +.description + .owner { + margin-top: 6px; +} + +.ownerLabel { + flex: none; +} + +.ownerValue { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; +} + +.error { + display: flex; + align-items: flex-start; + gap: 7px; + padding: 8px 0; + color: var(--dsw-alias-state-error-primary); + font-size: 12px; + line-height: 18px; + overflow-wrap: anywhere; +} diff --git a/packages/client/ui-agent-team/src/client/TeamBoard.tsx b/packages/client/ui-agent-team/src/client/TeamBoard.tsx new file mode 100644 index 0000000000..dbc554d33c --- /dev/null +++ b/packages/client/ui-agent-team/src/client/TeamBoard.tsx @@ -0,0 +1,112 @@ +import { useState } from 'react' +import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type { TeamTaskView, TeamTaskId } from '@deepseek-ai/dsh-agent-team/client' +import type {} from '@deepseek-ai/dsh-api-session-controller/client' +import { + IconChecklistOutlineRegular, IconChevronDownOutlineRegular, + StateDot, Tooltip, type StateDotState, +} from '@deepseek-ai/dsh-client-ui-primitives' +import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' +import type {} from '@deepseek-ai/dsh-client-ui-subagent/client' +import type {} from '@deepseek-ai/dsh-client-ui-conversation/client' +import { NS, type TeamKey } from './locales.ts' +import css from './TeamBoard.module.css' + +/** Full props of the task board below the shared subagent catalog. */ +export type TeamBoardProps = + PropsRuntime<'conversation.session.header.actions.subagent.footer'> & PropsLocale + +type VisibleTask = TeamTaskView & { status: Exclude } + +const statusKeys = { + pending: 'status.pending', in_progress: 'status.in_progress', completed: 'status.completed', +} satisfies Record + +const dotStates = { + pending: 'idle', in_progress: 'ongoing', completed: 'done', +} satisfies Record + +function progressLabel(tasks: readonly VisibleTask[], t: TeamBoardProps['t']): string { + const completed = tasks.filter(task => task.status === 'completed').length + const active = tasks.filter(task => task.status === 'in_progress').length + const pending = tasks.length - completed - active + return [ + ...completed > 0 ? [t('progress.completed', { count: completed })] : [], + ...active > 0 ? [t('progress.active', { count: active })] : [], + ...pending > 0 ? [t('progress.pending', { count: pending })] : [], + ].join('\u2002·\u2002') +} + +function TaskRow({ task, rootId, expanded, onToggle, t }: Pick & { + task: VisibleTask + rootId: SessionId + expanded: boolean + onToggle: () => void +}) { + const owner = task.ownerId ?? t('unowned') + const blocked = task.status === 'pending' && !task.ready + const detailsId = `team-task-${rootId}-${task.id}` + return ( +
  • + + + + {expanded && ( +
    + {task.description &&

    {task.description}

    } +

    + {t('owner')}: + + {owner} + +

    +
    + )} +
  • + ) +} + +/** Render read-only tasks in source order; the enclosing subagent menu owns scrolling. */ +export function TeamBoard({ sessionId, useProjection, useSessions, t }: TeamBoardProps) { + const team = useProjection('agentTeam') + const hasMembers = useSessions(state => (state.projectionsBySession[sessionId]?.values.subagentCatalog?.length ?? 0) > 0) + const [expandedTaskId, setExpandedTaskId] = useState(null) + if (!team?.enabled) return null + const tasks = team.tasks.filter((task): task is VisibleTask => task.status !== 'deleted') + const failure = team.failure === undefined ? null : ( +
    {t('failure', { message: team.failure })}
    + ) + if (tasks.length === 0) return failure === null ? null :
    {failure}
    + return ( +
    +

    + + {t('tasks')} + {progressLabel(tasks, t)} +

    + {failure} +
      + {tasks.map(task => ( + { setExpandedTaskId(current => current === task.id ? null : task.id) }} + t={t} + /> + ))} +
    +
    + ) +} diff --git a/packages/client/ui-agent-team/src/client/TeamCommandRow.module.css b/packages/client/ui-agent-team/src/client/TeamCommandRow.module.css new file mode 100644 index 0000000000..a36f5ff903 --- /dev/null +++ b/packages/client/ui-agent-team/src/client/TeamCommandRow.module.css @@ -0,0 +1,44 @@ +.root { + display: flex; + flex-direction: column; +} + +.title { font-weight: 400; } + +.separator { + flex: none; + width: 2px; + height: 2px; + margin: 0 8px; + border-radius: 1px; + background: var(--dsw-alias-label-caption); +} + +.summary { + min-width: 0; + overflow: hidden; + flex: 1 1 auto; + font-size: var(--dsh-content-font-size-secondary, 13px); + line-height: calc(24px + var(--dsh-content-font-delta, 0px)); + text-overflow: ellipsis; + white-space: nowrap; +} + +.summary[data-error], +.body[data-error] { + color: var(--dsw-alias-state-error-primary); +} + +.body { + max-height: 260px; + margin: 4px 0 4px 4px; + padding: 12px 16px; + overflow: auto; + border: 0.5px solid var(--dsw-alias-border-l1); + border-radius: var(--dsw-radius-lg); + background: var(--dsw-alias-markdown-code-block); + color: var(--dsw-alias-label-primary); + font: var(--dsw-font-markdown-code-block-small); + white-space: pre-wrap; + overflow-wrap: anywhere; +} diff --git a/packages/client/ui-agent-team/src/client/TeamCommandRow.tsx b/packages/client/ui-agent-team/src/client/TeamCommandRow.tsx new file mode 100644 index 0000000000..dde0b5cced --- /dev/null +++ b/packages/client/ui-agent-team/src/client/TeamCommandRow.tsx @@ -0,0 +1,65 @@ +/** Read-only historical /team failures and fallback output. */ +import { useState } from 'react' +import { DisclosureRow, IconUsersOutlineRegular, TextShimmer } from '@deepseek-ai/dsh-client-ui-primitives' +import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' +import type {} from '@deepseek-ai/dsh-client-ui-chat/client' +import { assertNever } from '@deepseek-ai/dsh-util-values' +import type { NS } from './locales.ts' +import css from './TeamCommandRow.module.css' + +/** Framework command data and the owning Team dictionary. */ +export type TeamCommandRowProps = PropsRuntime<'conversation.chat.commandview'> & PropsLocale + +const TEAM_ICON = + +function commandSummary(node: TeamCommandRowProps['node'], t: TeamCommandRowProps['t']): string { + const outcome = node.outcome + if (outcome === null) return t('command.running') + switch (outcome.kind) { + case 'success': + return outcome.text || t('command.done') + case 'error': { + const reason = outcome.text?.trim().split(/\r?\n/u)[0] + return reason ? t('command.failedReason', { reason }) : t('command.failed') + } + /* v8 ignore next -- CommandNode restricts outcomes to success or error. */ + default: return assertNever(outcome.kind) + } +} + +/** + * Render the recorded Team command with the shared agent glyph and disclosure. + * @param props - command record and localized Team text. + * @returns an outcome row; recorded failures and multiline output remain expandable. + */ +export function TeamCommandRow({ node, t }: Pick) { + const [expanded, setExpanded] = useState(false) + const running = node.outcome === null + const failed = node.outcome?.kind === 'error' + const summary = commandSummary(node, t) + const body = node.outcome?.text + const expandable = body !== undefined && body !== '' && (failed || body.includes('\n')) + return ( +
    + { setExpanded(value => !value) }} + collapsedContent={( + <> + + {summary} + + )} + > +
    {body}
    +
    +
    + ) +} diff --git a/packages/client/ui-agent-team/src/client/TeamDisableConfirmation.module.css b/packages/client/ui-agent-team/src/client/TeamDisableConfirmation.module.css new file mode 100644 index 0000000000..5763edbcbc --- /dev/null +++ b/packages/client/ui-agent-team/src/client/TeamDisableConfirmation.module.css @@ -0,0 +1,31 @@ +.dialog { + width: min(440px, 100%); + max-height: 100%; +} + +.content { + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; +} + +.copy { + min-width: 0; + overflow-wrap: anywhere; + color: var(--dsw-alias-label-secondary); + font-size: 14px; + line-height: 22px; +} + +.copy p { + margin: 0; +} + +.copy p + p { + margin-top: 12px; +} + +.error { + color: var(--dsw-alias-state-error-primary); + overflow-wrap: anywhere; +} diff --git a/packages/client/ui-agent-team/src/client/TeamDisableConfirmation.tsx b/packages/client/ui-agent-team/src/client/TeamDisableConfirmation.tsx new file mode 100644 index 0000000000..e02bc87ba3 --- /dev/null +++ b/packages/client/ui-agent-team/src/client/TeamDisableConfirmation.tsx @@ -0,0 +1,76 @@ +/** Session-targeted confirmation that outlives the model menu which opened it. */ +import { useState } from 'react' +import type { SessionId } from '@deepseek-ai/dsh-session/types' +import { Button, Modal } from '@deepseek-ai/dsh-client-ui-primitives' +import type { HostObservable, InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' +import type {} from '@deepseek-ai/dsh-client-ui-layout/client' +import type { NS } from './locales.ts' +import css from './TeamDisableConfirmation.module.css' + +/** The originating Session remains the target while the dialog is open. */ +export interface TeamDisableRequest { + sessionId: SessionId +} + +/** Confirmation request and Session-bound selection callbacks supplied by the plugin. */ +export interface TeamDisableConfirmationInjected { + hooks: { disableRequest: HostObservable } + cancelDisable: (sessionId: SessionId) => void + disableTeam: (sessionId: SessionId) => Promise +} + +/** Root overlay props, independent of the model menu lifetime. */ +export type TeamDisableConfirmationProps = PropsRuntime<'shell.overlay'> + & InjectFace & PropsLocale + +/** + * Render the requested confirmation outside the model menu. + * @param props - request observation, command callbacks, and localized text. + * @returns the confirmation, or nothing when no request is pending. + */ +export function TeamDisableConfirmation({ useDisableRequest, ...props }: TeamDisableConfirmationProps) { + const request = useDisableRequest(value => value) + if (request === null) return null + return +} + +/** One confirmation keeps its failure visible until retry or cancellation. */ +function DisableConfirmationForm({ sessionId, cancelDisable, disableTeam, t }: { + sessionId: SessionId + cancelDisable: TeamDisableConfirmationInjected['cancelDisable'] + disableTeam: TeamDisableConfirmationInjected['disableTeam'] + t: TeamDisableConfirmationProps['t'] +}) { + const [state, setState] = useState<{ kind: 'ready' } | { kind: 'saving' } | { kind: 'failed'; message: string }>({ kind: 'ready' }) + const saving = state.kind === 'saving' + const cancel = (): void => { + if (!saving) cancelDisable(sessionId) + } + const confirm = (): void => { + if (saving) return + setState({ kind: 'saving' }) + void disableTeam(sessionId).then((failure) => { + if (failure === null) cancelDisable(sessionId) + else setState({ kind: 'failed', message: failure }) + }, (error: unknown) => { + setState({ kind: 'failed', message: error instanceof Error ? error.message : String(error) }) + }) + } + return ( + + + + + )} + > +
    +

    {t('disable.description')}

    +

    {t('disable.warning')}

    +
    + {state.kind === 'failed' &&

    {t('mode.failed', { message: state.message })}

    } +
    + ) +} diff --git a/packages/client/ui-agent-team/src/client/TeamModeToggle.module.css b/packages/client/ui-agent-team/src/client/TeamModeToggle.module.css new file mode 100644 index 0000000000..75eedfb3e7 --- /dev/null +++ b/packages/client/ui-agent-team/src/client/TeamModeToggle.module.css @@ -0,0 +1,52 @@ +.row { + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + min-height: 34px; + padding: 0 8px; + border-radius: var(--dsw-radius-md); + color: var(--dsw-alias-label-primary); + font-size: 13px; + line-height: 20px; +} + +.label { + display: inline-flex; + align-items: center; + gap: 4px; + min-width: 0; +} + +.infoButton { + position: relative; + z-index: 1; + flex: none; + width: 20px; + height: 20px; + padding: 0; + color: var(--dsw-alias-label-caption); +} + +/* Extend the native switch hit target without nesting the information button. */ +.row .toggle { + position: static; +} + +.toggle::after { + content: ''; + position: absolute; + inset: 0; + border-radius: var(--dsw-radius-md); +} + +.row:has(.toggle:focus-visible) { + background: var(--dsw-alias-interactive-bg-hover); +} + +@media (hover: hover) { + .row:has(.toggle:not(:disabled)):hover { + background: var(--dsw-alias-interactive-bg-hover); + } +} diff --git a/packages/client/ui-agent-team/src/client/TeamModeToggle.tsx b/packages/client/ui-agent-team/src/client/TeamModeToggle.tsx new file mode 100644 index 0000000000..cd0d3dcafa --- /dev/null +++ b/packages/client/ui-agent-team/src/client/TeamModeToggle.tsx @@ -0,0 +1,70 @@ +/** Session-owned Agent Teams selection in the model and reasoning menu. */ + +import { useState } from 'react' +import { Button, IconInfoOutlineRegular, Switch, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives' +import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' +import type {} from '@deepseek-ai/dsh-client-ui-model-selection/client' +import type {} from '@deepseek-ai/dsh-agent-team/client' +import type {} from '@deepseek-ai/dsh-agent' +import type { NS } from './locales.ts' +import css from './TeamModeToggle.module.css' + +/** Session selection callbacks supplied by the Client plugin. */ +export interface TeamModeToggleInjected { + /** Save the selection; null means success, otherwise the string describes the failure. */ + selectTeamMode: (enabled: boolean) => Promise + /** Open a Session-targeted confirmation outside this menu. */ + requestDisableConfirmation: () => void +} + +/** Full props of the model-menu Team switch. */ +export type TeamModeToggleProps = PropsRuntime<'conversation.input.model.options'> + & PropsLocale & TeamModeToggleInjected + +/** + * Render the saved selection without an optimistic update. + * @param props - Session projection, menu state, localized copy, and selection callback. + * @returns the Team switch when the capability projection is available. + */ +export function TeamModeToggle(props: TeamModeToggleProps) { + const addressedChild = props.useSession(snapshot => snapshot.subagent !== null) + const listedChild = props.useSessions(state => state.byId[props.sessionId]?.origin === 'subagent') + if (addressedChild || listedChild) return null + return +} + +/** Read and change the selection only for root Sessions. */ +function RootTeamModeToggle({ + useProjection, locked, dismiss, reportFailure, selectTeamMode, requestDisableConfirmation, t, +}: TeamModeToggleProps) { + const mode = useProjection('agentTeam') + const modelSelection = useProjection('modelSelection') + const [pending, setPending] = useState(false) + if (mode === undefined) return null + const select = (enabled: boolean): void => { + if (!enabled && modelSelection !== undefined && modelSelection.lastUsed !== null) { + dismiss() + requestDisableConfirmation() + return + } + setPending(true) + void selectTeamMode(enabled).then((failure) => { + if (failure !== null) reportFailure(t('mode.failed', { message: failure })) + }, (error: unknown) => { + reportFailure(t('mode.failed', { message: error instanceof Error ? error.message : String(error) })) + }).finally(() => { setPending(false) }) + } + return ( +
    + + {t('mode.label')} + + + + + +
    + ) +} diff --git a/packages/experimental/client-ui-agent-team/src/client/index.ts b/packages/client/ui-agent-team/src/client/index.ts similarity index 60% rename from packages/experimental/client-ui-agent-team/src/client/index.ts rename to packages/client/ui-agent-team/src/client/index.ts index a4896c82af..23de9e7ee5 100644 --- a/packages/experimental/client-ui-agent-team/src/client/index.ts +++ b/packages/client/ui-agent-team/src/client/index.ts @@ -1,14 +1,14 @@ -/** Browser entry registering the Agent Teams conversation-header action. */ +/** Browser entry registering the task-board sections in the subagent catalog. */ import type { Context as ClientContext } from '@deepseek-ai/cordis' import { registerAgentTeamUi } from './mount.ts' export { inject } from './mount.ts' -export type { TeamActionInjected, TeamActionProps } from './TeamAction.tsx' +export type { TeamBoardProps } from './TeamBoard.tsx' export type { TeamKey } from './locales.ts' /** - * Register the Team locale dictionaries and header action on the Client Context. + * Register the task-board locale dictionaries and catalog sections on the Client Context. * @param ctx - Client Context with the declared `inject` services available. */ export function apply(ctx: ClientContext): void { diff --git a/packages/client/ui-agent-team/src/client/locales.ts b/packages/client/ui-agent-team/src/client/locales.ts new file mode 100644 index 0000000000..0c5b29a4fb --- /dev/null +++ b/packages/client/ui-agent-team/src/client/locales.ts @@ -0,0 +1,279 @@ +/** Agent teamwork Web dictionaries. */ + +/** Locale namespace owned by the Agent teamwork Web UI. */ +export const NS = 'agent-team' + +/** Simplified Chinese dictionary and key source. */ +export const zh = { + 'progress.completed': '{count} 已完成', + 'progress.active': '{count} 进行中', + 'progress.pending': '{count} 待处理', + 'command.title': '智能体团队协作', + 'command.running': '执行中', + 'command.done': '已完成', + 'command.failed': '执行失败', + 'command.failedReason': '执行失败:{reason}', + trigger: '任务板', + 'mode.label': '启用智能体团队协作', + 'mode.infoLabel': '关于智能体团队协作', + 'mode.description': '通过共享任务板协调多个智能体的分工与进度,复杂任务表现更佳,token 开销也相应更高。', + 'mode.failed': '无法确认智能体团队协作设置已保存,请重试:{message}', + 'disable.title': '确认停用智能体团队协作?', + 'disable.description': '对话过程中停用智能体团队协作,将停止共享任务工具和团队协调指令。', + 'disable.warning': '停用后可能影响缓存复用、增加后续 token 用量与开销,并影响当前任务的协作效果。', + 'disable.cancel': '取消', + 'disable.confirm': '确认停用', + 'disable.close': '关闭', + loading: '正在加载任务板…', + unavailable: '任务板暂不可用', + failure: '任务记录无效:{message}', + empty: '暂无共享任务,可以通过对话创建', + tasks: '共享任务', + owner: '负责人', + unowned: '未分配', + blocked: '被依赖阻塞', + 'status.pending': '待处理', + 'status.in_progress': '进行中', + 'status.completed': '已完成', +} satisfies Record + +/** Agent teamwork locale key union. */ +export type TeamKey = keyof typeof zh + +/** English dictionary checked against the Chinese key set. */ +export const en = { + 'progress.completed': '{count} completed', + 'progress.active': '{count} in progress', + 'progress.pending': '{count} pending', + 'command.title': 'Agent teamwork', + 'command.running': 'Running', + 'command.done': 'Completed', + 'command.failed': 'Failed', + 'command.failedReason': 'Failed: {reason}', + trigger: 'Task board', + 'mode.label': 'Enable agent teamwork', + 'mode.infoLabel': 'About agent teamwork', + 'mode.description': 'Coordinate work and progress across agents with a shared task board for better results on complex tasks, with correspondingly higher token costs.', + 'mode.failed': 'Could not confirm the agent teamwork setting was saved. Try again: {message}', + 'disable.title': 'Disable agent teamwork?', + 'disable.description': 'Disabling agent teamwork during a conversation stops the use of shared task tools and team coordination instructions.', + 'disable.warning': 'Disabling it may affect cache reuse, increase subsequent token usage and costs, and affect collaboration on the current task.', + 'disable.cancel': 'Cancel', + 'disable.confirm': 'Disable', + 'disable.close': 'Close', + loading: 'Loading task board…', + unavailable: 'Task board is unavailable', + failure: 'Invalid persisted task record: {message}', + empty: 'No shared tasks yet. Create them through the conversation.', + tasks: 'Shared tasks', + owner: 'Owner', + unowned: 'Unowned', + blocked: 'Blocked by dependencies', + 'status.pending': 'Pending', + 'status.in_progress': 'In progress', + 'status.completed': 'Completed', +} satisfies Record + +/** Japanese copy, checked against the English keys. */ +export const ja = { + 'progress.completed': '{count}件完了', + 'progress.active': '{count}件進行中', + 'progress.pending': '{count}件保留中', + 'command.title': 'エージェント連携', + 'command.running': '実行中', + 'command.done': '完了', + 'command.failed': '実行失敗', + 'command.failedReason': '実行失敗: {reason}', + 'mode.label': 'エージェント連携を有効にする', + 'mode.infoLabel': 'エージェント連携について', + 'mode.description': '共有タスクボードで複数のエージェントの分担と進捗を調整します。複雑なタスクでより良い結果が得られますが、その分トークンのコストも高くなります。', + 'mode.failed': 'エージェント連携の設定が保存されたことを確認できませんでした。再試行してください: {message}', + 'disable.title': 'エージェント連携を無効にしますか?', + 'disable.description': '会話中にエージェント連携を無効にすると、共有タスクツールとチーム調整用の指示の使用が停止されます。', + 'disable.warning': '無効にすると、キャッシュの再利用に影響し、その後のトークン使用量や費用が増加する可能性があります。また、現在のタスクでの連携に影響する可能性があります。', + 'disable.cancel': 'キャンセル', + 'disable.confirm': '無効にする', + 'disable.close': '閉じる', + 'trigger': 'タスクボード', + 'loading': 'タスクボードを読み込み中…', + 'unavailable': 'タスクボードは利用できません', + 'failure': '保存されたタスクレコードが無効です: {message}', + 'empty': '共有タスクはまだありません。会話から作成してください。', + 'tasks': '共有タスク', + 'owner': 'オーナー', + 'unowned': 'オーナーなし', + 'blocked': '依存関係によりブロック中', + 'status.pending': '保留中', + 'status.in_progress': '進行中', + 'status.completed': '完了', +} satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'progress.completed': '{count} completado', + 'progress.active': '{count} en progreso', + 'progress.pending': '{count} pendiente', + 'command.title': 'Colaboración entre agentes', + 'command.running': 'En ejecución', + 'command.done': 'Completado', + 'command.failed': 'Error', + 'command.failedReason': 'Error: {reason}', + 'mode.label': 'Activar colaboración entre agentes', + 'mode.infoLabel': 'Acerca de la colaboración entre agentes', + 'mode.description': 'Coordina el trabajo y el progreso de varios agentes mediante un tablero de tareas compartido, con mejores resultados en tareas complejas y un coste de tokens proporcionalmente mayor.', + 'mode.failed': 'No se pudo confirmar que se guardó la configuración de colaboración entre agentes. Inténtalo de nuevo: {message}', + 'disable.title': '¿Desactivar la colaboración entre agentes?', + 'disable.description': 'Desactivar la colaboración entre agentes durante una conversación detiene el uso de las herramientas de tareas compartidas y las instrucciones de coordinación del equipo.', + 'disable.warning': 'La desactivación puede afectar a la reutilización de la caché, aumentar el consumo posterior de tokens y los costes, y afectar a la colaboración en la tarea actual.', + 'disable.cancel': 'Cancelar', + 'disable.confirm': 'Confirmar desactivación', + 'disable.close': 'Cerrar', + trigger: 'Tablero de tareas', + loading: 'Cargando tablero de tareas…', + unavailable: 'El tablero de tareas no está disponible', + failure: 'Registro de tarea persistente no válido: {message}', + empty: 'Aún no hay tareas compartidas. Créelos a través de la conversación.', + tasks: 'Tareas compartidas', + owner: 'propietario', + unowned: 'Sin dueño', + blocked: 'Bloqueado por dependencias', + 'status.pending': 'Pendiente', + 'status.in_progress': 'En progreso', + 'status.completed': 'Completado', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'progress.completed': '{count} terminé', + 'progress.active': '{count} en cours', + 'progress.pending': '{count} en attente', + 'command.title': 'Collaboration entre agents', + 'command.running': 'En cours', + 'command.done': 'Terminé', + 'command.failed': 'Échec', + 'command.failedReason': 'Échec : {reason}', + 'mode.label': 'Activer la collaboration entre agents', + 'mode.infoLabel': 'À propos de la collaboration entre agents', + 'mode.description': 'Coordonnez la répartition du travail et la progression de plusieurs agents via un tableau de tâches partagé, pour de meilleurs résultats sur les tâches complexes, avec un coût en tokens plus élevé en contrepartie.', + 'mode.failed': 'Impossible de confirmer l’enregistrement du réglage de collaboration entre agents. Réessayez : {message}', + 'disable.title': 'Désactiver la collaboration entre agents ?', + 'disable.description': 'Désactiver la collaboration entre agents pendant une conversation arrête l’utilisation des outils de tâches partagées et des instructions de coordination de l’équipe.', + 'disable.warning': 'La désactivation peut affecter la réutilisation du cache, augmenter la consommation ultérieure de tokens et les coûts, et affecter la collaboration sur la tâche en cours.', + 'disable.cancel': 'Annuler', + 'disable.confirm': 'Confirmer la désactivation', + 'disable.close': 'Fermer', + trigger: 'Tableau des tâches', + loading: 'Chargement du tableau des tâches…', + unavailable: 'Le tableau des tâches n’est pas disponible', + failure: 'Enregistrement de tâche persistant non valide : {message}', + empty: "Aucune tâche partagée pour l'instant. Créez-les à travers la conversation.", + tasks: 'Tâches partagées', + owner: 'Propriétaire', + unowned: 'Sans propriétaire', + blocked: 'Bloqué par les dépendances', + 'status.pending': 'En attente', + 'status.in_progress': 'En cours', + 'status.completed': 'Terminé', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'progress.completed': '{count} abgeschlossen', + 'progress.active': '{count} in Bearbeitung', + 'progress.pending': '{count} ausstehend', + 'command.title': 'Agentenzusammenarbeit', + 'command.running': 'Wird ausgeführt', + 'command.done': 'Abgeschlossen', + 'command.failed': 'Fehlgeschlagen', + 'command.failedReason': 'Fehlgeschlagen: {reason}', + 'mode.label': 'Agentenzusammenarbeit aktivieren', + 'mode.infoLabel': 'Über Agentenzusammenarbeit', + 'mode.description': 'Koordiniert die Arbeitsteilung und den Fortschritt mehrerer Agenten über eine gemeinsame Aufgabenübersicht. Erzielt bessere Ergebnisse bei komplexen Aufgaben, bei entsprechend höheren Tokenkosten.', + 'mode.failed': 'Das Speichern der Einstellung zur Agentenzusammenarbeit konnte nicht bestätigt werden. Erneut versuchen: {message}', + 'disable.title': 'Agentenzusammenarbeit deaktivieren?', + 'disable.description': 'Wenn Sie die Agentenzusammenarbeit während eines Gesprächs deaktivieren, werden die Werkzeuge für gemeinsame Aufgaben und die Anweisungen zur Teamkoordination nicht mehr verwendet.', + 'disable.warning': 'Die Deaktivierung kann die Wiederverwendung des Caches beeinträchtigen, den späteren Tokenverbrauch und die Kosten erhöhen und die Zusammenarbeit bei der aktuellen Aufgabe beeinträchtigen.', + 'disable.cancel': 'Abbrechen', + 'disable.confirm': 'Deaktivierung bestätigen', + 'disable.close': 'Schließen', + trigger: 'Aufgabenübersicht', + loading: 'Aufgabenübersicht wird geladen…', + unavailable: 'Aufgabenübersicht ist nicht verfügbar', + failure: 'Ungültiger gespeicherter Aufgabeneintrag: {message}', + empty: 'Noch keine geteilten Aufgaben. Erstellen Sie sie durch das Gespräch.', + tasks: 'Geteilte Aufgaben', + owner: 'Besitzer', + unowned: 'Unbesessen', + blocked: 'Blockiert durch Abhängigkeiten', + 'status.pending': 'Ausstehend', + 'status.in_progress': 'In Bearbeitung', + 'status.completed': 'Abgeschlossen', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'progress.completed': '{count} завершено', + 'progress.active': '{count} в процессе', + 'progress.pending': '{count} в ожидании', + 'command.title': 'Совместная работа агентов', + 'command.running': 'Выполняется', + 'command.done': 'Завершено', + 'command.failed': 'Ошибка', + 'command.failedReason': 'Ошибка: {reason}', + 'mode.label': 'Включить совместную работу агентов', + 'mode.infoLabel': 'О совместной работе агентов', + 'mode.description': 'Координируйте распределение работы и прогресс нескольких агентов с помощью общей доски задач: лучшие результаты в сложных задачах при соответственно более высоких затратах на токены.', + 'mode.failed': 'Не удалось подтвердить сохранение настройки совместной работы агентов. Повторите попытку: {message}', + 'disable.title': 'Отключить совместную работу агентов?', + 'disable.description': 'Отключение совместной работы агентов во время разговора прекращает использование инструментов общих задач и инструкций по координации команды.', + 'disable.warning': 'Отключение может повлиять на повторное использование кэша, увеличить последующий расход токенов и затраты, а также повлиять на совместную работу над текущей задачей.', + 'disable.cancel': 'Отмена', + 'disable.confirm': 'Подтвердить отключение', + 'disable.close': 'Закрыть', + trigger: 'Доска задач', + loading: 'Загрузка доски задач…', + unavailable: 'Доска задач недоступна', + failure: 'Недопустимая сохранённая запись задачи: {message}', + empty: 'Общие задачи пока отсутствуют. Создайте их посредством разговора.', + tasks: 'Общие задачи', + owner: 'Владелец', + unowned: 'Невладеющий', + blocked: 'Заблокировано зависимостями', + 'status.pending': 'Ожидается', + 'status.in_progress': 'В процессе', + 'status.completed': 'Завершено', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'progress.completed': '{count} concluído', + 'progress.active': '{count} em andamento', + 'progress.pending': '{count} pendente', + 'command.title': 'Colaboração entre agentes', + 'command.running': 'Em execução', + 'command.done': 'Concluído', + 'command.failed': 'Falha', + 'command.failedReason': 'Falha: {reason}', + 'mode.label': 'Ativar colaboração entre agentes', + 'mode.infoLabel': 'Sobre a colaboração entre agentes', + 'mode.description': 'Coordene a divisão do trabalho e o progresso de vários agentes com um quadro de tarefas compartilhado, para melhores resultados em tarefas complexas, com custos de tokens proporcionalmente maiores.', + 'mode.failed': 'Não foi possível confirmar que a configuração de colaboração entre agentes foi salva. Tente novamente: {message}', + 'disable.title': 'Desativar a colaboração entre agentes?', + 'disable.description': 'Desativar a colaboração entre agentes durante uma conversa interrompe o uso das ferramentas de tarefas compartilhadas e das instruções de coordenação da equipe.', + 'disable.warning': 'A desativação pode afetar a reutilização do cache, aumentar o consumo posterior de tokens e os custos, e afetar a colaboração na tarefa atual.', + 'disable.cancel': 'Cancelar', + 'disable.confirm': 'Confirmar desativação', + 'disable.close': 'Fechar', + trigger: 'Quadro de tarefas', + loading: 'Carregando quadro de tarefas…', + unavailable: 'O quadro de tarefas não está disponível', + failure: 'Registro de tarefa persistente inválido: {message}', + empty: 'Ainda não há tarefas compartilhadas. Crie-os através da conversa.', + tasks: 'Tarefas compartilhadas', + owner: 'Proprietário', + unowned: 'Sem dono', + blocked: 'Bloqueado por dependências', + 'status.pending': 'Pendente', + 'status.in_progress': 'Em andamento', + 'status.completed': 'Concluído', +} satisfies Record diff --git a/packages/client/ui-agent-team/src/client/mount.ts b/packages/client/ui-agent-team/src/client/mount.ts new file mode 100644 index 0000000000..833267e39e --- /dev/null +++ b/packages/client/ui-agent-team/src/client/mount.ts @@ -0,0 +1,71 @@ +/** Source-safe task-board browser registration. */ + +import type { Context as ClientContext } from '@deepseek-ai/cordis' +import type {} from '@deepseek-ai/dsh-api-session-controller/client' +import type {} from '@deepseek-ai/dsh-client-ui-subagent/client' +import type {} from '@deepseek-ai/dsh-client-ui-conversation/client' +import type {} from '@deepseek-ai/dsh-client-locale/client' +import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' +import type {} from '@deepseek-ai/dsh-api-remotes/client' +import type {} from '@deepseek-ai/dsh-client-ui-model-selection/client' +import { createSnapshotStore } from '@deepseek-ai/dsh-client-store' +import type { SessionId } from '@deepseek-ai/dsh-session/types' +import { TeamDisableConfirmation, type TeamDisableConfirmationInjected, type TeamDisableRequest } from './TeamDisableConfirmation.tsx' +import { TeamModeToggle, type TeamModeToggleInjected } from './TeamModeToggle.tsx' +import { TeamBoard } from './TeamBoard.tsx' +import { TeamCommandRow } from './TeamCommandRow.tsx' +import { en, ja, es, fr, de, ru, pt, NS, zh, type TeamKey } from './locales.ts' + +declare module '@deepseek-ai/dsh-client-ui-slots' { + interface LocaleNamespaceMap { + /** Shared task-board copy. */ + 'agent-team': TeamKey + } +} + +/** Required browser services for slots and localized copy. */ +export const inject = ['slots', 'locale'] + +/** + * Register the task-board dictionaries and subagent catalog sections. + * The panel reads the root Session’s `agentTeam` projection through standard Session hooks. + * @param ctx - Client Context carrying the injected locale and slot services. + */ +export function registerAgentTeamUi(ctx: ClientContext): void { + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'client-ui-agent-team: dictionaries') + ctx.slots.inject('conversation.chat.commandview', () => ctx.slots.register({ + name: 'conversation.chat.commandview', key: 'team', locale: NS, + }, TeamCommandRow)) + ctx.inject(['remote', 'remote.agentTeams'], (scope) => { + scope.slots.inject('shell.overlay', function* () { + const disableRequest = createSnapshotStore(null) + const selectTeamMode = async (sessionId: SessionId, enabled: boolean): Promise => { + const response = await scope.remote.agentTeams.setEnabled(sessionId, enabled) + return response.ok ? null : response.error.message + } + yield () => { disableRequest.set(null) } + yield scope.slots.register({ + name: 'shell.overlay', id: 'agent-team.disable', locale: NS, + inject: (): TeamDisableConfirmationInjected => ({ + hooks: { disableRequest }, + cancelDisable: (sessionId) => { + if (disableRequest.getSnapshot()?.sessionId === sessionId) disableRequest.set(null) + }, + disableTeam: sessionId => selectTeamMode(sessionId, false), + }), + }, TeamDisableConfirmation) + yield scope.slots.inject('conversation.input.model.options', () => scope.slots.register({ + name: 'conversation.input.model.options', + id: 'agent-team', + locale: NS, + inject: (sessionId): TeamModeToggleInjected => ({ + selectTeamMode: enabled => selectTeamMode(sessionId, enabled), + requestDisableConfirmation: () => { disableRequest.set({ sessionId }) }, + }), + }, TeamModeToggle)) + }) + }) + ctx.slots.inject('conversation.session.header.actions.subagent.footer', () => ctx.slots.register({ + name: 'conversation.session.header.actions.subagent.footer', id: 'agent-team', locale: NS, + }, TeamBoard)) +} diff --git a/packages/experimental/client-ui-agent-team/src/css-modules.d.ts b/packages/client/ui-agent-team/src/css-modules.d.ts similarity index 100% rename from packages/experimental/client-ui-agent-team/src/css-modules.d.ts rename to packages/client/ui-agent-team/src/css-modules.d.ts diff --git a/packages/experimental/client-ui-agent-team/src/index.ts b/packages/client/ui-agent-team/src/index.ts similarity index 100% rename from packages/experimental/client-ui-agent-team/src/index.ts rename to packages/client/ui-agent-team/src/index.ts diff --git a/packages/client/ui-agent-team/tests/browser-plugin.client.spec.ts b/packages/client/ui-agent-team/tests/browser-plugin.client.spec.ts new file mode 100644 index 0000000000..ba964a033f --- /dev/null +++ b/packages/client/ui-agent-team/tests/browser-plugin.client.spec.ts @@ -0,0 +1,135 @@ +import { Context } from '@deepseek-ai/cordis' +import { describe, expect, it, onTestFinished, vi } from 'vitest' +import { SessionId } from '@deepseek-ai/dsh-session/types' +import { RemoteError, TestRemote } from '@deepseek-ai/dsh-client-test-runtime' +import { TeamModeToggle, type TeamModeToggleInjected } from '../src/client/TeamModeToggle.tsx' +import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client' +import { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client' +import { TeamBoard } from '../src/client/TeamBoard.tsx' +import { TeamCommandRow } from '../src/client/TeamCommandRow.tsx' +import { TeamDisableConfirmation, type TeamDisableConfirmationInjected } from '../src/client/TeamDisableConfirmation.tsx' +import { apply, inject } from '../src/client/index.ts' +import { apply as nodeApply } from '../src/index.ts' + +async function bench() { + const ctx = new Context() + onTestFinished(() => ctx.fiber.dispose()) + ctx.provide('locale', new LocaleRuntime(ctx)) + await ctx.plugin(SlotRegistry).await() + const collapseHeader = ctx.slots.register({ + name: 'root', + children: { + 'conversation.session.header.actions.subagent.footer': { kind: 'list', scope: 'session' }, + 'conversation.input.model': { kind: 'single', scope: 'session' }, + 'shell.overlay': { kind: 'list', scope: 'root' }, + }, + } as never, () => null) + const fiber = ctx.plugin({ inject: [...inject], apply }) + await fiber.await() + const entry = () => ctx.slots.entries('conversation.session.header.actions.subagent.footer') + .find(candidate => candidate.component === TeamBoard) + return { ctx, fiber, entry, collapseHeader } +} + +describe('task-board browser plugin', () => { + it('registers a disposable root task-board section without navigation or Team Remote services', async () => { + const b = await bench() + expect(inject).toEqual(['slots', 'locale']) + expect(b.entry()).toMatchObject({ + options: { id: 'agent-team' }, + locale: 'agent-team', + }) + expect(b.entry()!.inject).toBeUndefined() + expect(b.ctx.slots.entries('conversation.session.header.actions')).toHaveLength(0) + const t = b.ctx.locale.bind('agent-team') + expect(t('trigger')).toBe('Task board') + await b.fiber.dispose() + expect(b.entry()).toBeUndefined() + expect(t('empty')).toBe('empty') + }) + + it('re-registers when the conversation header slot is declared again', async () => { + const b = await bench() + expect(b.entry()).toBeDefined() + b.collapseHeader() + expect(b.entry()).toBeUndefined() + b.ctx.slots.register({ + name: 'root', + children: { 'conversation.session.header.actions.subagent.footer': { kind: 'list', scope: 'session' } }, + } as never, () => null) + await Promise.resolve() + expect(b.entry()).toBeDefined() + await b.fiber.dispose() + }) + + it('registers the mode switch when Agent Teams arrives and reports each execution outcome', async () => { + const b = await bench() + b.ctx.slots.register({ + name: 'conversation.input.model', + children: { 'conversation.input.model.options': { kind: 'list', scope: 'session' } }, + } as never, () => null) + const entry = () => b.ctx.slots.entries('conversation.input.model.options') + .find(candidate => candidate.component === TeamModeToggle) + expect(entry()).toBeUndefined() + const setEnabled = vi.fn() + new TestRemote(b.ctx, { agentTeams: { setEnabled } }) + await vi.waitFor(() => { expect(entry()).toBeDefined() }) + const injected = entry()!.inject as (sessionId: SessionId) => Record + const value: object = injected(SessionId('chosen-session')) + const mode = value as TeamModeToggleInjected + setEnabled.mockResolvedValueOnce({ ok: true, value: undefined }) + await expect(mode.selectTeamMode(true)).resolves.toBeNull() + expect(setEnabled).toHaveBeenLastCalledWith('chosen-session', true) + setEnabled.mockResolvedValueOnce({ ok: true, value: undefined }) + await expect(mode.selectTeamMode(false)).resolves.toBeNull() + expect(setEnabled).toHaveBeenLastCalledWith('chosen-session', false) + setEnabled.mockResolvedValueOnce({ ok: false, error: new RemoteError('gateway/internal', 'connection failed', {}) }) + await expect(mode.selectTeamMode(true)).resolves.toBe('connection failed') + const overlayEntry = () => b.ctx.slots.entries('shell.overlay').find(candidate => candidate.component === TeamDisableConfirmation) + const overlayValue: object = overlayEntry()!.inject!() + const overlay = overlayValue as TeamDisableConfirmationInjected + const request = overlay.hooks.disableRequest + expect(request.getSnapshot()).toBeNull() + const beforeConfirmation = setEnabled.mock.calls.length + mode.requestDisableConfirmation() + expect(request.getSnapshot()).toEqual({ sessionId: 'chosen-session' }) + expect(setEnabled).toHaveBeenCalledTimes(beforeConfirmation) + overlay.cancelDisable(SessionId('other-session')) + expect(request.getSnapshot()).toEqual({ sessionId: 'chosen-session' }) + setEnabled.mockResolvedValueOnce({ ok: true, value: undefined }) + await expect(overlay.disableTeam(SessionId('chosen-session'))).resolves.toBeNull() + expect(setEnabled).toHaveBeenLastCalledWith('chosen-session', false) + overlay.cancelDisable(SessionId('chosen-session')) + expect(request.getSnapshot()).toBeNull() + mode.requestDisableConfirmation() + await b.fiber.dispose() + expect(entry()).toBeUndefined() + expect(overlayEntry()).toBeUndefined() + expect(request.getSnapshot()).toBeNull() + }) + + it('binds Team command receipts to the command slot lifetime without Remote services', async () => { + const b = await bench() + const entry = () => b.ctx.slots.entries('conversation.chat.commandview').find(value => value.component === TeamCommandRow) + expect(entry()).toBeUndefined() + b.collapseHeader() + const registerParent = () => b.ctx.slots.register({ + name: 'root', + children: { 'conversation.chat.commandview': { kind: 'keyed', scope: 'session' } }, + } as never, () => null) + const collapse = registerParent() + await vi.waitFor(() => { expect(entry()).toBeDefined() }) + expect(entry()).toMatchObject({ options: { key: 'team' }, locale: 'agent-team' }) + expect(entry()!.inject).toBeUndefined() + collapse() + expect(entry()).toBeUndefined() + registerParent() + await vi.waitFor(() => { expect(entry()).toBeDefined() }) + await b.fiber.dispose() + expect(entry()).toBeUndefined() + }) + + it('keeps the node half inert', () => { + expect(() => { nodeApply() }).not.toThrow() + }) +}) diff --git a/packages/client/ui-agent-team/tests/team-board.client.spec.tsx b/packages/client/ui-agent-team/tests/team-board.client.spec.tsx new file mode 100644 index 0000000000..b09aedc4e5 --- /dev/null +++ b/packages/client/ui-agent-team/tests/team-board.client.spec.tsx @@ -0,0 +1,416 @@ +// @vitest-environment jsdom + +import { afterEach, describe, expect, it, vi } from 'vitest' +import { act, cleanup, fireEvent, render, screen, within } from '@testing-library/react' +import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type { TeamProjection, TeamTaskId, TeamTaskView as TeamTask } from '@deepseek-ai/dsh-agent-team/client' +import type { SessionListState, SessionSummary } from '@deepseek-ai/dsh-api-session-controller/client' +import { createSnapshotStore } from '@deepseek-ai/dsh-client-store' +import { bindSnapshotSelector, makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' +import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts' +import { TeamBoard, type TeamBoardProps } from '../src/client/TeamBoard.tsx' +import { en, zh } from '../src/client/locales.ts' + +afterEach(() => { + cleanup() + vi.useRealTimers() + vi.restoreAllMocks() + vi.unstubAllGlobals() +}) + +const SESSION = 'root-session-id' as SessionId +const WORKER = 'worker-id' as SessionId +const NESTED = 'nested-id' as SessionId +const TASK_1 = 'task-1' as TeamTaskId +const TASK_2 = 'task-2' as TeamTaskId +const task: TeamTask = { + id: TASK_1, + revision: 1, + subject: 'Implement runtime', + description: 'Build the Team runtime', + status: 'in_progress', + ownerId: SESSION, + blockedBy: [], + writeScopes: ['src'], + ready: false, + writeScopeWarnings: ['write scopes overlap with task-2'], +} +const team: TeamProjection = { enabled: true, tasks: [task] } +type Projections = SessionListState['projectionsBySession'] + +function summary(id: SessionId, title?: string): SessionSummary { + return { + id, ...(title === undefined ? {} : { title }), displayTitle: title ?? id, + running: false, retainedBy: {}, blank: false, updatedAt: 0, + } +} + +function bench(options: { + projections?: Projections + sessionId?: SessionId + byId?: Record + value?: TeamProjection +} = {}) { + const sessionId = options.sessionId ?? SESSION + const byId = options.byId ?? { [SESSION]: summary(SESSION) } + const sessions = createSnapshotStore({ + ids: Object.keys(byId) as SessionId[], byId, phase: 'ready', + projectionsBySession: options.projections ?? { [SESSION]: { state: 'ready', error: null, values: { agentTeam: options.value ?? team } } }, + }) + const useProjection = ((_key: 'agentTeam', selector?: (value: TeamProjection | undefined) => unknown) => + bindSnapshotSelector(sessions)((state) => { + const value = state.projectionsBySession[sessionId]?.values.agentTeam + return selector === undefined ? value : selector(value) + })) as TeamBoardProps['useProjection'] + const props = { + sessionId, useProjection, + useSessions: bindSnapshotSelector(sessions), + t: makeTranslate(zh, commonZh), + } as TeamBoardProps + return { props, sessions } +} + +function setProjectionSnapshot( + sessions: ReturnType['sessions'], sessionId: SessionId, snapshot: Projections[SessionId], +): void { + act(() => { + const current = sessions.getSnapshot() + sessions.set({ ...current, projectionsBySession: { ...current.projectionsBySession, [sessionId]: snapshot } }) + }) +} + +function setProjection(sessions: ReturnType['sessions'], sessionId: SessionId, value: TeamProjection): void { + setProjectionSnapshot(sessions, sessionId, { state: 'ready', error: null, values: { agentTeam: value } }) +} + +function row(subject = task.subject): HTMLElement { + return screen.getByRole('button', { name: new RegExp(subject, 'u') }) +} + +function ownerValue(): string | null { + return document.querySelector('[data-team-task-owner] span:last-child')?.textContent ?? null +} + +function enableTooltips(): void { + class TooltipObserver implements ResizeObserver { + constructor(private readonly callback: ResizeObserverCallback) {} + observe(target: Element): void { + const size = { inlineSize: 160, blockSize: 30 } + this.callback([{ + target, borderBoxSize: [size], contentBoxSize: [size], devicePixelContentBoxSize: [size], contentRect: new DOMRect(), + }], this) + } + unobserve(): void {} + disconnect(): void {} + } + vi.stubGlobal('ResizeObserver', TooltipObserver) + vi.stubGlobal('matchMedia', vi.fn(() => ({ matches: true }))) +} + +describe('TeamBoard', () => { + it('shows a nonclickable Checklist header and compact initially closed rows', () => { + const b = bench() + render() + const heading = screen.getByRole('heading') + expect(heading.textContent).toBe(`${zh.tasks}${b.props.t('progress.active', { count: 1 })}`) + expect(heading.querySelector('button')).toBeNull() + expect(heading.querySelector('[data-icon="dsw-icon-checklist-outline-regular"]')).toBeTruthy() + expect(screen.getAllByRole('button')).toEqual([row()]) + expect(row().getAttribute('aria-expanded')).toBe('false') + expect(row().textContent).toBe(task.subject) + expect(within(row()).getByRole('img', { name: zh['status.in_progress'] })).toBeTruthy() + expect(row().querySelector('[data-state="ongoing"]')).toBeTruthy() + expect(row().querySelector('[data-icon="dsw-icon-chevron-down-outline-regular"]')).toBeTruthy() + expect(screen.queryByText(task.description)).toBeNull() + expect(screen.queryByText(zh.owner)).toBeNull() + expect(screen.queryByRole('button', { name: '展开' })).toBeNull() + }) + + it('expands the full description before the owner and keeps only one task open', () => { + const longSubject = 'A complete subject that must remain readable when the task is expanded' + const description = 'First line\n'.repeat(30) + 'Final description line' + const b = bench({ value: { ...team, tasks: [ + { ...task, subject: longSubject, description }, + { ...task, id: TASK_2, subject: 'Review implementation', ownerId: WORKER }, + ] } }) + render() + const first = row(longSubject) + fireEvent.click(first) + expect(first.getAttribute('aria-expanded')).toBe('true') + expect(row(longSubject)).toBe(first) + const details = document.getElementById(first.getAttribute('aria-controls')!)! + const owner = details.querySelector('[data-team-task-owner]')! + expect(owner.tagName).toBe('P') + expect(owner.textContent).toBe(`${zh.owner}: ${SESSION}`) + expect(ownerValue()).toBe(SESSION) + const paragraphs = details.querySelectorAll('p') + expect(paragraphs).toHaveLength(2) + expect(paragraphs[0]?.textContent).toBe(description) + expect(paragraphs[1]).toBe(owner) + expect(details.querySelector('dl, dt, dd')).toBeNull() + expect(screen.queryByText(TASK_1)).toBeNull() + expect(screen.queryByText('src')).toBeNull() + expect(screen.queryByText(task.writeScopeWarnings[0]!)).toBeNull() + expect(screen.getAllByRole('button')).toHaveLength(2) + fireEvent.click(row('Review implementation')) + expect(first.getAttribute('aria-expanded')).toBe('false') + expect(ownerValue()).toBe(WORKER) + expect(document.querySelectorAll('[data-team-task-details]')).toHaveLength(1) + expect(screen.queryByText(description)).toBeNull() + fireEvent.click(row('Review implementation')) + expect(ownerValue()).toBeNull() + expect(screen.getAllByRole('button').every(button => button.getAttribute('aria-expanded') === 'false')).toBe(true) + }) + + it('preserves source order, keeps completed tasks, and excludes deleted tombstones from rows and counts', () => { + const b = bench({ value: { ...team, tasks: [ + { ...task, subject: 'Pending first', status: 'pending', ready: true }, + { ...task, id: TASK_2, subject: 'Completed second', status: 'completed' }, + { ...task, id: 'third' as TeamTaskId, subject: 'Active third' }, + { ...task, id: 'deleted' as TeamTaskId, subject: 'Deleted task', status: 'deleted' }, + ] } }) + render() + expect(screen.getAllByRole('button').map(button => button.textContent)).toEqual(['Pending first', 'Completed second', 'Active third']) + expect(screen.getByRole('heading').textContent).toBe(`${zh.tasks}${[ + b.props.t('progress.completed', { count: 1 }), b.props.t('progress.active', { count: 1 }), b.props.t('progress.pending', { count: 1 }), + ].join('\u2002·\u2002')}`) + expect(screen.queryByText('Deleted task')).toBeNull() + expect(row('Pending first').querySelector('[data-state="idle"]')).toBeTruthy() + expect(row('Completed second').querySelector('[data-state="done"]')).toBeTruthy() + fireEvent.click(row('Completed second')) + expect(row('Completed second').getAttribute('aria-expanded')).toBe('true') + expect(ownerValue()).toBe(SESSION) + expect(screen.getByText(task.description)).toBeTruthy() + }) + + it('announces blocked status without displaying readiness, dependencies, scopes, warnings, or task ids', () => { + const { ownerId: _ownerId, ...unowned } = task + const b = bench({ value: { ...team, tasks: [ + { ...unowned, status: 'pending', blockedBy: [TASK_2, 'missing' as TeamTaskId] }, + { ...task, id: TASK_2, subject: 'Prepare shared types', status: 'completed' }, + ] } }) + render() + expect(within(row()).getByRole('img', { name: zh.blocked })).toBeTruthy() + expect(row().querySelector('[data-state="warning"]')).toBeTruthy() + fireEvent.click(row()) + expect(ownerValue()).toBe(zh.unowned) + const details = document.querySelector('[data-team-task-details]')! + expect(details.textContent).toBe(`${task.description}${zh.owner}: ${zh.unowned}`) + expect(details.querySelector('dl, dt, dd')).toBeNull() + for (const metadata of [ + TASK_1, TASK_2, 'Prepare shared types', 'missing', 'src', task.writeScopeWarnings[0]!, zh.blocked, + ]) expect(within(details).queryByText(metadata)).toBeNull() + expect(b.sessions.getSnapshot().projectionsBySession[SESSION]!.values.agentTeam?.tasks[0]).toEqual({ + ...unowned, status: 'pending', blockedBy: [TASK_2, 'missing' as TeamTaskId], + }) + }) + + it('keeps the pending glyph and shows only the owner when description is empty', () => { + render() + fireEvent.click(row()) + expect(within(row()).getByRole('img', { name: zh['status.pending'] })).toBeTruthy() + expect(row().querySelector('[data-state="idle"]')).toBeTruthy() + expect(ownerValue()).toBe(SESSION) + expect(document.querySelector('[data-team-task-details]')?.children).toHaveLength(1) + expect(document.querySelector('[data-team-task-details] p:not([data-team-task-owner])')).toBeNull() + }) + + it('uses the owner id independently of catalog descriptions and Session titles', () => { + const b = bench({ + byId: { [SESSION]: summary(SESSION), [NESTED]: { ...summary(NESTED, 'Session title'), parentId: WORKER } }, + projections: { + [SESSION]: { state: 'ready', error: null, values: { + agentTeam: { ...team, tasks: [{ ...task, ownerId: NESTED }] }, + } }, + [WORKER]: { state: 'ready', error: null, values: { + subagentCatalog: [{ id: NESTED, createdAt: 0, mode: 'continuable', label: 'Catalog responsibility' }], + } }, + }, + }) + render() + fireEvent.click(row()) + expect(ownerValue()).toBe(NESTED) + setProjectionSnapshot(b.sessions, WORKER, { state: 'ready', error: null, values: {} }) + act(() => { b.sessions.update((draft) => { draft.byId[NESTED] = { ...summary(NESTED), displayTitle: 'Display title' } }) }) + expect(ownerValue()).toBe(NESTED) + setProjection(b.sessions, SESSION, { ...team, tasks: [{ ...task, ownerId: WORKER }] }) + expect(ownerValue()).toBe(WORKER) + }) + + it('uses owner id when neither root nor parent projections exist', () => { + const b = bench({ value: { ...team, tasks: [{ ...task, ownerId: NESTED }] }, byId: { + [NESTED]: { ...summary(NESTED), parentId: WORKER }, + } }) + render() + fireEvent.click(row()) + expect(ownerValue()).toBe(NESTED) + }) + + it('hides retained tasks while Team is disabled and shows them when enabled', () => { + const b = bench() + render() + setProjection(b.sessions, SESSION, { ...team, enabled: false }) + expect(screen.queryByRole('region')).toBeNull() + expect(screen.queryByText(task.subject)).toBeNull() + setProjection(b.sessions, SESSION, team) + expect(row()).toBeTruthy() + }) + + it('accepts absent baselines, capability removal, empty tasks, and deleted-only snapshots', () => { + const b = bench({ projections: {} }) + render() + expect(screen.queryByRole('region')).toBeNull() + setProjectionSnapshot(b.sessions, SESSION, { state: 'idle', error: null, values: { agentTeam: team } }) + expect(row()).toBeTruthy() + setProjectionSnapshot(b.sessions, SESSION, { state: 'idle', error: null, values: {} }) + expect(screen.queryByRole('region')).toBeNull() + for (const tasks of [[], [{ ...task, status: 'deleted' as const }]]) { + setProjection(b.sessions, SESSION, { enabled: true, tasks }) + expect(screen.queryByText(zh.empty)).toBeNull() + expect(screen.queryByRole('heading')).toBeNull() + expect(screen.queryByRole('button')).toBeNull() + expect(screen.queryByRole('region')).toBeNull() + expect(document.querySelector('[data-team-panel]')).toBeNull() + } + }) + + it('reads only the current Session projection', () => { + const b = bench({ sessionId: WORKER }) + render() + expect(screen.queryByRole('region')).toBeNull() + setProjection(b.sessions, WORKER, { ...team, tasks: [{ ...task, subject: 'Current task' }] }) + expect(row('Current task')).toBeTruthy() + setProjection(b.sessions, SESSION, { ...team, tasks: [{ ...task, subject: 'Other task' }] }) + expect(screen.queryByText('Other task')).toBeNull() + }) + + it('keeps failures beside valid tasks but shows only an alert without visible tasks', () => { + const b = bench({ value: { ...team, failure: 'revision is not contiguous' } }) + const view = render() + expect(screen.getByRole('alert').textContent).toBe('任务记录无效:revision is not contiguous') + expect(row()).toBeTruthy() + expect(screen.getByRole('heading')).toBeTruthy() + for (const tasks of [[], [{ ...task, status: 'deleted' as const }]]) { + setProjectionSnapshot(b.sessions, SESSION, { state: 'ready', error: null, values: { + agentTeam: { enabled: true, tasks, failure: 'invalid task' }, + subagentCatalog: [{ id: WORKER, createdAt: 0, mode: 'continuable', label: 'Worker' }], + } }) + const alert = screen.getByRole('alert') + expect(alert.textContent).toBe('任务记录无效:invalid task') + expect(screen.getAllByRole('alert')).toHaveLength(1) + expect(view.container.textContent).toBe(alert.textContent) + expect(view.container.querySelector('[data-with-members="true"]')).toBeNull() + expect(screen.queryByRole('heading')).toBeNull() + expect(screen.queryByRole('region')).toBeNull() + expect(screen.queryByText(zh.empty)).toBeNull() + expect(document.querySelector('[data-team-panel]')).toBeNull() + } + setProjection(b.sessions, SESSION, { enabled: false, tasks: [], failure: 'invalid task' }) + expect(view.container.childElementCount).toBe(0) + }) + + it('renders nothing for empty or deleted-only tasks even when members exist', () => { + const b = bench() + const view = render() + for (const tasks of [[], [{ ...task, status: 'deleted' as const }]]) { + setProjectionSnapshot(b.sessions, SESSION, { state: 'ready', error: null, values: { + agentTeam: { enabled: true, tasks }, + subagentCatalog: [{ id: WORKER, createdAt: 0, mode: 'continuable', label: 'Worker' }], + } }) + expect(view.container.childElementCount).toBe(0) + } + }) + + it('keeps live updates and expansion tied to task identity without sorting or polling', () => { + const b = bench() + render() + fireEvent.click(row()) + setProjection(b.sessions, SESSION, { ...team, tasks: [ + { ...task, id: TASK_2, subject: 'New task', status: 'pending', ready: true }, + { ...task, status: 'completed', description: 'Updated details' }, + ] }) + expect(screen.getAllByRole('button').map(button => button.textContent)).toEqual(['New task', task.subject]) + expect(row().getAttribute('aria-expanded')).toBe('true') + expect(row('New task').getAttribute('aria-expanded')).toBe('false') + expect(screen.getByText('Updated details')).toBeTruthy() + setProjection(b.sessions, SESSION, { ...team, tasks: [{ ...task, status: 'deleted' }] }) + expect(screen.queryByText('Updated details')).toBeNull() + }) + + it('uses translated English progress and details without Show more or Show less controls', () => { + const b = bench() + render() + expect(screen.getByRole('heading').textContent).toBe(`${en.tasks}${makeTranslate(en)('progress.active', { count: 1 })}`) + fireEvent.click(row()) + expect(ownerValue()).toBe(SESSION) + expect(document.querySelector('[data-team-task-owner]')?.textContent).toBe(`${en.owner}: ${SESSION}`) + expect(screen.queryByText(TASK_1)).toBeNull() + expect(screen.queryByText('Show more')).toBeNull() + expect(screen.queryByText('Show less')).toBeNull() + }) + + it('shows the clipped owner tooltip after 500ms of hover', () => { + vi.useFakeTimers() + enableTooltips() + const ownerId = '8c0404a0-4c67-48ab-9513-50b4a755d6de' as SessionId + const b = bench({ + value: { ...team, tasks: [{ ...task, ownerId }] }, + }) + const view = render() + fireEvent.click(row()) + const owner = screen.getByText(ownerId) + expect(owner.parentElement?.hasAttribute('data-team-task-owner')).toBe(true) + const scrollWidth = vi.spyOn(Element.prototype, 'scrollWidth', 'get').mockReturnValue(80) + vi.spyOn(Element.prototype, 'clientWidth', 'get').mockReturnValue(80) + fireEvent.pointerEnter(owner) + act(() => { vi.advanceTimersByTime(500) }) + expect(screen.queryByRole('tooltip')).toBeNull() + fireEvent.pointerLeave(owner) + scrollWidth.mockImplementation(function (this: Element) { return this === owner ? 240 : 80 }) + fireEvent.pointerEnter(owner) + act(() => { vi.advanceTimersByTime(499) }) + expect(screen.queryByRole('tooltip')).toBeNull() + act(() => { vi.advanceTimersByTime(1) }) + expect(screen.getByRole('tooltip').textContent).toBe(ownerId) + expect(view.container.contains(screen.getByRole('tooltip'))).toBe(false) + expect(document.querySelectorAll('[data-team-task-owner]')).toHaveLength(1) + fireEvent.click(row()) + expect(screen.queryByRole('tooltip')).toBeNull() + expect(ownerValue()).toBeNull() + }) + + it('delays clipped collapsed subject hover by 500ms while keeping keyboard focus immediate', () => { + vi.useFakeTimers() + enableTooltips() + const view = render() + const button = row() + const subject = screen.getByText(task.subject) + const scrollWidth = vi.spyOn(Element.prototype, 'scrollWidth', 'get').mockReturnValue(80) + vi.spyOn(Element.prototype, 'clientWidth', 'get').mockReturnValue(80) + fireEvent.pointerEnter(button) + act(() => { vi.advanceTimersByTime(500) }) + expect(screen.queryByRole('tooltip')).toBeNull() + fireEvent.pointerLeave(button) + scrollWidth.mockImplementation(function (this: Element) { return this === subject ? 180 : 80 }) + fireEvent.pointerEnter(button) + act(() => { vi.advanceTimersByTime(499) }) + expect(screen.queryByRole('tooltip')).toBeNull() + act(() => { vi.advanceTimersByTime(1) }) + expect(screen.getByRole('tooltip').textContent).toBe(task.subject) + expect(view.container.contains(screen.getByRole('tooltip'))).toBe(false) + fireEvent.click(button) + expect(row()).toBe(button) + expect(screen.queryByRole('tooltip')).toBeNull() + fireEvent.pointerLeave(button) + fireEvent.pointerEnter(button) + act(() => { vi.advanceTimersByTime(500) }) + expect(screen.queryByRole('tooltip')).toBeNull() + fireEvent.click(button) + fireEvent.pointerLeave(button) + fireEvent.keyDown(document.body, { key: 'Tab' }) + fireEvent.focus(button) + expect(screen.getByRole('tooltip').textContent).toBe(task.subject) + }) +}) diff --git a/packages/client/ui-agent-team/tests/team-command.client.spec.tsx b/packages/client/ui-agent-team/tests/team-command.client.spec.tsx new file mode 100644 index 0000000000..999b336108 --- /dev/null +++ b/packages/client/ui-agent-team/tests/team-command.client.spec.tsx @@ -0,0 +1,98 @@ +// @vitest-environment jsdom +import { afterEach, describe, expect, it } from 'vitest' +import { cleanup, fireEvent, render } from '@testing-library/react' +import { CommandId } from '@deepseek-ai/dsh-commands/brand' +import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' +import { IconUsersOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives' +import { TeamCommandRow, type TeamCommandRowProps } from '../src/client/TeamCommandRow.tsx' +import { zh, en, ja, es, fr, de, ru, pt } from '../src/client/locales.ts' + +afterEach(cleanup) + +type Node = TeamCommandRowProps['node'] +function command(args: string | null, outcome: Node['outcome']): Node { + return { kind: 'command', seq: 1, time: 1, commandId: CommandId('team-command'), name: 'team', args, outcome } +} + +describe.each([ + { id: 'zh', dictionary: zh }, { id: 'en', dictionary: en }, { id: 'ja', dictionary: ja }, { id: 'es', dictionary: es }, + { id: 'fr', dictionary: fr }, { id: 'de', dictionary: de }, { id: 'ru', dictionary: ru }, { id: 'pt', dictionary: pt }, +])('Team command fallback in $id', ({ dictionary }) => { + const t = makeTranslate(dictionary) + + it.each(['status', 'ON', '', null])('shows a localized pending status for unrecognized input %j', (args) => { + const view = render() + expect(view.getByText(dictionary['command.title'])).toBeTruthy() + expect(view.getByText(dictionary['command.running'])).toBeTruthy() + expect(view.container.querySelector('[data-team-command]')?.getAttribute('aria-busy')).toBe('true') + expect(view.container.querySelector('[data-team-command]')?.getAttribute('data-state')).toBe('running') + expect(view.queryByRole('button')).toBeNull() + }) + + it.each(['status', 'ON', '', null])('preserves successful output for unrecognized input %j', (args) => { + const text = 'Agent Teams enabled.' + const node = command(args, { kind: 'success', text }) + const original = JSON.stringify(node) + const view = render() + expect(view.getByText(dictionary['command.title'])).toBeTruthy() + expect(view.getByText(text)).toBeTruthy() + expect(view.queryByRole('button')).toBeNull() + expect(view.container.querySelector('[data-team-command]')?.getAttribute('data-state')).toBe('ok') + expect(JSON.stringify(node)).toBe(original) + }) + + it.each([undefined, ''])('uses a localized completed fallback for empty output %j', (text) => { + const outcome: Node['outcome'] = { kind: 'success', ...text === undefined ? {} : { text } } + const view = render() + expect(view.getByText(dictionary['command.title'])).toBeTruthy() + expect(view.getByText(dictionary['command.done'])).toBeTruthy() + }) + + it.each(['on', 'off', 'status', null])('keeps the raw failure expandable for input %j', (args) => { + const raw = ' Save failed\r\nThe original diagnostic remains available.\n' + const node = command(args, { kind: 'error', text: raw }) + const original = JSON.stringify(node) + const view = render() + expect(view.getByText(dictionary['command.title'])).toBeTruthy() + expect(view.getByText(t('command.failedReason', { reason: 'Save failed' }))).toBeTruthy() + expect(view.container.querySelector('[data-team-command]')?.getAttribute('data-state')).toBe('error') + const toggle = view.getByRole('button', { expanded: false }) + fireEvent.keyDown(toggle, { key: 'Enter' }) + expect(view.container.querySelector('pre')?.textContent).toBe(raw) + fireEvent.keyDown(toggle, { key: ' ' }) + expect(view.container.querySelector('pre')).toBeNull() + expect(JSON.stringify(node)).toBe(original) + }) + + it.each([undefined, '', '\n '])('uses a localized failure fallback for empty reason %j', (text) => { + const outcome: Node['outcome'] = { kind: 'error', ...text === undefined ? {} : { text } } + const view = render() + expect(view.getByText(dictionary['command.failed'])).toBeTruthy() + }) +}) + +it('uses the shared agent glyph for a failed command', () => { + const view = render() + const icon = render() + expect(view.container.querySelector('svg')?.outerHTML).toBe(icon.container.querySelector('svg')?.outerHTML) +}) + +it('keeps a long single-line failure expandable', () => { + const raw = 'The original failure detail '.repeat(20) + const view = render() + fireEvent.click(view.getByRole('button', { expanded: false })) + expect(view.container.querySelector('pre')?.textContent).toBe(raw) +}) + +it.each(['success', 'error'] as const)('relocalizes the title without rewriting expanded %s output', (kind) => { + const node = command('status', { kind, text: 'Recorded output\nAdditional recorded detail.' }) + const original = JSON.stringify(node) + const view = render() + expect(view.getByText(en['command.title'])).toBeTruthy() + fireEvent.click(view.getByRole('button', { expanded: false })) + expect(view.container.querySelector('pre')?.textContent).toBe(node.outcome?.text) + view.rerender() + expect(view.getByText(zh['command.title'])).toBeTruthy() + expect(view.container.querySelector('pre')?.textContent).toBe(node.outcome?.text) + expect(JSON.stringify(node)).toBe(original) +}) diff --git a/packages/client/ui-agent-team/tests/team-disable-confirmation-callbacks.client.spec.tsx b/packages/client/ui-agent-team/tests/team-disable-confirmation-callbacks.client.spec.tsx new file mode 100644 index 0000000000..92fa679c4c --- /dev/null +++ b/packages/client/ui-agent-team/tests/team-disable-confirmation-callbacks.client.spec.tsx @@ -0,0 +1,43 @@ +// @vitest-environment jsdom + +import type { ComponentProps } from 'react' +import { afterEach, expect, it, onTestFinished, vi } from 'vitest' +import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' +import { SessionId } from '@deepseek-ai/dsh-session' +import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' +import type { Button } from '@deepseek-ai/dsh-client-ui-primitives' +import { TeamDisableConfirmation, type TeamDisableConfirmationProps } from '../src/client/TeamDisableConfirmation.tsx' +import { zh } from '../src/client/locales.ts' + +vi.mock('../src/client/TeamDisableConfirmation.module.css', () => ({ default: {} })) +vi.mock('@deepseek-ai/dsh-client-ui-primitives', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // Deliver repeated activations to the form even while its native button would suppress them. + Button: (props: ComponentProps) => , + } +}) + +afterEach(cleanup) + +it('keeps one disable operation pending despite repeated callbacks without optional stylesheet classes', async () => { + const pending = Promise.withResolvers() + onTestFinished(async () => { await act(async () => { pending.resolve(null) }) }) + const sessionId = SessionId('pending-disable-session') + const disableTeam = vi.fn(() => pending.promise) + const cancelDisable = vi.fn() + const useDisableRequest: TeamDisableConfirmationProps['useDisableRequest'] = select => select({ sessionId }) + const props = { useDisableRequest, disableTeam, cancelDisable, t: makeTranslate(zh) } as TeamDisableConfirmationProps + render() + const confirm = screen.getByRole('button', { name: zh['disable.confirm'] }) + fireEvent.click(confirm) + expect(confirm.getAttribute('aria-disabled')).toBe('true') + fireEvent.click(confirm) + fireEvent.click(screen.getByRole('button', { name: zh['disable.cancel'] })) + expect(disableTeam).toHaveBeenCalledExactlyOnceWith(sessionId) + expect(cancelDisable).not.toHaveBeenCalled() + expect(screen.getByRole('dialog', { name: zh['disable.title'] })).toBeTruthy() + await act(async () => { pending.resolve(null) }) + expect(cancelDisable).toHaveBeenCalledExactlyOnceWith(sessionId) +}) diff --git a/packages/client/ui-agent-team/tests/team-disable-confirmation.client.spec.tsx b/packages/client/ui-agent-team/tests/team-disable-confirmation.client.spec.tsx new file mode 100644 index 0000000000..58b3fa9004 --- /dev/null +++ b/packages/client/ui-agent-team/tests/team-disable-confirmation.client.spec.tsx @@ -0,0 +1,172 @@ +// @vitest-environment jsdom + +import { afterEach, expect, it, onTestFinished, vi } from 'vitest' +import { act, cleanup, fireEvent, render, screen, within } from '@testing-library/react' +import { SessionId } from '@deepseek-ai/dsh-session' +import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' +import { + TeamDisableConfirmation, type TeamDisableConfirmationProps, type TeamDisableRequest, +} from '../src/client/TeamDisableConfirmation.tsx' +import { zh } from '../src/client/locales.ts' + +afterEach(cleanup) + +const ORIGINAL = SessionId('original-session') +const REPLACEMENT = SessionId('replacement-session') + +function props( + request: TeamDisableRequest | null = { sessionId: ORIGINAL }, + disableTeam = vi.fn(async () => null), +) { + const useDisableRequest: TeamDisableConfirmationProps['useDisableRequest'] = select => select(request) + const cancelDisable = vi.fn() + return { + props: { useDisableRequest, cancelDisable, disableTeam, t: makeTranslate(zh) } as TeamDisableConfirmationProps, + cancelDisable, + disableTeam, + } +} + +function pendingDisable() { + const result = Promise.withResolvers() + onTestFinished(async () => { + await act(async () => { result.resolve(null) }) + }) + return result +} + +it('renders nothing when no disable request exists', () => { + const input = props(null) + const view = render() + expect(view.container.childElementCount).toBe(0) + expect(screen.queryByRole('dialog')).toBeNull() + expect(screen.queryByRole('button')).toBeNull() + expect(input.disableTeam).not.toHaveBeenCalled() + expect(input.cancelDisable).not.toHaveBeenCalled() +}) + +it('shows the localized title, two paragraphs, and actions without a checkbox', () => { + const input = props() + render() + const dialog = within(screen.getByRole('dialog', { name: '确认停用智能体团队协作?' })) + expect(dialog.getByRole('heading', { name: '确认停用智能体团队协作?' })).toBeTruthy() + expect(dialog.getByText(zh['disable.description'], { selector: 'p' })).toBeTruthy() + expect(dialog.getByText(zh['disable.warning'], { selector: 'p' })).toBeTruthy() + expect(dialog.getAllByRole('paragraph')).toHaveLength(2) + const cancel = dialog.getByRole('button', { name: zh['disable.cancel'] }) + expect(cancel.hasAttribute('disabled')).toBe(false) + expect(document.activeElement).toBe(cancel) + expect(dialog.getByRole('button', { name: zh['disable.confirm'] }).hasAttribute('disabled')).toBe(false) + expect(dialog.getByRole('button', { name: zh['disable.close'] })).toBeTruthy() + expect(dialog.queryByRole('checkbox')).toBeNull() + expect(dialog.queryByRole('alert')).toBeNull() + expect(input.disableTeam).not.toHaveBeenCalled() +}) + +it.each(['cancel', 'close', 'Escape'] as const)('dismisses through %s without disabling Team', (action) => { + const input = props() + const view = render() + if (action === 'Escape') fireEvent.keyDown(document, { key: 'Escape' }) + else fireEvent.click(screen.getByRole('button', { name: zh[`disable.${action}`] })) + expect(input.cancelDisable).toHaveBeenCalledExactlyOnceWith(ORIGINAL) + expect(input.disableTeam).not.toHaveBeenCalled() + view.rerender( select(null)} />) + expect(screen.queryByRole('dialog')).toBeNull() + fireEvent.keyDown(document, { key: 'Escape' }) + expect(input.cancelDisable).toHaveBeenCalledTimes(1) +}) + +it('disables the originating Session once and blocks cancellation until success', async () => { + const result = pendingDisable() + const input = props({ sessionId: ORIGINAL }, vi.fn(() => result.promise)) + const view = render() + const confirm = screen.getByRole('button', { name: zh['disable.confirm'] }) + fireEvent.click(confirm) + expect(input.disableTeam).toHaveBeenCalledExactlyOnceWith(ORIGINAL) + expect(confirm.hasAttribute('disabled')).toBe(true) + const cancel = screen.getByRole('button', { name: zh['disable.cancel'] }) + expect(cancel.hasAttribute('disabled')).toBe(true) + fireEvent.click(confirm) + fireEvent.click(cancel) + fireEvent.click(screen.getByRole('button', { name: zh['disable.close'] })) + fireEvent.keyDown(document, { key: 'Escape' }) + expect(input.disableTeam).toHaveBeenCalledTimes(1) + expect(input.cancelDisable).not.toHaveBeenCalled() + expect(screen.getByRole('dialog')).toBeTruthy() + await act(async () => { result.resolve(null) }) + expect(input.cancelDisable).toHaveBeenCalledExactlyOnceWith(ORIGINAL) + view.rerender( select(null)} />) + expect(screen.queryByRole('dialog')).toBeNull() +}) + +it.each(['returned', 'error', 'string'] as const)('keeps a %s failure visible and allows retry', async (failure) => { + const result = pendingDisable() + const disableTeam = vi.fn() + .mockImplementationOnce(async () => { + if (failure === 'error') throw new Error('offline') + if (failure === 'string') throw 'offline' + return 'offline' + }) + .mockImplementationOnce(() => result.promise) + const input = props({ sessionId: ORIGINAL }, disableTeam) + const view = render() + await act(async () => { fireEvent.click(screen.getByRole('button', { name: zh['disable.confirm'] })) }) + expect(screen.getByRole('alert').textContent).toBe(input.props.t('mode.failed', { message: 'offline' })) + expect(input.cancelDisable).not.toHaveBeenCalled() + expect(screen.getByRole('button', { name: zh['disable.cancel'] }).hasAttribute('disabled')).toBe(false) + expect(screen.getByRole('button', { name: zh['disable.confirm'] }).hasAttribute('disabled')).toBe(false) + view.rerender( select({ sessionId: ORIGINAL })} />) + expect(screen.getByRole('alert').textContent).toContain('offline') + fireEvent.click(screen.getByRole('button', { name: zh['disable.confirm'] })) + expect(disableTeam.mock.calls).toEqual([[ORIGINAL], [ORIGINAL]]) + expect(screen.queryByRole('alert')).toBeNull() + expect(screen.getByRole('button', { name: zh['disable.confirm'] }).hasAttribute('disabled')).toBe(true) + await act(async () => { result.resolve(null) }) + expect(input.cancelDisable).toHaveBeenCalledExactlyOnceWith(ORIGINAL) +}) + +it('clears the previous failure when the request targets a different Session', async () => { + const input = props({ sessionId: ORIGINAL }, vi.fn() + .mockResolvedValueOnce('old failure').mockResolvedValueOnce(null)) + const view = render() + await act(async () => { fireEvent.click(screen.getByRole('button', { name: zh['disable.confirm'] })) }) + expect(screen.getByRole('alert').textContent).toContain('old failure') + view.rerender( select({ sessionId: REPLACEMENT })} />) + expect(screen.queryByRole('alert')).toBeNull() + expect(screen.getByRole('button', { name: zh['disable.confirm'] }).hasAttribute('disabled')).toBe(false) + await act(async () => { fireEvent.click(screen.getByRole('button', { name: zh['disable.confirm'] })) }) + expect(input.disableTeam.mock.calls).toEqual([[ORIGINAL], [REPLACEMENT]]) + expect(input.cancelDisable).toHaveBeenCalledExactlyOnceWith(REPLACEMENT) +}) + +it.each(['success', 'returned', 'error'] as const)('isolates a replacement request from the old pending %s', async (outcome) => { + const original = pendingDisable() + const replacement = pendingDisable() + const input = props({ sessionId: ORIGINAL }, vi.fn() + .mockImplementationOnce(() => original.promise).mockImplementationOnce(() => replacement.promise)) + const view = render() + fireEvent.click(screen.getByRole('button', { name: zh['disable.confirm'] })) + expect(screen.getByRole('button', { name: zh['disable.confirm'] }).hasAttribute('disabled')).toBe(true) + view.rerender( select({ sessionId: REPLACEMENT })} />) + const confirm = screen.getByRole('button', { name: zh['disable.confirm'] }) + expect(confirm.hasAttribute('disabled')).toBe(false) + expect(screen.getByRole('button', { name: zh['disable.cancel'] }).hasAttribute('disabled')).toBe(false) + expect(screen.queryByRole('alert')).toBeNull() + fireEvent.click(confirm) + expect(input.disableTeam.mock.calls).toEqual([[ORIGINAL], [REPLACEMENT]]) + await act(async () => { + if (outcome === 'error') original.reject(new Error('old failure')) + else original.resolve(outcome === 'success' ? null : 'old failure') + }) + expect(screen.getByRole('dialog', { name: zh['disable.title'] })).toBeTruthy() + expect(screen.queryByRole('alert')).toBeNull() + expect(confirm.hasAttribute('disabled')).toBe(true) + if (outcome === 'success') expect(input.cancelDisable).toHaveBeenCalledExactlyOnceWith(ORIGINAL) + else expect(input.cancelDisable).not.toHaveBeenCalled() + fireEvent.click(confirm) + fireEvent.keyDown(document, { key: 'Escape' }) + expect(input.disableTeam).toHaveBeenCalledTimes(2) + expect(input.cancelDisable).not.toHaveBeenCalledWith(REPLACEMENT) + await act(async () => { replacement.resolve(null) }) + expect(input.cancelDisable.mock.calls).toEqual(outcome === 'success' ? [[ORIGINAL], [REPLACEMENT]] : [[REPLACEMENT]]) +}) diff --git a/packages/client/ui-agent-team/tests/team-mode.client.spec.tsx b/packages/client/ui-agent-team/tests/team-mode.client.spec.tsx new file mode 100644 index 0000000000..13a1da8450 --- /dev/null +++ b/packages/client/ui-agent-team/tests/team-mode.client.spec.tsx @@ -0,0 +1,131 @@ +// @vitest-environment jsdom + +import { afterEach, expect, it, vi } from 'vitest' +import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' +import { SessionId } from '@deepseek-ai/dsh-session' +import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' +import { TeamModeToggle, type TeamModeToggleProps } from '../src/client/TeamModeToggle.tsx' +import { de, en, es, fr, ja, pt, ru, zh } from '../src/client/locales.ts' + +afterEach(() => { + cleanup() + vi.unstubAllGlobals() +}) + +function props(enabled: boolean | undefined, selectTeamMode = vi.fn(async () => null as string | null), child?: 'addressed' | 'listed', requestedModel: boolean | 'loading' = false): TeamModeToggleProps { + return { + sessionId: SessionId('root'), + useSession: (select: (value: { subagent: object | null }) => unknown) => select({ subagent: child === 'addressed' ? {} : null }), + useSessions: (select: (value: { byId: object }) => unknown) => select({ byId: child === 'listed' ? { root: { origin: 'subagent' } } : {} }), + useProjection: (key: string) => key === 'modelSelection' + ? requestedModel === 'loading' ? undefined : { lastUsed: requestedModel ? { provider: 'mock', model: 'mock' } : null, next: { provider: 'mock', model: 'mock' } } + : enabled === undefined ? undefined : { enabled, tasks: [] }, + locked: false, dismiss: vi.fn(), reportFailure: vi.fn(), selectTeamMode, requestDisableConfirmation: vi.fn(), + t: makeTranslate(zh), + } as TeamModeToggleProps +} + +it('renders only available mode and waits for persisted state before displaying a new selection', async () => { + let finish!: (result: string | null) => void + const select = vi.fn(() => new Promise((resolve) => { finish = resolve })) + const view = render() + expect(screen.queryByRole('switch')).toBeNull() + view.rerender() + const toggle = screen.getByRole('switch', { name: '启用智能体团队协作' }) + fireEvent.click(toggle) + expect(select).toHaveBeenCalledWith(true) + expect(toggle.getAttribute('aria-checked')).toBe('false') + expect(toggle.hasAttribute('disabled')).toBe(true) + await act(async () => { finish(null) }) + view.rerender() + expect(toggle.getAttribute('aria-checked')).toBe('true') +}) + +it.each(['returned', 'error', 'string'])('reports rejected mode writes through the menu host (%s)', async (failure) => { + const p = props(false, vi.fn(async () => { + if (failure === 'error') throw new Error('offline') + if (failure === 'string') throw 'offline' + return 'offline' + })) + render() + await act(async () => { fireEvent.click(screen.getByRole('switch')) }) + expect(p.reportFailure).toHaveBeenCalledWith(expect.stringContaining('offline')) + expect(screen.getByRole('switch').getAttribute('aria-checked')).toBe('false') +}) + +it.each(Object.entries({ zh, en, ja, es, fr, de, ru, pt }).flatMap(([locale, dictionary]) => + [false, true].map(locked => ({ locale, dictionary, locked })), +))('opens the $locale explanation without changing Team mode (locked: $locked)', ({ dictionary, locked }) => { + vi.stubGlobal('ResizeObserver', class { + observe() {} + unobserve() {} + disconnect() {} + }) + const input = props(false) + render() + const info = screen.getByRole('button', { name: dictionary['mode.infoLabel'] }) + expect(info.hasAttribute('disabled')).toBe(false) + fireEvent.click(info) + const tooltip = screen.getByRole('tooltip', { hidden: true }) + expect(tooltip.textContent).toBe(dictionary['mode.description']) + expect(info.getAttribute('aria-describedby')).toBe(tooltip.id) + expect(document.body.contains(tooltip)).toBe(true) + expect(input.selectTeamMode).not.toHaveBeenCalled() + expect(screen.getByRole('switch').getAttribute('aria-checked')).toBe('false') + fireEvent.keyDown(info, { key: 'Escape' }) + expect(screen.queryByRole('tooltip', { hidden: true })).toBeNull() + fireEvent.click(info) + fireEvent.pointerDown(document.body) + expect(screen.queryByRole('tooltip', { hidden: true })).toBeNull() + expect(input.selectTeamMode).not.toHaveBeenCalled() +}) + +it('requests confirmation before disabling after a recorded model request', () => { + const input = props(true, undefined, undefined, true) + render() + fireEvent.click(screen.getByRole('switch')) + expect(input.dismiss).toHaveBeenCalledOnce() + expect(input.dismiss).toHaveBeenCalledBefore(vi.mocked(input.requestDisableConfirmation)) + expect(input.requestDisableConfirmation).toHaveBeenCalledOnce() + expect(input.selectTeamMode).not.toHaveBeenCalled() + expect(screen.getByRole('switch').getAttribute('aria-checked')).toBe('true') +}) + +it.each([false, true])('enables directly regardless of model history (requested: %s)', async (requested) => { + const input = props(false, undefined, undefined, requested) + render() + await act(async () => { fireEvent.click(screen.getByRole('switch')) }) + expect(input.selectTeamMode).toHaveBeenCalledWith(true) + expect(input.requestDisableConfirmation).not.toHaveBeenCalled() +}) + +it('disables directly when a model is selected but no request has been recorded', async () => { + const input = props(true) + render() + await act(async () => { fireEvent.click(screen.getByRole('switch')) }) + expect(input.selectTeamMode).toHaveBeenCalledWith(false) + expect(input.requestDisableConfirmation).not.toHaveBeenCalled() +}) + +it('waits for model history before allowing the switch to change', () => { + const input = props(true, undefined, undefined, 'loading') + render() + expect(screen.getByRole('switch').hasAttribute('disabled')).toBe(true) + fireEvent.click(screen.getByRole('switch')) + expect(input.selectTeamMode).not.toHaveBeenCalled() + expect(input.requestDisableConfirmation).not.toHaveBeenCalled() +}) + +it('disables selection while the Session is locked', () => { + render() + expect(screen.getByRole('switch').hasAttribute('disabled')).toBe(true) +}) + +it.each(['addressed', 'listed'] as const)('hides the switch on a child Session (%s)', (source) => { + const input = props(false, undefined, source) + const useProjection = vi.fn((): never => { throw new Error('Child must not read Team mode') }) + render() + expect(screen.queryByRole('switch')).toBeNull() + expect(useProjection).not.toHaveBeenCalled() + expect(input.selectTeamMode).not.toHaveBeenCalled() +}) diff --git a/packages/client/ui-agent-team/tsconfig.json b/packages/client/ui-agent-team/tsconfig.json new file mode 100644 index 0000000000..429b9adac3 --- /dev/null +++ b/packages/client/ui-agent-team/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../../tsconfig.base.client.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": ["src"], + "references": [ + { "path": "../../../vendor/cordis" }, + { "path": "../../team/agent-team" }, + { "path": "../../api/session-controller/tsconfig.client.json" }, + { "path": "../../api/remotes/tsconfig.client.json" }, + { "path": "../ui-model-selection" }, + { "path": "../ui-layout" }, + { "path": "../../core/agent" }, + { "path": "../ui-subagent" }, + { "path": "../locale/tsconfig.client.json" }, + { "path": "../store" }, + { "path": "../ui-conversation/tsconfig.client.json" }, + { "path": "../ui-chat/tsconfig.client.json" }, + { "path": "../../util/values" }, + { "path": "../ui-primitives" }, + { "path": "../ui-renderer" }, + { "path": "../ui-slots" }, + { "path": "../../core/session" } + ] +} diff --git a/packages/experimental/client-ui-agent-team/tsdown.config.ts b/packages/client/ui-agent-team/tsdown.config.ts similarity index 54% rename from packages/experimental/client-ui-agent-team/tsdown.config.ts rename to packages/client/ui-agent-team/tsdown.config.ts index 71d5bd77f8..7aa03cebc1 100644 --- a/packages/experimental/client-ui-agent-team/tsdown.config.ts +++ b/packages/client/ui-agent-team/tsdown.config.ts @@ -1,3 +1,3 @@ -import { clientBundle } from '../../client/tsdown.client.ts' +import { clientBundle } from '../tsdown.client.ts' export default clientBundle(import.meta.dirname, ['lib/types/index.js']) diff --git a/packages/client/ui-chat/README.i18n.yaml b/packages/client/ui-chat/README.i18n.yaml index 0966845bde..54168bf257 100644 --- a/packages/client/ui-chat/README.i18n.yaml +++ b/packages/client/ui-chat/README.i18n.yaml @@ -18,11 +18,11 @@ en: 9386791a9e5efa00 zh: 7cb9f3bbc42319d6 /deepseek-ai-dsh-client-ui-chat/hidden-chat-rows: - en: b0c4e6711d69d2f8 - zh: 09add0ea1a243922 + en: 91b7c7a5263eda35 + zh: 16d39b45edee038f /deepseek-ai-dsh-client-ui-chat/command-and-failure-rows: - en: be90c24392fb021b - zh: 992b8745b385f9ab + en: c5b23d0ed29a0425 + zh: 689bab2c8253294a /deepseek-ai-dsh-client-ui-chat/turn-token-usage: en: cfdfe3d99d3f4e09 zh: 9330bad3d3f7a689 diff --git a/packages/client/ui-chat/README.md b/packages/client/ui-chat/README.md index be43482e8a..b23b9dc3e1 100644 --- a/packages/client/ui-chat/README.md +++ b/packages/client/ui-chat/README.md @@ -54,7 +54,7 @@ Settings → General → Open chat links in selects the destination for ordinary ## Hidden Chat rows -Chat omits system-prompt, ordinary Context injection, and `permission` command rows in every work-details mode. Context containing tool additions or removals remains visible. The filter changes neither recorded Session events nor Trajectory inspection. Non-human Turn triggers remain independent, bordered notices with a contrasting neutral background; other command rows remain in Chat. +Chat omits system-prompt, ordinary Context injection, and `permission` command rows in every work-details mode. It also hides pending and successful `/team on` and `/team off` receipts, identified by their recorded name and trimmed arguments; failures and commands with missing or unrecognized arguments remain visible. Context containing tool additions or removals remains visible. The filter changes neither recorded Session events nor Trajectory inspection and excludes hidden receipts from flow spacing and process summaries. Non-human Turn triggers remain independent, bordered notices with a contrasting neutral background; other command rows remain in Chat. When an Assistant attempt retires without a visible message, Chat hides its already-published Node instead of removing its key. A retry in the same Step reuses that key when visible content returns. This also applies when the loaded window lacks the Step start. @@ -63,6 +63,8 @@ Text and reasoning contribute to Assistant visibility only when they contain a n ## Command and failure rows +Subagent result notices use the Users icon and show only the localized execution outcome in their heading, without a task description or child ID. Completed means that execution ended normally, not that its output passed validation. Historical notices without a readable outcome retain the generic title; unfamiliar outcomes display “Subagent run ended unexpectedly”. Expanded notices preserve their original model-facing body. + Generic command rows retain the ordinary command glyph in every lifecycle state; failure remains explicit through the row state and summary. Every terminal Turn failure renders its inline red-dot row; a quota failure's row states the neutral `message.failure.quota` copy instead of the provider message. The transient notice for a newly appended `QUOTA` or `ACCOUNT_QUOTA` comes from this package's frame-wide entry in `shell.overlay`, which outlives the Chat panel: it offers the one live notice to the `shell.quota-notice` chain and falls back to its own warning Toast, while an entry that claims the code replaces that fallback. Only Sessions this Client has bound and materialized publish; quota failures in Sessions it never opened do not. A newer notice replaces the current one unless a claiming entry retains it with `keepOpen()`: that call returns a release the caller owns and must run on unmount, any live hold keeps the claiming entry mounted and drops later notices, and releasing resumes later notices without replaying the dropped ones. The fallback Toast has no deferral of its own: while the Desktop account's opaque native Platform page covers the document, it still runs underneath and its display timer may elapse unseen, dismissing the notice itself, so only the persistent failure row remains. A release drops only its own hold, so one that runs after a dismissal or a newer hold leaves that newer hold intact. Dismissal and sign-out clear every hold, and dropped notices are not queued while their persistent failure rows still render. History replacement and pagination never publish a notice. Intermediate retries do not create a terminal row; output-token limits use the amber warning dot. A Turn that restart recovery closed as `interrupted` renders a localized stopped row stating that the DSH process exited mid-Turn, without a code chip, and its process summary reads Stopped. Chat adapts Markdown targets at `MarkdownDelegateProvider.getTriggerProps`. The assistant-node Slot obtains `useContextMenu` directly and passes a file-path adapter to the optional mention service; intermediate flow and node owners carry no menu callbacks. Paths resolve against the originating Session’s current working directory when the gesture begins. Menus retain that captured path and line even if focus changes. Undeclared plugin links keep native behavior. diff --git a/packages/client/ui-chat/README.zh.md b/packages/client/ui-chat/README.zh.md index 9c0314fbd8..e5f8add05b 100644 --- a/packages/client/ui-chat/README.zh.md +++ b/packages/client/ui-chat/README.zh.md @@ -54,7 +54,7 @@ Session 作用域的 `conversation.chat.user.content` Factory 只渲染用户内 ## Chat 隐藏的行 -Chat 在所有工作步骤展示模式下都不显示系统提示词行、普通上下文注入和 `permission` 命令行。包含工具添加或移除记录的上下文仍然可见。该过滤不改变已记录的 Session 事件或 Trajectory 查看能力。非人工轮次触发仍作为独立通知显示,其他命令行仍保留在 Chat 中。 +Chat 在所有工作步骤展示模式下都不显示系统提示词行、普通上下文注入和 `permission` 命令行。记录中的命令名和去除首尾空白后的参数确定为 `/team on` 或 `/team off` 时,也隐藏执行中与成功回执;失败以及参数缺失或无法识别的命令仍然可见。包含工具添加或移除记录的上下文仍然可见。该过滤不改变已记录的 Session 事件或 Trajectory 查看能力,隐藏回执也不计入行间距和过程摘要。非人工轮次触发仍作为独立通知显示,其他命令行仍保留在 Chat 中。 Assistant 尝试结束且没有可见消息时,Chat 隐藏已发布的 Node,不移除其 key。同一 Step 的重试再次产生可见内容时,复用该 key。已加载窗口缺少 Step 起点时也遵循此规则。 @@ -63,6 +63,8 @@ Assistant 尝试结束且没有可见消息时,Chat 隐藏已发布的 Node, ## 指令与失败行 +子智能体结果通知使用 Users 图标,标题仅显示本地化的执行状态,不附加任务描述或子智能体 ID。完成表示执行正常结束,不表示输出已通过验证。历史通知没有可读取的结束状态时保留通用标题;未知状态显示“子智能体异常结束”。展开后的通知保留原始模型可见正文。 + 通用指令行在所有生命周期状态中都保留普通指令图标;失败仍通过行状态与摘要表达。每个终止轮次都渲染自己的红点行;欠费失败的行使用中立的 `message.failure.quota` 文案,而不是提供方消息。新追加的 `QUOTA` 或 `ACCOUNT_QUOTA` 产生的瞬时提示来自本包注册在 `shell.overlay` 的全局条目,它不随 Chat 面板消失:该条目把唯一一条实时提示交给 `shell.quota-notice` 链,并在无人接管时回退到自己的警告 Toast,接管该错误码的条目会替换该回退。只有本 Client 已绑定并已物化的 Session 会发布提示;它从未打开的 Session 中的欠费不会发布。更新的提示会替换当前提示,除非认领条目用 `keepOpen()` 保持住当前提示:该调用返回一个由调用方持有、必须在卸载时调用的释放函数,只要存在保持,认领条目就继续挂载,后续提示被丢弃,释放后恢复后续提示但不重放已丢弃的提示。回退 Toast 自身没有延后策略:当 Desktop 账号的不透明原生 Platform 页盖住本页时,它仍会在下方运行,其显示计时器可能在不可见时走完并自行撤下提示,因此只留下持久化失败行。释放只移除自己的保持,因此它在关闭或更新的保持之后执行时不会影响更新的保持。关闭与退出登录会清空所有保持;被丢弃的提示不排队,其对应的持久化失败行仍会渲染。历史替换和分页不会发布提示。中间重试不会创建终止行;输出 token 上限使用琥珀色警告点。重启恢复以 `interrupted` 结束的轮次会渲染本地化的停止行,说明 DSH 进程在轮次中途退出,不显示代码标签,其过程摘要显示“已停止”。 Chat 在 `MarkdownDelegateProvider.getTriggerProps` 接入处适配 Markdown 目标。助手节点 Slot 直接获取 `useContextMenu`,向可选的文件引用服务传入路径适配函数;消息流和节点中间层不再携带菜单回调。手势发生时按原始 Session 的当前工作目录解析路径,菜单保留捕获的路径和行号,不受焦点变化影响。未声明的插件链接保留原生行为。 diff --git a/packages/client/ui-chat/src/client/chat/TurnTriggerNodeView.module.css b/packages/client/ui-chat/src/client/chat/TurnTriggerNodeView.module.css index 4a860b611f..4bf309274c 100644 --- a/packages/client/ui-chat/src/client/chat/TurnTriggerNodeView.module.css +++ b/packages/client/ui-chat/src/client/chat/TurnTriggerNodeView.module.css @@ -26,7 +26,7 @@ .header:hover { color: var(--dsw-alias-label-secondary); } } .icon { display: inline-flex; flex: none; color: inherit; } -.title { flex: none; font-size: var(--dsh-content-font-size, 14px); } +.title { flex: 1; min-width: 0; overflow-wrap: anywhere; font-size: var(--dsh-content-font-size, 14px); } .time { flex: none; margin-left: auto; color: inherit; font: var(--dsw-font-xxs-12); } .chevron, .openChevron { flex: none; color: inherit; } .openChevron { transform: rotate(180deg); } diff --git a/packages/client/ui-chat/src/client/chat/TurnTriggerNodeView.tsx b/packages/client/ui-chat/src/client/chat/TurnTriggerNodeView.tsx index afa2106ca9..e6e366666a 100644 --- a/packages/client/ui-chat/src/client/chat/TurnTriggerNodeView.tsx +++ b/packages/client/ui-chat/src/client/chat/TurnTriggerNodeView.tsx @@ -4,7 +4,7 @@ import { IconAgentPresetOutlineRegular, IconClockOutlineRegular, IconBranchOutlineRegular, IconChevronDownOutlineRegular, IconContextInjectionOutlineRegular, IconCordisPluginOutlineRegular, IconGoalOutlineRegular, IconGlobeOutlineRegular, IconPaperPlaneOutlineRegular, IconQueueOutlineRegular, - type IconProps, + IconUsersOutlineRegular, type IconProps, } from '@deepseek-ai/dsh-client-ui-primitives' import type { ChatNodeViewProps } from '../contract/slots.ts' import { formatMessageClock } from './message-chrome.ts' @@ -17,7 +17,7 @@ const TRIGGER_ICONS: Record> = { goal: IconGoalOutlineRegular, agent: IconPaperPlaneOutlineRegular, team: IconAgentPresetOutlineRegular, - subagent: IconAgentPresetOutlineRegular, + subagent: IconUsersOutlineRegular, github: IconBranchOutlineRegular, webhook: IconGlobeOutlineRegular, schedule: IconClockOutlineRegular, diff --git a/packages/client/ui-chat/src/client/chat/turn-trigger.ts b/packages/client/ui-chat/src/client/chat/turn-trigger.ts index d4912668ea..ade8093490 100644 --- a/packages/client/ui-chat/src/client/chat/turn-trigger.ts +++ b/packages/client/ui-chat/src/client/chat/turn-trigger.ts @@ -13,6 +13,20 @@ function field(source: Record, key: string): string { return typeof source[key] === 'string' ? source[key] : '' } +/** Missing or malformed historical reasons stay generic; unfamiliar reasons never imply success. */ +function subagentTitle(reason: unknown): ChatKey { + if (typeof reason !== 'string' || reason === '') return 'message.trigger.subagent' + switch (reason) { + case 'completed': return 'message.trigger.subagent.completed' + case 'aborted': return 'message.trigger.subagent.aborted' + case 'error': return 'message.trigger.subagent.error' + case 'max-tokens': return 'message.trigger.subagent.maxTokens' + case 'refusal': return 'message.trigger.subagent.refusal' + // Backends may extend the recorded reason vocabulary. + default: return 'message.trigger.subagent.unknown' + } +} + /** * Describe a waking message using its recorded source. * @param node - durable context, including the original notification body. @@ -40,11 +54,10 @@ export function turnTriggerDetails(node: ContextMessageNode): { title = 'message.trigger.team' icon = 'team' break - case 'subagent-settled': { - title = 'message.trigger.subagent' + case 'subagent-settled': + title = subagentTitle(source.stopReason) icon = 'subagent' break - } case 'webhook': { const github = field(source, 'provider') === 'github' title = github ? 'message.trigger.github' : 'message.trigger.webhook' diff --git a/packages/client/ui-chat/src/client/contract/chat-visibility.ts b/packages/client/ui-chat/src/client/contract/chat-visibility.ts index 2fd60b4b1d..8f1c6f89b5 100644 --- a/packages/client/ui-chat/src/client/contract/chat-visibility.ts +++ b/packages/client/ui-chat/src/client/contract/chat-visibility.ts @@ -2,15 +2,21 @@ import type { ChatNode } from './chat-nodes.ts' /** - * Exclude system prompts, ordinary Context, and permission commands from visible Chat rows. - * Context containing tool changes retains its notice row. + * Exclude system prompts, ordinary Context, permission commands, and normal Team toggle receipts. + * Failed Team toggles and Context containing tool changes remain visible. * @param node - projected Chat node. * @returns whether the node contributes a visible Chat row. */ export function isVisibleChatNode(node: ChatNode): boolean { + if (node.kind === 'command') { + if (node.data.name === 'permission') return false + if (node.data.name === 'team' && node.data.outcome?.kind !== 'error') { + const selection = node.data.args?.trim() + if (selection === 'on' || selection === 'off') return false + } + } return node.visibility === 'visible' && node.kind !== 'system-prompt' && (node.kind !== 'context' || node.data.content.some(block => block.type === 'tool-addition' || block.type === 'tool-removal')) - && !(node.kind === 'command' && node.data.name === 'permission') } diff --git a/packages/client/ui-chat/src/client/locale.ts b/packages/client/ui-chat/src/client/locale.ts index 6f0e6eda52..221ab256e8 100644 --- a/packages/client/ui-chat/src/client/locale.ts +++ b/packages/client/ui-chat/src/client/locale.ts @@ -55,6 +55,12 @@ export const zh = { 'message.trigger.agent': '收到任务消息', 'message.trigger.team': '收到团队消息', 'message.trigger.subagent': '子任务状态更新', + 'message.trigger.subagent.completed': '子智能体已完成任务', + 'message.trigger.subagent.aborted': '子智能体任务已停止', + 'message.trigger.subagent.error': '子智能体执行失败', + 'message.trigger.subagent.maxTokens': '子智能体已达 token 上限', + 'message.trigger.subagent.refusal': '子智能体拒绝执行', + 'message.trigger.subagent.unknown': '子智能体异常结束', 'message.trigger.github': '收到 GitHub 事件', 'message.trigger.webhook': '收到外部事件', 'message.trigger.schedule': '自动化任务', @@ -257,6 +263,12 @@ export const en = { 'message.trigger.agent': 'Task message received', 'message.trigger.team': 'Team message received', 'message.trigger.subagent': 'Subtask status updated', + 'message.trigger.subagent.completed': 'Subagent run completed', + 'message.trigger.subagent.aborted': 'Subagent run stopped', + 'message.trigger.subagent.error': 'Subagent run failed', + 'message.trigger.subagent.maxTokens': 'Subagent reached the token limit', + 'message.trigger.subagent.refusal': 'Subagent declined the task', + 'message.trigger.subagent.unknown': 'Subagent run ended unexpectedly', 'message.trigger.github': 'GitHub event received', 'message.trigger.webhook': 'External event received', 'message.trigger.schedule': 'Automation task', @@ -456,6 +468,12 @@ export const ja = { 'message.trigger.agent': 'タスクメッセージを受信しました', 'message.trigger.team': 'チームメッセージを受信しました', 'message.trigger.subagent': 'サブタスクの状態が更新されました', + 'message.trigger.subagent.completed': 'サブエージェントの実行が完了しました', + 'message.trigger.subagent.aborted': 'サブエージェントの実行が停止しました', + 'message.trigger.subagent.error': 'サブエージェントの実行が失敗しました', + 'message.trigger.subagent.maxTokens': 'サブエージェントがトークンの上限に達しました', + 'message.trigger.subagent.refusal': 'サブエージェントがタスクを拒否しました', + 'message.trigger.subagent.unknown': 'サブエージェントの実行が予期せず終了しました', 'message.trigger.github': 'GitHubイベントを受信しました', 'message.trigger.webhook': '外部イベントを受信しました', 'message.trigger.schedule': '自動化タスク', @@ -655,6 +673,12 @@ export const es = { 'message.trigger.agent': 'Mensaje de tarea recibido', 'message.trigger.team': 'Mensaje del equipo recibido', 'message.trigger.subagent': 'Estado de subtarea actualizado', + 'message.trigger.subagent.completed': 'Ejecución del subagente completada', + 'message.trigger.subagent.aborted': 'Ejecución del subagente detenida', + 'message.trigger.subagent.error': 'La ejecución del subagente falló', + 'message.trigger.subagent.maxTokens': 'El subagente alcanzó el límite de tokens', + 'message.trigger.subagent.refusal': 'El subagente rechazó la tarea', + 'message.trigger.subagent.unknown': 'La ejecución del subagente terminó de forma inesperada', 'message.trigger.github': 'Evento GitHub recibido', 'message.trigger.webhook': 'Evento externo recibido', 'message.trigger.schedule': 'Tarea de automatización', @@ -854,6 +878,12 @@ export const fr = { 'message.trigger.agent': 'Message de tâche reçu', 'message.trigger.team': "Message d'équipe reçu", 'message.trigger.subagent': 'Statut de la sous-tâche mis à jour', + 'message.trigger.subagent.completed': 'Exécution du sous-agent terminée', + 'message.trigger.subagent.aborted': 'Exécution du sous-agent arrêtée', + 'message.trigger.subagent.error': 'Échec de l’exécution du sous-agent', + 'message.trigger.subagent.maxTokens': 'Le sous-agent a atteint la limite de jetons', + 'message.trigger.subagent.refusal': 'Le sous-agent a refusé la tâche', + 'message.trigger.subagent.unknown': 'L’exécution du sous-agent s’est terminée de façon inattendue', 'message.trigger.github': 'Événement GitHub reçu', 'message.trigger.webhook': 'Événement externe reçu', 'message.trigger.schedule': "Tâche d'automatisation", @@ -1053,6 +1083,12 @@ export const de = { 'message.trigger.agent': 'Aufgabennachricht erhalten', 'message.trigger.team': 'Teamnachricht erhalten', 'message.trigger.subagent': 'Unteraufgabenstatus aktualisiert', + 'message.trigger.subagent.completed': 'Ausführung des Subagenten abgeschlossen', + 'message.trigger.subagent.aborted': 'Ausführung des Subagenten gestoppt', + 'message.trigger.subagent.error': 'Ausführung des Subagenten fehlgeschlagen', + 'message.trigger.subagent.maxTokens': 'Subagent hat das Token-Limit erreicht', + 'message.trigger.subagent.refusal': 'Subagent hat die Aufgabe abgelehnt', + 'message.trigger.subagent.unknown': 'Ausführung des Subagenten unerwartet beendet', 'message.trigger.github': 'GitHub-Ereignis empfangen', 'message.trigger.webhook': 'Externes Ereignis empfangen', 'message.trigger.schedule': 'Automatisierungsaufgabe', @@ -1252,6 +1288,12 @@ export const ru = { 'message.trigger.agent': 'Сообщение о задаче получено', 'message.trigger.team': 'Сообщение команды получено', 'message.trigger.subagent': 'Статус подзадачи обновлен', + 'message.trigger.subagent.completed': 'Работа субагента завершена', + 'message.trigger.subagent.aborted': 'Работа субагента остановлена', + 'message.trigger.subagent.error': 'Работа субагента завершилась с ошибкой', + 'message.trigger.subagent.maxTokens': 'Субагент достиг лимита токенов', + 'message.trigger.subagent.refusal': 'Субагент отказался выполнять задачу', + 'message.trigger.subagent.unknown': 'Работа субагента неожиданно завершилась', 'message.trigger.github': 'Событие GitHub получено', 'message.trigger.webhook': 'Получено внешнее событие', 'message.trigger.schedule': 'Задача автоматизации', @@ -1451,6 +1493,12 @@ export const pt = { 'message.trigger.agent': 'Mensagem de tarefa recebida', 'message.trigger.team': 'Mensagem da equipe recebida', 'message.trigger.subagent': 'Status da subtarefa atualizado', + 'message.trigger.subagent.completed': 'Execução do subagente concluída', + 'message.trigger.subagent.aborted': 'Execução do subagente interrompida', + 'message.trigger.subagent.error': 'Falha na execução do subagente', + 'message.trigger.subagent.maxTokens': 'O subagente atingiu o limite de tokens', + 'message.trigger.subagent.refusal': 'O subagente recusou a tarefa', + 'message.trigger.subagent.unknown': 'A execução do subagente terminou inesperadamente', 'message.trigger.github': 'GitHub evento recebido', 'message.trigger.webhook': 'Evento externo recebido', 'message.trigger.schedule': 'Tarefa de automação', diff --git a/packages/client/ui-chat/tests/chat-visibility.client.spec.ts b/packages/client/ui-chat/tests/chat-visibility.client.spec.ts new file mode 100644 index 0000000000..70b2d38fef --- /dev/null +++ b/packages/client/ui-chat/tests/chat-visibility.client.spec.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from 'vitest' +import { CommandId } from '@deepseek-ai/dsh-commands/brand' +import type { ChatNode } from '../src/client/contract/chat-nodes.ts' +import { isVisibleChatNode } from '../src/client/contract/chat-visibility.ts' + +const placement = { + key: 'command:test', id: 'test', target: 'chat', anchorSeq: 1, + location: { kind: 'unresolved' }, visibility: 'visible', +} as const + +function command(args: string | null, outcome: ChatNode<'command'>['data']['outcome'], name: string | null = 'team'): ChatNode<'command'> { + return { + ...placement, kind: 'command', + data: { kind: 'command', seq: 1, time: 0, commandId: CommandId('test'), name, args, outcome }, + } +} + +describe('Chat-only command visibility', () => { + it.each(['on', 'off', ' on ', '\toff\n'])('omits pending and successful Team selection %j without rewriting the record', (args) => { + for (const outcome of [null, { kind: 'success', text: 'Unchanged recorded output' }] as const) { + const node = command(args, outcome) + const before = JSON.stringify(node) + expect(isVisibleChatNode(node)).toBe(false) + expect(JSON.stringify(node)).toBe(before) + } + }) + + it.each(['on', 'off', null, 'invalid'])('keeps a failed Team command visible for input %j', (args) => { + expect(isVisibleChatNode(command(args, { kind: 'error', text: 'The operation failed' }))).toBe(true) + }) + + it.each(['ON', 'status', '', null])('does not infer a toggle from result text for input %j', (args) => { + expect(isVisibleChatNode(command(args, { kind: 'success', text: 'Agent Teams enabled.' }))).toBe(true) + expect(isVisibleChatNode(command(args, null))).toBe(true) + }) + + it.each(['other', null])('retains unrelated and window-cut commands named %j', (name) => { + expect(isVisibleChatNode(command('on', { kind: 'success', text: 'done' }, name))).toBe(true) + }) + + it('retains a failed toggle after its hidden pending record settles', () => { + const pending = command('on', null) + const failed = { ...pending, data: { ...pending.data, outcome: { kind: 'error' as const, text: 'Save failed' } } } + expect(isVisibleChatNode(pending)).toBe(false) + expect(isVisibleChatNode(failed)).toBe(true) + }) + + it('keeps the existing permission and explicit hidden-node policies', () => { + expect(isVisibleChatNode(command('on', { kind: 'error', text: 'failure' }, 'permission'))).toBe(false) + expect(isVisibleChatNode({ ...command('on', { kind: 'error', text: 'failure' }), visibility: 'hidden' })).toBe(false) + }) +}) + +it('keeps ordinary prompt/context hidden but retains tool changes', () => { + const prompt: ChatNode<'system-prompt'> = { ...placement, kind: 'system-prompt', data: { text: 'system instructions' } } + const context: ChatNode<'context'> = { + ...placement, kind: 'context', + data: { + kind: 'context', seq: 1, time: 0, content: [{ type: 'text', text: 'context' }], source: null, + producer: { role: 'inject', label: null }, form: null, + }, + } + expect(isVisibleChatNode(prompt)).toBe(false) + expect(isVisibleChatNode(context)).toBe(false) + for (const type of ['tool-addition', 'tool-removal'] as const) { + expect(isVisibleChatNode({ ...context, data: { ...context.data, content: [{ type, toolName: 'example' }] } })).toBe(true) + } +}) diff --git a/packages/client/ui-chat/tests/turn-trigger.client.spec.tsx b/packages/client/ui-chat/tests/turn-trigger.client.spec.tsx index c45672dac5..bbb2999cc7 100644 --- a/packages/client/ui-chat/tests/turn-trigger.client.spec.tsx +++ b/packages/client/ui-chat/tests/turn-trigger.client.spec.tsx @@ -2,7 +2,7 @@ import { afterEach, describe, expect, it } from 'vitest' import { cleanup, fireEvent, render } from '@testing-library/react' import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' -import { IconClockOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives' +import { IconClockOutlineRegular, IconUsersOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives' import type { ChatNode } from '../src/client/contract/chat-nodes.ts' import { TurnTriggerNodeView } from '../src/client/chat/TurnTriggerNodeView.tsx' import { turnTriggerDetails } from '../src/client/chat/turn-trigger.ts' @@ -72,6 +72,37 @@ describe('Turn trigger notices', () => { expect(node.data.source).toEqual({ kind: 'schedule' }) }) + it.each([ + ['completed', 'Subagent run completed', '子智能体已完成任务'], + ['aborted', 'Subagent run stopped', '子智能体任务已停止'], + ['error', 'Subagent run failed', '子智能体执行失败'], + ['max-tokens', 'Subagent reached the token limit', '子智能体已达 token 上限'], + ['refusal', 'Subagent declined the task', '子智能体拒绝执行'], + ['backend-specific', 'Subagent run ended unexpectedly', '子智能体异常结束'], + ])('presents only the recorded %s outcome in both locales', (stopReason, english, chinese) => { + const source = { kind: 'subagent-settled', stopReason, senderSessionId: 'child-1' } + const node = trigger(source) + const view = render() + expect(view.getByRole('button').textContent).toContain(english) + expect(view.getByRole('button').textContent).not.toContain(' · ') + const reference = render() + expect(view.getByRole('button').querySelector('svg')?.outerHTML) + .toBe(reference.container.querySelector('svg')?.outerHTML) + view.rerender() + expect(view.getByRole('button').textContent).toContain(chinese) + expect(view.getByRole('button').textContent).not.toContain(' · ') + fireEvent.click(view.getByRole('button')) + expect(view.container.querySelector('[data-context-text]')?.textContent) + .toBe('Recorded notification\nwith its original body.') + expect(node.data.source).toEqual(source) + }) + + it.each([undefined, null, false, 12, {}, '', []])('does not infer an absent or malformed outcome %j from English text', (stopReason) => { + const node = trigger({ kind: 'subagent-settled', stopReason, summary: 'Background subagent child-1 finished.' }) + node.data.content = [{ type: 'text', text: 'Background subagent child-1 finished.' }] + expect(turnTriggerDetails(node.data)).toEqual({ title: 'message.trigger.subagent', icon: 'subagent' }) + }) + it('opens the recorded notice body and closes it independently of the Turn', () => { const view = render() const button = view.getByRole('button') diff --git a/packages/client/ui-commands/src/client/presentation.ts b/packages/client/ui-commands/src/client/presentation.ts index f5abacd2a4..95204d468e 100644 --- a/packages/client/ui-commands/src/client/presentation.ts +++ b/packages/client/ui-commands/src/client/presentation.ts @@ -2,7 +2,8 @@ import type { ComponentType } from 'react' import type { InputTriggerCandidate } from '@deepseek-ai/dsh-client-ui-input-trigger/client' import { - IconCompactOutlineRegular, IconDownloadOutlineRegular, IconGoalOutlineRegular, IconPaperPlaneOutlineRegular, IconPlanOutlineRegular, + IconCompactOutlineRegular, IconDownloadOutlineRegular, + IconGoalOutlineRegular, IconPaperPlaneOutlineRegular, IconPlanOutlineRegular, PermissionIconFullAccessRegular, } from '@deepseek-ai/dsh-client-ui-primitives' import type { IconProps } from '@deepseek-ai/dsh-client-ui-primitives' diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index f17358e4b5..6c078adbe3 100644 --- a/packages/client/ui-conversation/README.i18n.yaml +++ b/packages/client/ui-conversation/README.i18n.yaml @@ -21,8 +21,8 @@ en: cc4dafd1aae02468 zh: be6b67724b7ac4a3 /deepseek-ai-dsh-client-ui-conversation/shell-and-standard-props: - en: 99ec447f0a6bda48 - zh: 922cf64666676530 + en: 3fa2fb91ad383c05 + zh: 1e1edd7f9a830377 /deepseek-ai-dsh-client-ui-conversation/temporary-composer-entries: en: 59cd63057dfb6eeb zh: b2e2c86e810b7bb8 diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index 14f54dc287..cc60deae1c 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -124,7 +124,7 @@ A default send waits for the Host: Enter captures the draft text, its reference Queued submission echoes show “Sending…” beside disabled edit, remove, and steer buttons; a collapsed dock keeps the sending status in its header. A matching Host queue row replaces the echo and enables each action according to its normal text-content and running-state requirements. Prompt acknowledgement alone does not enable queue actions. A refused or failed submission publishes no echo and leaves the draft in the composer with an error notice. A refusal from the Agent admission stage (`session/prompt-rejected`) shows its own text, unless `presented` says the refusing plugin explains it in its own interface, in which case the composer adds no notice. -Disabled Send and Stop buttons suppress their tooltips, including a Stop button that becomes a disabled Send button when the turn ends. While a normal composer is running, its primary pointer action remains Stop when the draft is empty or input is unavailable. Actionable text or attachments switch the same seat to Send; clearing or successfully submitting the draft restores Stop. While a message waits for the Host, the seat shows its progress, and a separate Stop stays beside it. The busy-Enter setting selects the Queue or Steer delivery for ordinary Sessions and continuable children, and the running Send button delivers through the same mode plain Enter resolves to; while it is enabled (no upload pending) over a plain message draft its label names that mode (Queue message or Steer message), so the setting governs Enter and the button together while Cmd/Ctrl+Enter still uses the other mode, and idle sessions, empty drafts, and `/` command lines keep the plain Send label. Their QueueDock rows share Edit, Remove, and Steer, and an empty draft shares the steer-all chord. One-shot children remain read-only. Plan mode and active goals do not change attachment intake. Continuable children keep separate Send and Stop actions and accept pasted or dropped images, but expose no File row; if their parent is offline, Send and the composer gestures lock while QueueDock controls for the live inbox remain available ([decisions](../../../.agents/notes/archived/bug-fix/2026-08-20-running-draft-primary-send.md), [inbox controls](../../subagent/subagent/README.md)). +Disabled Send and Stop buttons suppress their tooltips, including a Stop button that becomes a disabled Send button when the turn ends. While a normal composer is running, its primary pointer action remains Stop when the draft is empty or input is unavailable. Actionable text or attachments switch the same seat to Send; clearing or successfully submitting the draft restores Stop. While a message waits for the Host, the seat shows its progress, and a separate Stop stays beside it. The busy-Enter setting selects the Queue or Steer delivery for ordinary Sessions and continuable children, and the running Send button delivers through the same mode plain Enter resolves to; while it is enabled (no upload pending) over a plain message draft its label names that mode (Queue message or Steer message), so the setting governs Enter and the button together while Cmd/Ctrl+Enter still uses the other mode, and idle sessions, empty drafts, and `/` command lines keep the plain Send label. Their QueueDock rows share Edit, Remove, and Steer, and an empty draft shares the steer-all chord. One-shot children remain read-only. Plan mode and active goals do not change attachment intake. Continuable children keep separate Send and Stop actions and accept pasted or dropped images, but expose no File row; creator availability does not disable input, and Host admission failures use the ordinary prompt error feedback ([decisions](../../../.agents/notes/archived/bug-fix/2026-08-20-running-draft-primary-send.md), [inbox controls](../../subagent/subagent/README.md)). File chips and editable skill references share a whole-reference hover background and follow the composer's line height and text baseline. The first click delegates preview opening to the registered reference source immediately, including the first click of a double-click sequence. Subsequent clicks retain native text selection; an existing noncollapsed selection suppresses pointer preview activation. Previewing does not change the draft, its clipboard projection, or submission. diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index 473e15825e..75aa7b7fbe 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -124,7 +124,7 @@ Session 首次绑定或缓存的 Session 成为 current 时,shell 会在渲染 排队提交的本地回显在禁用的编辑、删除、插话按钮旁显示“发送中…”;折叠后的队列在标题栏保留发送状态。匹配的 Host 队列行替换回显后,各操作按原有的纯文本内容和运行状态要求启用。仅收到提示词确认不会启用队列操作。被拒绝或失败的提交不会公开回显,草稿留在 composer 中并显示错误提示。Agent 接纳阶段的拒绝(`session/prompt-rejected`)显示它自带的文字;若 `presented` 表明拒绝它的插件会在自己的界面里说明,composer 不再提示。 -Send 和 Stop 按钮禁用时不显示提示气泡,轮次结束后由 Stop 切换成禁用 Send 的按钮也遵循此规则。普通 composer 运行时,如果草稿为空或输入不可用,主指针操作保持为 Stop。可提交的文字或附件会把同一位置切换为 Send;清空或成功提交草稿后恢复 Stop。消息等待 Host 接收期间,该位置显示发送进度,旁边另有独立的 Stop。繁忙态 Enter 设置为普通 Session 与可继续 child 选择 Queue 或 Steer 投递,运行中的 Send 按钮按 plain Enter 解析出的同一模式投递;当它在普通消息草稿上可用(没有待上传文件)时,其标签以该模式命名(排队发送或插话发送),因此该设置同时约束 Enter 与按钮,而 Cmd/Ctrl+Enter 仍使用另一模式;空闲会话、空草稿与 `/` 命令行保留普通的 Send 标签。它们的 QueueDock 行共享 Edit、Remove 与 Steer,空草稿也共享 steer-all 组合键。One-shot child 继续只读。Plan Mode 与 active goal 不改变附件入口。可继续 child 保留独立的 Send 与 Stop 操作,允许粘贴或拖入图片,但不提供「文件」菜单项;parent 离线时,Send 与 composer 手势锁定,但在线 inbox 的 QueueDock 控制仍可使用([决策](../../../.agents/notes/archived/bug-fix/2026-08-20-running-draft-primary-send.md)、[inbox 控制](../../subagent/subagent/README.zh.md))。 +Send 和 Stop 按钮禁用时不显示提示气泡,轮次结束后由 Stop 切换成禁用 Send 的按钮也遵循此规则。普通 composer 运行时,如果草稿为空或输入不可用,主指针操作保持为 Stop。可提交的文字或附件会把同一位置切换为 Send;清空或成功提交草稿后恢复 Stop。消息等待 Host 接收期间,该位置显示发送进度,旁边另有独立的 Stop。繁忙态 Enter 设置为普通 Session 与可继续 child 选择 Queue 或 Steer 投递,运行中的 Send 按钮按 plain Enter 解析出的同一模式投递;当它在普通消息草稿上可用(没有待上传文件)时,其标签以该模式命名(排队发送或插话发送),因此该设置同时约束 Enter 与按钮,而 Cmd/Ctrl+Enter 仍使用另一模式;空闲会话、空草稿与 `/` 命令行保留普通的 Send 标签。它们的 QueueDock 行共享 Edit、Remove 与 Steer,空草稿也共享 steer-all 组合键。One-shot child 继续只读。Plan Mode 与 active goal 不改变附件入口。可继续 child 保留独立的 Send 与 Stop 操作,允许粘贴或拖入图片,但不提供「文件」菜单项;创建者的在线状态不禁用输入,Host 接收失败使用普通提示词错误反馈([决策](../../../.agents/notes/archived/bug-fix/2026-08-20-running-draft-primary-send.md)、[inbox 控制](../../subagent/subagent/README.zh.md))。 文件标签和可编辑的 skill 引用共用覆盖整个引用的悬停背景,并跟随输入框的行高与文字基线。首次点击立即由已注册的引用来源负责打开预览,包括双击序列的第一次点击。后续点击保留原生文本选择行为;已有非折叠选区时,指针点击不打开预览。预览不改变草稿、剪贴板文本或提交内容。 diff --git a/packages/client/ui-conversation/src/client/input/hub.ts b/packages/client/ui-conversation/src/client/input/hub.ts index 02897a18c6..9ed94a567b 100644 --- a/packages/client/ui-conversation/src/client/input/hub.ts +++ b/packages/client/ui-conversation/src/client/input/hub.ts @@ -130,7 +130,7 @@ export class InputHub implements SessionInputResolver { quoteInsertionAllowed: () => { const state = session.getSnapshot() return !state.removed && (state.subagent === null - || (state.subagent.address.mode === 'continuable' && state.subagent.parentAvailable === true)) + || state.subagent.address.mode === 'continuable') && this.conversation().blocks.storeFor(session.sessionId).getSnapshot() === undefined }, steerQueue: () => { void this.steerQueue(session, shell) }, diff --git a/packages/client/ui-conversation/src/client/locales.ts b/packages/client/ui-conversation/src/client/locales.ts index 2918c177a0..d89b2e91c4 100644 --- a/packages/client/ui-conversation/src/client/locales.ts +++ b/packages/client/ui-conversation/src/client/locales.ts @@ -20,7 +20,6 @@ export const zh = { 'placeholder.plan': PLAN_NEXT_ACTION_ZH, 'placeholder.default': '发消息或创建任务, / 调用指令, @ 文件或对话', 'placeholder.unavailable': '会话不可用', - 'placeholder.parentOffline': '父会话已离线,无法继续发送;仍可停止当前运行', 'placeholder.hero': '描述你想要构建的内容, / 调用指令, @ 文件或对话', 'placeholder.workspace': '选择一个工作区开始', 'placeholder.steerQueue': 'Cmd/Ctrl+Enter 插话发送全部排队消息', @@ -164,6 +163,7 @@ export const zh = { 'tool.title.traceSession': '追踪会话', 'tool.title.listModels': '查看可用模型', 'tool.title.subagent': '创建子智能体', + 'tool.title.subagentFork': '派生子智能体', 'tool.title.listAgents': '查看子智能体', 'tool.title.sendMessage': '发送消息', 'tool.title.interruptAgent': '中断智能体', @@ -185,7 +185,7 @@ export const zh = { 'tool.title.getTeamTask': '读取团队任务', 'tool.title.updateTeamTask': '更新团队任务', 'tool.title.listTeamTasks': '查看团队任务', - 'tool.title.waitAgent': '等待子智能体', + 'tool.title.waitAgent': '等待消息…', 'detail.recordedResult': '调用结果', 'detail.empty': '暂无结果', 'detail.none': '无', @@ -242,11 +242,12 @@ export const zh = { 'detail.field.agents': '启动智能体数', 'detail.field.result': '结果', 'detail.field.parent': '父级', - 'detail.field.depth': '层级', 'detail.field.exitCode': '退出码', 'detail.field.signal': '信号', 'detail.field.previousStatus': '中断前状态', 'detail.field.agent': '智能体 ID', + 'detail.field.agentName': '智能体名称', + 'detail.field.agentDescription': '智能体任务描述', 'detail.field.job': '任务 ID', 'detail.field.task': '任务内容', 'detail.field.processGroup': '进程组', @@ -270,9 +271,9 @@ export const zh = { 'detail.receipt.closed': '已关闭', 'detail.receipt.closing': '关闭中', 'detail.wait.noProgress': '没有正在运行的子智能体', - 'detail.wait.title': '子智能体状态', - 'detail.wait.timeout': '等待超时', - 'detail.wait.changed': '检测到变化', + 'detail.wait.title': '等待结果', + 'detail.wait.timeout': '等待已超时', + 'detail.wait.ended': '等待已结束', 'detail.agent.reply': '智能体回复', 'detail.models.title': '可用模型', 'detail.output.lines': '第 {begin}–{end} 行,共 {total} 行', @@ -410,7 +411,6 @@ export const en = { 'placeholder.plan': PLAN_NEXT_ACTION_EN, 'placeholder.default': 'Message or run a task, / commands, @ files or sessions', 'placeholder.unavailable': 'Session unavailable', - 'placeholder.parentOffline': 'Parent session offline; sending is unavailable but you can still stop the run', 'placeholder.hero': 'Describe what you want to build, / commands, @ files or sessions', 'placeholder.workspace': 'Choose a workspace to start', 'placeholder.steerQueue': 'Cmd/Ctrl+Enter steers all queued messages', @@ -554,6 +554,7 @@ export const en = { 'tool.title.traceSession': 'Trace session', 'tool.title.listModels': 'List models', 'tool.title.subagent': 'Create subagent', + 'tool.title.subagentFork': 'Fork subagent', 'tool.title.listAgents': 'List subagents', 'tool.title.sendMessage': 'Send message', 'tool.title.interruptAgent': 'Interrupt agent', @@ -575,7 +576,7 @@ export const en = { 'tool.title.getTeamTask': 'Read team task', 'tool.title.updateTeamTask': 'Update team task', 'tool.title.listTeamTasks': 'List team tasks', - 'tool.title.waitAgent': 'Wait for subagent', + 'tool.title.waitAgent': 'Waiting for messages…', 'detail.recordedResult': 'Recorded result', 'detail.empty': 'No results', 'detail.none': 'None', @@ -632,11 +633,12 @@ export const en = { 'detail.field.agents': 'Agents started', 'detail.field.result': 'Result', 'detail.field.parent': 'Parent', - 'detail.field.depth': 'Depth', 'detail.field.exitCode': 'Exit code', 'detail.field.signal': 'Signal', 'detail.field.previousStatus': 'Previous status', 'detail.field.agent': 'Agent ID', + 'detail.field.agentName': 'Agent name', + 'detail.field.agentDescription': 'Agent task description', 'detail.field.job': 'Job ID', 'detail.field.task': 'Task', 'detail.field.processGroup': 'Process group', @@ -660,9 +662,9 @@ export const en = { 'detail.receipt.closed': 'Closed', 'detail.receipt.closing': 'Closing', 'detail.wait.noProgress': 'No active subagents', - 'detail.wait.title': 'Subagent activity', + 'detail.wait.title': 'Wait result', 'detail.wait.timeout': 'Wait timed out', - 'detail.wait.changed': 'Change detected', + 'detail.wait.ended': 'Wait ended', 'detail.agent.reply': 'Agent response', 'detail.models.title': 'Available models', 'detail.output.lines': 'Lines {begin}–{end} of {total}', @@ -799,7 +801,6 @@ export const ja = { 'placeholder.plan': '計画を生成するにはタスクを説明してください', 'placeholder.default': 'メッセージを送信するかタスクを実行、/ コマンド、@ ファイルやセッション', 'placeholder.unavailable': 'セッションを利用できません', - 'placeholder.parentOffline': '親セッションがオフラインです。送信はできませんが、実行は停止できます', 'placeholder.hero': '作りたいものを説明してください。/ コマンド、@ ファイルやセッション', 'placeholder.workspace': '開始するワークスペースを選択', 'placeholder.steerQueue': 'Cmd/Ctrl+Enterでキュー内のすべてのメッセージを実行中の指示に追加', @@ -943,6 +944,7 @@ export const ja = { 'tool.title.traceSession': 'セッションをトレース', 'tool.title.listModels': 'モデルを一覧表示', 'tool.title.subagent': 'サブエージェントを作成', + 'tool.title.subagentFork': 'サブエージェントを作成(コンテキストを継承)', 'tool.title.listAgents': 'サブエージェントを一覧表示', 'tool.title.sendMessage': 'メッセージを送信', 'tool.title.interruptAgent': 'エージェントを中断', @@ -964,7 +966,7 @@ export const ja = { 'tool.title.getTeamTask': 'チームタスクを読み取り', 'tool.title.updateTeamTask': 'チームタスクを更新', 'tool.title.listTeamTasks': 'チームタスクを一覧表示', - 'tool.title.waitAgent': 'サブエージェントを待機', + 'tool.title.waitAgent': 'メッセージを待機中…', 'detail.recordedResult': '記録された結果', 'detail.empty': '結果なし', 'detail.none': 'なし', @@ -1021,11 +1023,12 @@ export const ja = { 'detail.field.agents': '起動したエージェント', 'detail.field.result': '結果', 'detail.field.parent': '親', - 'detail.field.depth': '深さ', 'detail.field.exitCode': '終了コード', 'detail.field.signal': 'シグナル', 'detail.field.previousStatus': '以前のステータス', 'detail.field.agent': 'エージェント ID', + 'detail.field.agentName': 'エージェント名', + 'detail.field.agentDescription': 'エージェントのタスク説明', 'detail.field.job': 'ジョブ ID', 'detail.field.task': 'タスク', 'detail.field.processGroup': 'プロセスグループ', @@ -1051,7 +1054,7 @@ export const ja = { 'detail.wait.noProgress': 'アクティブなサブエージェントなし', 'detail.wait.title': 'サブエージェントのアクティビティ', 'detail.wait.timeout': '待機がタイムアウトしました', - 'detail.wait.changed': '変更を検出', + 'detail.wait.ended': '待機が終了しました', 'detail.agent.reply': 'エージェントの応答', 'detail.models.title': '利用可能なモデル', 'detail.output.lines': '{total} 行中 {begin}–{end} 行', @@ -1186,7 +1189,6 @@ export const es = { 'placeholder.plan': PLAN_NEXT_ACTION_EN, 'placeholder.default': 'Envía mensajes o ejecuta una tarea, / comandos, @ archivos o sesiones', 'placeholder.unavailable': 'Sesión no disponible', - 'placeholder.parentOffline': 'Sesión para padres sin conexión; El envío no está disponible pero aún puedes detener la ejecución.', 'placeholder.hero': 'Describe lo que quieres construir, / comandos, @ archivos o sesiones', 'placeholder.workspace': 'Elija un espacio de trabajo para comenzar', 'placeholder.steerQueue': 'Cmd/Ctrl+Enter dirige todos los mensajes en cola', @@ -1325,6 +1327,7 @@ export const es = { 'tool.title.traceSession': 'Sesión de seguimiento', 'tool.title.listModels': 'Listar modelos', 'tool.title.subagent': 'Crear subagente', + 'tool.title.subagentFork': 'Crear subagente (heredar contexto)', 'tool.title.listAgents': 'Listar subagentes', 'tool.title.sendMessage': 'Enviar mensaje', 'tool.title.interruptAgent': 'agente de interrupción', @@ -1346,7 +1349,7 @@ export const es = { 'tool.title.getTeamTask': 'Leer tarea de equipo', 'tool.title.updateTeamTask': 'Actualizar tarea de equipo', 'tool.title.listTeamTasks': 'Listar tareas del equipo', - 'tool.title.waitAgent': 'Espere al subagente', + 'tool.title.waitAgent': 'Esperando mensajes…', 'detail.recordedResult': 'Resultado registrado', 'detail.empty': 'Sin resultados', 'detail.none': 'Ninguno', @@ -1403,11 +1406,12 @@ export const es = { 'detail.field.agents': 'Los agentes comenzaron', 'detail.field.result': 'Resultado', 'detail.field.parent': 'Padre', - 'detail.field.depth': 'Profundidad', 'detail.field.exitCode': 'código de salida', 'detail.field.signal': 'señal', 'detail.field.previousStatus': 'Estado anterior', 'detail.field.agent': 'ID del agente', + 'detail.field.agentName': 'Nombre del agente', + 'detail.field.agentDescription': 'Descripción de la tarea del agente', 'detail.field.job': 'ID de trabajo', 'detail.field.task': 'Tarea', 'detail.field.processGroup': 'grupo de procesos', @@ -1433,7 +1437,7 @@ export const es = { 'detail.wait.noProgress': 'No hay subagentes activos', 'detail.wait.title': 'Actividad del subagente', 'detail.wait.timeout': 'Se agotó el tiempo de espera', - 'detail.wait.changed': 'Cambio detectado', + 'detail.wait.ended': 'Espera finalizada', 'detail.agent.reply': 'Respuesta del agente', 'detail.models.title': 'Modelos disponibles', 'detail.output.lines': 'Líneas {begin}–{end} de {total}', @@ -1575,7 +1579,6 @@ export const fr = { 'placeholder.plan': PLAN_NEXT_ACTION_EN, 'placeholder.default': 'Envoyez un message ou exécutez une tâche, des commandes /, des fichiers @ ou des sessions', 'placeholder.unavailable': 'Séance indisponible', - 'placeholder.parentOffline': "Session parent hors ligne ; l'envoi n'est pas disponible mais vous pouvez toujours arrêter l'exécution", 'placeholder.hero': 'Décrivez ce que vous souhaitez construire, / commandes, @ fichiers ou sessions', 'placeholder.workspace': 'Choisissez un espace de travail pour commencer', 'placeholder.steerQueue': "Cmd/Ctrl+Entrée dirige tous les messages en file d'attente", @@ -1714,6 +1717,7 @@ export const fr = { 'tool.title.traceSession': 'Session de traçage', 'tool.title.listModels': 'Liste des modèles', 'tool.title.subagent': 'Créer un sous-agent', + 'tool.title.subagentFork': 'Créer un sous-agent (hériter du contexte)', 'tool.title.listAgents': 'Liste des sous-agents', 'tool.title.sendMessage': 'Envoyer le message', 'tool.title.interruptAgent': "Agent d'interruption", @@ -1735,7 +1739,7 @@ export const fr = { 'tool.title.getTeamTask': "Lire la tâche d'équipe", 'tool.title.updateTeamTask': "Mettre à jour la tâche de l'équipe", 'tool.title.listTeamTasks': "Répertorier les tâches de l'équipe", - 'tool.title.waitAgent': 'Attendez le sous-agent', + 'tool.title.waitAgent': 'En attente de messages…', 'detail.recordedResult': 'Résultat enregistré', 'detail.empty': 'Aucun résultat', 'detail.none': 'Aucun', @@ -1792,11 +1796,12 @@ export const fr = { 'detail.field.agents': 'Les agents ont commencé', 'detail.field.result': 'Résultat', 'detail.field.parent': 'Parent', - 'detail.field.depth': 'Profondeur', 'detail.field.exitCode': 'Code de sortie', 'detail.field.signal': 'Signal', 'detail.field.previousStatus': 'Statut précédent', 'detail.field.agent': "ID d'agent", + 'detail.field.agentName': 'Nom de l’agent', + 'detail.field.agentDescription': 'Description de la tâche de l’agent', 'detail.field.job': 'ID de travail', 'detail.field.task': 'Tâche', 'detail.field.processGroup': 'Groupe de processus', @@ -1822,7 +1827,7 @@ export const fr = { 'detail.wait.noProgress': 'Aucun sous-agent actif', 'detail.wait.title': 'Activité du sous-agent', 'detail.wait.timeout': "Le délai d'attente a expiré", - 'detail.wait.changed': 'Changement détecté', + 'detail.wait.ended': 'Attente terminée', 'detail.agent.reply': "Réponse de l'agent", 'detail.models.title': 'Modèles disponibles', 'detail.output.lines': 'Lignes {begin}–{end} de {total}', @@ -1964,7 +1969,6 @@ export const de = { 'placeholder.plan': PLAN_NEXT_ACTION_EN, 'placeholder.default': 'Senden Sie eine Nachricht oder führen Sie eine Aufgabe, /-Befehle, @-Dateien oder Sitzungen aus', 'placeholder.unavailable': 'Sitzung nicht verfügbar', - 'placeholder.parentOffline': 'Elternsitzung offline; Das Senden ist nicht möglich, aber Sie können den Lauf trotzdem stoppen', 'placeholder.hero': 'Beschreiben Sie, was Sie erstellen möchten: /-Befehle, @-Dateien oder Sitzungen', 'placeholder.workspace': 'Wählen Sie einen Arbeitsbereich zum Starten aus', 'placeholder.steerQueue': 'Cmd/Strg+Enter steuert alle Nachrichten in der Warteschlange', @@ -2103,6 +2107,7 @@ export const de = { 'tool.title.traceSession': 'Trace-Sitzung', 'tool.title.listModels': 'Modelle auflisten', 'tool.title.subagent': 'Subagent erstellen', + 'tool.title.subagentFork': 'Subagent erstellen (Kontext übernehmen)', 'tool.title.listAgents': 'Subagenten auflisten', 'tool.title.sendMessage': 'Nachricht senden', 'tool.title.interruptAgent': 'Interrupt-Agent', @@ -2124,7 +2129,7 @@ export const de = { 'tool.title.getTeamTask': 'Teamaufgabe lesen', 'tool.title.updateTeamTask': 'Teamaufgabe aktualisieren', 'tool.title.listTeamTasks': 'Teamaufgaben auflisten', - 'tool.title.waitAgent': 'Warten Sie auf den Subagenten', + 'tool.title.waitAgent': 'Warten auf Nachrichten…', 'detail.recordedResult': 'Aufgezeichnetes Ergebnis', 'detail.empty': 'Keine Ergebnisse', 'detail.none': 'Keine', @@ -2181,11 +2186,12 @@ export const de = { 'detail.field.agents': 'Agenten begannen', 'detail.field.result': 'Ergebnis', 'detail.field.parent': 'Eltern', - 'detail.field.depth': 'Tiefe', 'detail.field.exitCode': 'Exit-Code', 'detail.field.signal': 'Signal', 'detail.field.previousStatus': 'Vorheriger Stand', 'detail.field.agent': 'Agenten-ID', + 'detail.field.agentName': 'Agentenname', + 'detail.field.agentDescription': 'Aufgabenbeschreibung des Agenten', 'detail.field.job': 'Job-ID', 'detail.field.task': 'Aufgabe', 'detail.field.processGroup': 'Prozessgruppe', @@ -2211,7 +2217,7 @@ export const de = { 'detail.wait.noProgress': 'Keine aktiven Subagenten', 'detail.wait.title': 'Aktivität des Subagenten', 'detail.wait.timeout': 'Wartezeit abgelaufen', - 'detail.wait.changed': 'Änderung erkannt', + 'detail.wait.ended': 'Warten beendet', 'detail.agent.reply': 'Antwort des Agenten', 'detail.models.title': 'Verfügbare Modelle', 'detail.output.lines': 'Linien {begin}–{end} von {total}', @@ -2353,7 +2359,6 @@ export const ru = { 'placeholder.plan': PLAN_NEXT_ACTION_EN, 'placeholder.default': 'Отправьте сообщение или запустите задачу, /команды, @файлы или сеансы', 'placeholder.unavailable': 'Сеанс недоступен', - 'placeholder.parentOffline': 'Родительская сессия в автономном режиме; отправка недоступна, но вы все равно можете остановить запуск', 'placeholder.hero': 'Опишите, что вы хотите построить: /команды, @файлы или сеансы.', 'placeholder.workspace': 'Выберите рабочее пространство для начала', 'placeholder.steerQueue': 'Cmd/Ctrl+Enter управляет всеми сообщениями в очереди.', @@ -2492,6 +2497,7 @@ export const ru = { 'tool.title.traceSession': 'Трассировка сеанса', 'tool.title.listModels': 'Список моделей', 'tool.title.subagent': 'Создать субагент', + 'tool.title.subagentFork': 'Создать субагент (наследовать контекст)', 'tool.title.listAgents': 'Список субагентов', 'tool.title.sendMessage': 'Отправить сообщение', 'tool.title.interruptAgent': 'Агент прерываний', @@ -2513,7 +2519,7 @@ export const ru = { 'tool.title.getTeamTask': 'Прочитать командное задание', 'tool.title.updateTeamTask': 'Обновить групповую задачу', 'tool.title.listTeamTasks': 'Перечислить командные задачи', - 'tool.title.waitAgent': 'Подождите субагента', + 'tool.title.waitAgent': 'Ожидание сообщений…', 'detail.recordedResult': 'Записанный результат', 'detail.empty': 'Нет результатов', 'detail.none': 'Нет', @@ -2570,11 +2576,12 @@ export const ru = { 'detail.field.agents': 'Агенты запущены', 'detail.field.result': 'Результат', 'detail.field.parent': 'Родитель', - 'detail.field.depth': 'Глубина', 'detail.field.exitCode': 'Код выхода', 'detail.field.signal': 'Сигнал', 'detail.field.previousStatus': 'Предыдущий статус', 'detail.field.agent': 'Идентификатор агента', + 'detail.field.agentName': 'Имя агента', + 'detail.field.agentDescription': 'Описание задачи агента', 'detail.field.job': 'Идентификатор вакансии', 'detail.field.task': 'Задача', 'detail.field.processGroup': 'Группа процессов', @@ -2600,7 +2607,7 @@ export const ru = { 'detail.wait.noProgress': 'Нет активных субагентов', 'detail.wait.title': 'Субагентская деятельность', 'detail.wait.timeout': 'Время ожидания истекло', - 'detail.wait.changed': 'Обнаружено изменение', + 'detail.wait.ended': 'Ожидание завершено', 'detail.agent.reply': 'Ответ агента', 'detail.models.title': 'Доступные модели', 'detail.output.lines': 'Линии {begin} – {end} из {total}', @@ -2742,7 +2749,6 @@ export const pt = { 'placeholder.plan': PLAN_NEXT_ACTION_EN, 'placeholder.default': 'Envie mensagens ou execute uma tarefa, / comandos, @ arquivos ou sessões', 'placeholder.unavailable': 'Sessão indisponível', - 'placeholder.parentOffline': 'Sessão pai offline; o envio não está disponível, mas você ainda pode interromper a execução', 'placeholder.hero': 'Descreva o que você deseja construir, / comandos, @ arquivos ou sessões', 'placeholder.workspace': 'Escolha um espaço de trabalho para começar', 'placeholder.steerQueue': 'Cmd/Ctrl+Enter direciona todas as mensagens na fila', @@ -2881,6 +2887,7 @@ export const pt = { 'tool.title.traceSession': 'Sessão de rastreamento', 'tool.title.listModels': 'Listar modelos', 'tool.title.subagent': 'Criar subagente', + 'tool.title.subagentFork': 'Criar subagente (herdar contexto)', 'tool.title.listAgents': 'Listar subagentes', 'tool.title.sendMessage': 'Enviar mensagem', 'tool.title.interruptAgent': 'Agente de interrupção', @@ -2902,7 +2909,7 @@ export const pt = { 'tool.title.getTeamTask': 'Ler tarefa da equipe', 'tool.title.updateTeamTask': 'Atualizar tarefa da equipe', 'tool.title.listTeamTasks': 'Listar tarefas da equipe', - 'tool.title.waitAgent': 'Aguarde o subagente', + 'tool.title.waitAgent': 'Aguardando mensagens…', 'detail.recordedResult': 'Resultado registrado', 'detail.empty': 'Nenhum resultado', 'detail.none': 'Nenhum', @@ -2959,11 +2966,12 @@ export const pt = { 'detail.field.agents': 'Agentes iniciados', 'detail.field.result': 'Resultado', 'detail.field.parent': 'Pai', - 'detail.field.depth': 'Profundidade', 'detail.field.exitCode': 'Código de saída', 'detail.field.signal': 'Sinal', 'detail.field.previousStatus': 'Estado anterior', 'detail.field.agent': 'ID do agente', + 'detail.field.agentName': 'Nome do agente', + 'detail.field.agentDescription': 'Descrição da tarefa do agente', 'detail.field.job': 'ID do trabalho', 'detail.field.task': 'Tarefa', 'detail.field.processGroup': 'Grupo de processos', @@ -2989,7 +2997,7 @@ export const pt = { 'detail.wait.noProgress': 'Nenhum subagente ativo', 'detail.wait.title': 'Atividade do subagente', 'detail.wait.timeout': 'A espera expirou', - 'detail.wait.changed': 'Alteração detectada', + 'detail.wait.ended': 'Espera encerrada', 'detail.agent.reply': 'Resposta do agente', 'detail.models.title': 'Modelos disponíveis', 'detail.output.lines': 'Linhas {begin}–{end} de {total}', diff --git a/packages/client/ui-conversation/src/client/skeleton/ConversationMainPanel.tsx b/packages/client/ui-conversation/src/client/skeleton/ConversationMainPanel.tsx index 674b644a9f..dc82d99775 100644 --- a/packages/client/ui-conversation/src/client/skeleton/ConversationMainPanel.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/ConversationMainPanel.tsx @@ -29,15 +29,7 @@ export function ConversationMainPanel(props: ConversationSlotProps) { // The exemption is deliberately open-state-wide, not loading-only: a // summary-blank session is the hero before its open starts (`cold`) and // after one fails (`error`) for the same reason — there is no history. - // A restored continuable subagent waits for a Host summary to establish - // parent availability. This keeps the composer - // hidden instead of briefly rendering the parent-offline takeover. - const parentAvailabilityPending = session?.subagent?.address.mode === 'continuable' - && session.subagent.parentAvailable === undefined - const settling = sessionId !== undefined && ( - (shellPhase === 'blank' && openState === 'loading' && summaryBlank !== true) - || parentAvailabilityPending - ) + const settling = sessionId !== undefined && shellPhase === 'blank' && openState === 'loading' && summaryBlank !== true const hero = sessionId === undefined || (shellPhase === 'blank' && (openState === 'open' || summaryBlank === true)) const phase = settling ? 'settling' : hero ? 'hero' : 'active' diff --git a/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css b/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css index cfe0953095..dd60f4cff6 100644 --- a/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css +++ b/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css @@ -149,11 +149,6 @@ cursor: pointer; } -.crumbSubagent { - font-size: 12px; - line-height: 18px; -} - @media (hover: hover) { button.crumb:hover { background: var(--dsw-alias-interactive-bg-hover); @@ -170,7 +165,13 @@ display: flex; flex: none; align-items: center; - gap: 8px; + gap: 10px; +} + +@container (max-width: 460px) { + .headerActions { + gap: 8px; + } } .headerUtilities { diff --git a/packages/client/ui-conversation/src/client/skeleton/ConversationSession.tsx b/packages/client/ui-conversation/src/client/skeleton/ConversationSession.tsx index 14602cbca0..cc309bbfab 100644 --- a/packages/client/ui-conversation/src/client/skeleton/ConversationSession.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/ConversationSession.tsx @@ -157,14 +157,14 @@ export function ConversationSessionHeader({ // base.css) and leave the title inert for dragging too. const title = last ? ( - + {summary.displayTitle} ) : ( + + } + )} {browsable && 0} icon={} onSelect={() => { onDismiss(); browsePlugins() }}> {t('install')} diff --git a/packages/client/ui-entry-customization/src/client/locales.ts b/packages/client/ui-entry-customization/src/client/locales.ts index c0a0e2cccc..c9191fb438 100644 --- a/packages/client/ui-entry-customization/src/client/locales.ts +++ b/packages/client/ui-entry-customization/src/client/locales.ts @@ -1,5 +1,7 @@ /** Copy for the More menu rows: navigation actions, and Creator draft prompts per region. */ export const en = { + 'pinTooltip': 'Pin to left sidebar', + 'pin': 'Pin {name} to sidebar', install: 'Browse other plugins', create: 'Create your plugin', 'createPrompt.sidebar': 'Create a DSH plugin with a left sidebar entry.', @@ -9,6 +11,8 @@ export const en = { /** Chinese counterpart of the entry-customization dictionary. */ export const zh: Record = { + 'pinTooltip': '固定到左侧栏', + 'pin': '将{name}固定到侧边栏', install: '浏览更多插件', create: '创建你的插件', 'createPrompt.sidebar': '创建一个带左侧栏入口的 DSH 插件。', @@ -18,6 +22,8 @@ export const zh: Record = { /** Japanese copy, checked against the English keys. */ export const ja = { + 'pinTooltip': '左サイドバーに固定', + 'pin': '{name}をサイドバーに固定', 'install': '他のプラグインを探す', 'create': 'プラグインを作成', 'createPrompt.sidebar': '左サイドバーにエントリを持つ DSH プラグインを作成します。', @@ -27,6 +33,8 @@ export const ja = { /** Spanish dictionary, checked against the English keys. */ export const es = { + 'pinTooltip': 'Fijar a la barra lateral izquierda', + 'pin': 'Fijar {name} en la barra lateral', install: 'Explorar otros complementos', create: 'Crea tu complemento', 'createPrompt.sidebar': 'Cree un complemento DSH con una entrada en la barra lateral izquierda.', @@ -36,6 +44,8 @@ export const es = { /** French dictionary, checked against the English keys. */ export const fr = { + 'pinTooltip': 'Épingler sur la barre latérale gauche', + 'pin': 'Épingler {name} à la barre latérale', install: "Parcourir d'autres plugins", create: 'Créez votre plugin', 'createPrompt.sidebar': 'Créez un plugin DSH avec une entrée dans la barre latérale gauche.', @@ -45,6 +55,8 @@ export const fr = { /** German dictionary, checked against the English keys. */ export const de = { + 'pinTooltip': 'An die linke Seitenleiste anpinnen', + 'pin': '{name} an der Seitenleiste anheften', install: 'Durchsuchen Sie andere Plugins', create: 'Erstellen Sie Ihr Plugin', 'createPrompt.sidebar': 'Erstellen Sie ein DSH-Plugin mit einem Eintrag in der linken Seitenleiste.', @@ -54,6 +66,8 @@ export const de = { /** Russian dictionary, checked against the English keys. */ export const ru = { + 'pinTooltip': 'Закрепить на левой боковой панели', + 'pin': 'Закрепить {name} на боковой панели', install: 'Просмотрите другие плагины', create: 'Создайте свой плагин', 'createPrompt.sidebar': 'Создайте плагин DSH с записью на левой боковой панели.', @@ -63,6 +77,8 @@ export const ru = { /** Portuguese dictionary, checked against the English keys. */ export const pt = { + 'pinTooltip': 'Fixar na barra lateral esquerda', + 'pin': 'Fixar {name} na barra lateral', install: 'Procure outros plug-ins', create: 'Crie seu plug-in', 'createPrompt.sidebar': 'Crie um plug-in DSH com uma entrada na barra lateral esquerda.', diff --git a/packages/client/ui-entry-customization/tests/menu.client.spec.tsx b/packages/client/ui-entry-customization/tests/menu.client.spec.tsx index ff77c49ca7..334352d679 100644 --- a/packages/client/ui-entry-customization/tests/menu.client.spec.tsx +++ b/packages/client/ui-entry-customization/tests/menu.client.spec.tsx @@ -2,11 +2,12 @@ /** The plugin fills each region's More menu with the discovery and Creator actions. */ import type { Context } from '@deepseek-ai/cordis' import { afterEach, describe, expect, it, vi } from 'vitest' -import { act, cleanup, fireEvent, render, within } from '@testing-library/react' +import { act, cleanup, fireEvent, render, waitFor, within } from '@testing-library/react' import type { ComponentProps } from 'react' import { useState } from 'react' import { makeTranslate, SlotTestRuntime } from '@deepseek-ai/dsh-client-test-runtime' import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client' +import { Menu } from '@deepseek-ai/dsh-client-ui-primitives' import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots' import type { TrackProductEvent } from '@deepseek-ai/dsh-client-product-analytics/client' import { EntryMenuContent } from '../src/client/EntryMenuContent.tsx' @@ -35,16 +36,16 @@ function content(overrides: Partial = {}) { region: 'sidebar', t: makeTranslate(en), usePluginNavigation: (select: (present: boolean) => T): T => select(true), usePanelInfo: unused, useSessions: unused, useSessionStatus: unused, useSessionRetainInfo: unused, - useResource: unused, useWorkspaces: unused, + useResource: unused, useWorkspaces: unused, renderFactorySlot: () => null, ...callbacks, ...overrides, } as ContentProps const view = render(
    ) - return { view, ...callbacks } + return { view, props, ...callbacks } } describe('More menu rows', () => { - it.each(['sidebar', 'rightbar'] as const)('shows only discovery and creation in %s regardless of entries', (region) => { + it.each(['rightbar'] as const)('shows only discovery and creation in %s regardless of entries', (region) => { const b = content({ region, entries: [ { id: 'notes', label: 'Notes', pinned: true, active: true }, { id: 'tasks', label: 'Tasks', pinned: false }, @@ -61,6 +62,37 @@ describe('More menu rows', () => { expect(b.onPinnedChange).not.toHaveBeenCalled() }) + it('lists only unpinned sidebar entries and pins without navigating', () => { + const b = content() + expect(b.view.queryByRole('menuitem', { name: 'Notes' })).toBeNull() + fireEvent.click(b.view.getByRole('menuitem', { name: 'Tasks' })) + expect(b.onOpen).toHaveBeenCalledExactlyOnceWith('tasks') + expect(b.onPinnedChange).not.toHaveBeenCalled() + b.onOpen.mockClear() + fireEvent.click(b.view.getByRole('menuitem', { name: 'Pin Tasks to sidebar' })) + expect(b.onPinnedChange).toHaveBeenCalledExactlyOnceWith('tasks', true) + expect(b.onOpen).not.toHaveBeenCalled() + }) + + it('returns focus to More after keyboard pinning without opening the entry', async () => { + const b = content() + b.view.unmount() + function PinMenu() { + const [open, setOpen] = useState(true) + return { setOpen(false) }} autoFocus + anchor={}> + { setOpen(false) }} /> + + } + const view = render() + const pin = view.getByRole('menuitem', { name: 'Pin Tasks to sidebar' }) + act(() => { pin.focus() }) + fireEvent.keyDown(pin, { key: 'Tab' }) + await waitFor(() => { expect(document.activeElement).toBe(view.getByRole('button', { name: 'More' })) }) + expect(b.onPinnedChange).toHaveBeenCalledExactlyOnceWith('tasks', true) + expect(b.onOpen).not.toHaveBeenCalled() + }) + it.each([true, false])('opens folded Views and marks the active View with plugin navigation present=%s', (browsable) => { const b = content({ region: 'conversation', usePluginNavigation: select => select(browsable), @@ -90,7 +122,7 @@ describe('More menu rows', () => { }) it.each(['sidebar', 'rightbar'] as const)('keeps only creation in %s without plugin navigation', (region) => { - const b = content({ region, usePluginNavigation: select => select(false) }) + const b = content({ region, entries: [], usePluginNavigation: select => select(false) }) expect(b.view.getAllByRole('menuitem').map(row => row.textContent)).toEqual(['Create your plugin']) expect(b.view.queryByRole('separator')).toBeNull() fireEvent.click(b.view.getByRole('menuitem', { name: 'Create your plugin' })) diff --git a/packages/client/ui-jobs/README.i18n.yaml b/packages/client/ui-jobs/README.i18n.yaml index 9f8db4d72f..4d2ee72e3b 100644 --- a/packages/client/ui-jobs/README.i18n.yaml +++ b/packages/client/ui-jobs/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-client-ui-jobs/use-this-package: - en: 60f6d05d870f6518 - zh: dddf3f532d828b78 + en: 3466553808f81088 + zh: 02a806ece1c3e012 /deepseek-ai-dsh-client-ui-jobs/use-this-package/one-row-per-job: en: 64c1f55b35e73295 zh: 4d93594923cf787f diff --git a/packages/client/ui-jobs/README.md b/packages/client/ui-jobs/README.md index bd8a10e8f2..f4b2d9c091 100644 --- a/packages/client/ui-jobs/README.md +++ b/packages/client/ui-jobs/README.md @@ -27,6 +27,10 @@ English | [中文](README.zh.md) Load the plugin through the web-app manifest; it renders nothing until the session can see at least one job, so an ordinary conversation never grows a control for a capability it is not using. +The header shows the live job count when any jobs are running or stopping, otherwise the visible job count, without repeating the spinner's running state in visible text. The trigger's accessible description identifies live work in both wide and compact layouts. In wide layout, a 14px icon slot shows the spinner for live jobs or the 14px layers icon for idle jobs; the root trigger has no disclosure arrow. At container widths up to 460px, the count hides and the trigger always shows the layers icon, including while jobs are live; running indicators remain inside the menu. The trigger matches More and sidebar controls: a 28×28px icon button with no padding, a 15px glyph in a 15px slot, secondary ink, and ghost hover and active backgrounds. Header actions use an 8px gap in compact layout and 10px in wide layout. Its count tooltip appears after 500ms of hover or immediately on keyboard focus, and stays hidden while the list is open. + +Non-touch hover on the text-visible trigger opens the list after 150ms; leaving both the trigger and its portaled list closes it after 120ms. Clicking the wide trigger opens or pins the list; clicking an already pinned trigger closes it. The compact trigger toggles on click and never opens on hover, including when the count hides during a pending hover. ArrowDown opens and pins the list and focuses its first control. Pointer interaction or keyboard focus inside also pins it. Outside click or Escape dismisses it, and Tab connects the trigger to the portaled controls. Hover timers and pins reset when the session changes, no visible jobs remain, or the component unmounts. + ### One row per job The `job.list` stream `ctx.jobs` mirrors is the single roster: each `JobView` row carries lifecycle, duration, the live `progress` line or the terminal `detail`, and its retained byte count — a live job, or a settled one with retained output, is what makes a row expandable. There is no second roster to join. diff --git a/packages/client/ui-jobs/README.zh.md b/packages/client/ui-jobs/README.zh.md index 62201bccee..f1b99059af 100644 --- a/packages/client/ui-jobs/README.zh.md +++ b/packages/client/ui-jobs/README.zh.md @@ -27,6 +27,10 @@ kind: "package-reference" 通过 web-app 清单加载本插件;在会话看得到至少一个 job 之前它不渲染任何东西,因此普通对话不会为未使用的能力长出控件。 +有运行中或正在停止的任务时,头部显示这些任务的数量;否则显示可见任务总数,可见文字不重复旋转指示器已表达的运行状态。宽布局和紧凑布局中的触发器均通过无障碍描述说明是否有进行中的任务。宽布局下,14px 图标区域在有进行中任务时显示旋转指示器,否则显示 14px 图层图标;根触发器不带展开箭头。容器宽度不超过 460px 时,数量文字隐藏,触发器始终显示图层图标,包括任务运行期间;运行指示器仍在菜单内显示。触发器与更多及侧边栏控件一致:28×28px 图标按钮,无内边距,15px 图标位内显示 15px 图标,使用次级前景色以及 ghost 悬停和按下背景。页头操作在紧凑布局中间距为 8px,宽布局中为 10px。数量工具提示在悬停 500ms 后或获得键盘焦点时立即显示,列表打开期间保持隐藏。 + +非触摸指针在文字可见的触发器上悬停 150ms 后打开列表;指针离开触发器和挂载到 portal 的列表后,经过 120ms 关闭。点击宽布局触发器会打开或固定列表;再次点击已固定的触发器会关闭列表。紧凑触发器通过点击切换开关状态,不因悬停打开,包括等待悬停期间数量文字隐藏的情况。ArrowDown 会打开并固定列表,将焦点移至首个控件。在列表内进行指针交互或移入键盘焦点也会固定列表。点击外部或按 Escape 可关闭列表,Tab 将触发器与 portal 内的控件连接起来。切换会话、没有可见任务或组件卸载时,悬停计时器与固定状态都会重置。 + ### 一 job 一行 `ctx.jobs` 镜像的 `job.list` 流是唯一名册:每个 `JobView` 行携带生命周期、时长、实时 `progress` 行或终态 `detail`,以及其保留字节数——进行中的 job,或留有保留输出的已结束 job,就是行可展开的依据。不存在需要 join 的第二份名册。 diff --git a/packages/client/ui-jobs/src/client/JobListAction.module.css b/packages/client/ui-jobs/src/client/JobListAction.module.css index a9eedb8079..4283427e63 100644 --- a/packages/client/ui-jobs/src/client/JobListAction.module.css +++ b/packages/client/ui-jobs/src/client/JobListAction.module.css @@ -1,7 +1,7 @@ .trigger { display: inline-flex; align-items: center; - gap: 3px; + gap: 4px; min-height: 28px; padding: 3px 2px; border: 0; @@ -23,20 +23,61 @@ } } -.trigger svg { - transition: transform 120ms ease; -} - -.triggerOpen { - transform: rotate(180deg); -} - -.triggerDot { +.activitySlot { + display: inline-flex; flex: none; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; } -.count { - margin: 0 5px; +.trigger .compactIcon { + display: none; +} + +@container (max-width: 460px) { + .trigger .compactIcon { + display: block; + } + + .trigger .textModeActivity { + display: none; + } + + .trigger .count { + display: none; + } + + .trigger { + box-sizing: border-box; + width: 28px; + height: 28px; + padding: 0; + justify-content: center; + color: var(--dsw-alias-label-secondary); + } + + .trigger .activitySlot, + .trigger .activitySlot > svg { + width: 15px; + height: 15px; + } + + .trigger:focus-visible { + color: var(--dsw-alias-label-secondary); + } + + .trigger:active { + background: var(--dsw-alias-interactive-bg-active); + } + + @media (hover: hover) { + .trigger:hover { + color: var(--dsw-alias-label-secondary); + background: var(--dsw-alias-interactive-bg-hover); + } + } } /* Portaled and fixed in the viewport with coordinates supplied inline from the diff --git a/packages/client/ui-jobs/src/client/JobListAction.tsx b/packages/client/ui-jobs/src/client/JobListAction.tsx index 7027483b96..c985af6fef 100644 --- a/packages/client/ui-jobs/src/client/JobListAction.tsx +++ b/packages/client/ui-jobs/src/client/JobListAction.tsx @@ -1,9 +1,9 @@ -import { useEffect, useMemo, useRef, useState, type CSSProperties, type KeyboardEvent } from 'react' +import { useCallback, useEffect, useId, useMemo, useRef, useState, type CSSProperties, type KeyboardEvent, type PointerEvent } from 'react' import { createPortal } from 'react-dom' import type { JobsSnapshot, JobView, ObservedJob } from '@deepseek-ai/dsh-api-job-controller/client' import type { SessionId } from '@deepseek-ai/dsh-session/types' import { - IconChevronDownOutlineRegular, IconStopFillRegular, MenuSurface, StateDot, TerminalBlock, + IconChevronDownOutlineRegular, IconLayersOutlineRegular, IconStopFillRegular, MenuSurface, StateDot, TerminalBlock, Tooltip, useAnchoredPosition, useDismissOnOutsidePointer, type StateDotState, type TerminalBlockLabels, } from '@deepseek-ai/dsh-client-ui-primitives' @@ -329,15 +329,25 @@ function JobItem({ job, view, expanded, now, onToggle, kill, t }: { * or a settled one with retained output) starts its observation stream, and * collapsing (or closing the popover) stops it — output only flows while * someone is watching. A running row carries a two-press stop button that - * requests a human kill through the job controller. + * requests a human kill through the job controller. Non-touch hover on a + * text-visible trigger opens after 150ms and closes after 120ms outside both + * trigger and portal. Compact triggers toggle on click; ArrowDown opens and + * pins either layout. Interacting inside also pins the menu until explicit + * dismissal; timers and the pin are local to the mounted session. * @param props - runtime slot currency, the jobs snapshot hook, the roster, * observation, and kill controls, and the namespace translator. * @returns the trigger and its popover list, or null when there is nothing to show. */ export function JobListAction({ sessionId, useJobs, watchRows, observe, killJob, t }: JobListActionProps) { + const activityId = useId() const jobs = useJobs(state => state.rows[sessionId]) ?? NO_JOBS const observedViews = useJobs(state => state.observed) - const [open, setOpen] = useState(false) + const [menuState, setMenuState] = useState<{ sessionId: SessionId; mode: 'closed' | 'hover' | 'pinned' }>( + { sessionId, mode: 'closed' }, + ) + const hoverOpenTimer = useRef | undefined>(undefined) + const hoverCloseTimer = useRef | undefined>(undefined) + if (menuState.sessionId !== sessionId) setMenuState({ sessionId, mode: 'closed' }) const [expandedKey, setExpandedKey] = useState(undefined) const [now, setNow] = useState(() => Date.now()) // Settled-section fold: an explicit user toggle wins; before one, the tail @@ -350,6 +360,7 @@ export function JobListAction({ sessionId, useJobs, watchRows, observe, killJob, const [killPhase, setKillPhase] = useState<{ key: string; state: Exclude } | undefined>(undefined) const rootRef = useRef(null) const triggerRef = useRef(null) + const countRef = useRef(null) const menuRef = useRef(null) const rows = useMemo(() => ordered(jobs), [jobs]) @@ -360,6 +371,50 @@ export function JobListAction({ sessionId, useJobs, watchRows, observe, killJob, ) const settledExpanded = settledOpen ?? liveRows.length === 0 const visibleCount = liveRows.length + settledRows.length + const hasJobs = visibleCount > 0 + const open = hasJobs && menuState.sessionId === sessionId && menuState.mode !== 'closed' + + const cancelHover = useCallback((): void => { + clearTimeout(hoverOpenTimer.current) + clearTimeout(hoverCloseTimer.current) + hoverOpenTimer.current = undefined + hoverCloseTimer.current = undefined + }, []) + + const changeOpen = useCallback((next: boolean): void => { + cancelHover() + if (next) setNow(Date.now()) + setMenuState({ sessionId, mode: next ? 'pinned' : 'closed' }) + }, [cancelHover, sessionId]) + + // CSS owns the compact breakpoint; read its result only for pointer gestures. + const allowsHoverOpen = (): boolean => countRef.current !== null + && getComputedStyle(countRef.current).display !== 'none' + + const hoverEnter = (event: PointerEvent): void => { + if (event.pointerType === 'touch') return + cancelHover() + if (open || !allowsHoverOpen()) return + hoverOpenTimer.current = setTimeout(() => { + hoverOpenTimer.current = undefined + if (!allowsHoverOpen()) return + setNow(Date.now()) + setMenuState({ sessionId, mode: 'hover' }) + }, 150) + } + + const hoverLeave = (event: PointerEvent): void => { + if (event.pointerType === 'touch') return + cancelHover() + if (!open || menuState.mode === 'pinned') return + hoverCloseTimer.current = setTimeout(() => { + hoverCloseTimer.current = undefined + setMenuState({ sessionId, mode: 'closed' }) + }, 120) + } + + // Timers belong to this mounted session and cannot survive a disappearing trigger. + useEffect(() => cancelHover, [cancelHover, sessionId, hasJobs]) // The popover portals to the document body: the session header sits in a // clipping column that the right sidebar borders or overlaps, so an in-place @@ -367,7 +422,18 @@ export function JobListAction({ sessionId, useJobs, watchRows, observe, killJob, const menuPosition = useAnchoredPosition({ open, anchorRef: rootRef, panelRef: menuRef, gap: MENU_GAP, margin: VIEWPORT_MARGIN, }) - useDismissOnOutsidePointer(rootRef, open, setOpen, menuRef) + useDismissOnOutsidePointer(rootRef, open, changeOpen, menuRef) + // Hover does not move focus; Escape must also work from the rest of the page. + useEffect(() => { + if (!open) return + const dismiss = (event: globalThis.KeyboardEvent): void => { + if (event.key !== 'Escape' || event.defaultPrevented) return + event.preventDefault() + changeOpen(false) + } + document.addEventListener('keydown', dismiss) + return () => { document.removeEventListener('keydown', dismiss) } + }, [open, changeOpen]) // The roster follows the mounted session: one stream while this control // lives, released with it. @@ -392,11 +458,10 @@ export function JobListAction({ sessionId, useJobs, watchRows, observe, killJob, return observe(sessionId, activeJob) }, [sessionId, activeJob, observe]) - // The last visible job disappearing removes this control; close first so - // focus does not vanish from an unmounting node. + // A returning roster starts closed rather than restoring an obsolete pin. useEffect(() => { - if (visibleCount === 0 && open) setOpen(false) - }, [visibleCount, open]) + if (!hasJobs) changeOpen(false) + }, [hasJobs, changeOpen]) // An expanded row that left the list (owner disposal) folds its panel. useEffect(() => { @@ -448,10 +513,8 @@ export function JobListAction({ sessionId, useJobs, watchRows, observe, killJob, if (visibleCount === 0) return null - const countKey = liveRows.length > 0 - ? (liveRows.length === 1 ? 'count.live.one' : 'count.live.other') - : (visibleCount === 1 ? 'count.idle.one' : 'count.idle.other') - const countLabel = t(countKey, { count: liveRows.length > 0 ? liveRows.length : visibleCount }) + const count = liveRows.length > 0 ? liveRows.length : visibleCount + const countLabel = t(count === 1 ? 'count.idle.one' : 'count.idle.other', { count }) const clearSettled = (): void => { setClearedKeys((current) => { @@ -468,14 +531,14 @@ export function JobListAction({ sessionId, useJobs, watchRows, observe, killJob, // keep the in-place order: the trigger leads into the list, and leaving // either end of the list returns to the trigger's position in the page. const onKeyDown = (event: KeyboardEvent): void => { - if (!open) return const trigger = triggerRef.current if (event.key === 'Escape') { event.preventDefault() - setOpen(false) + changeOpen(false) trigger?.focus() return } + if (!open) return const menu = menuRef.current /* v8 ignore next -- both refs are attached while the open list renders. */ if (event.key !== 'Tab' || trigger === null || menu === null) return @@ -517,28 +580,43 @@ export function JobListAction({ sessionId, useJobs, watchRows, observe, killJob, return (
    - + + + + {liveRows.length > 0 && } {open ? createPortal( - + { changeOpen(true) }} + onFocusCapture={() => { changeOpen(true) }} + >
      {liveRows.length > 0 ? diff --git a/packages/client/ui-jobs/src/client/locales.ts b/packages/client/ui-jobs/src/client/locales.ts index 700009d9a5..c968a08b5d 100644 --- a/packages/client/ui-jobs/src/client/locales.ts +++ b/packages/client/ui-jobs/src/client/locales.ts @@ -5,8 +5,6 @@ export const NS = 'job' /** Simplified Chinese dictionary (the key-set source of truth). */ export const zh = { - 'count.live.one': '{count} 个后台任务运行中', - 'count.live.other': '{count} 个后台任务运行中', 'count.idle.one': '{count} 个后台任务', 'count.idle.other': '{count} 个后台任务', 'list.aria': '后台任务', @@ -49,8 +47,6 @@ export const zh = { /** English dictionary, key-identical to the Chinese source of truth. */ export const en: Record = { - 'count.live.one': '{count} background job running', - 'count.live.other': '{count} background jobs running', 'count.idle.one': '{count} background job', 'count.idle.other': '{count} background jobs', 'list.aria': 'Background jobs', @@ -96,8 +92,6 @@ export type JobKey = keyof typeof zh /** Japanese copy, checked against the English keys. */ export const ja = { - 'count.live.one': 'バックグラウンドジョブ {count} 件が実行中', - 'count.live.other': 'バックグラウンドジョブ {count} 件が実行中', 'count.idle.one': 'バックグラウンドジョブ {count} 件', 'count.idle.other': 'バックグラウンドジョブ {count} 件', 'list.aria': 'バックグラウンドジョブ', @@ -140,8 +134,6 @@ export const ja = { /** Spanish dictionary, checked against the English keys. */ export const es = { - 'count.live.one': 'Tareas en segundo plano en ejecución: {count}', - 'count.live.other': 'Tareas en segundo plano en ejecución: {count}', 'count.idle.one': 'Tareas en segundo plano: {count}', 'count.idle.other': 'Tareas en segundo plano: {count}', 'list.aria': 'Trabajos en segundo plano', @@ -184,8 +176,6 @@ export const es = { /** French dictionary, checked against the English keys. */ export const fr = { - 'count.live.one': 'Tâches en arrière-plan en cours : {count}', - 'count.live.other': 'Tâches en arrière-plan en cours : {count}', 'count.idle.one': 'Tâches en arrière-plan : {count}', 'count.idle.other': 'Tâches en arrière-plan : {count}', 'list.aria': 'Travaux en arrière-plan', @@ -228,8 +218,6 @@ export const fr = { /** German dictionary, checked against the English keys. */ export const de = { - 'count.live.one': 'Laufende Hintergrundaufgaben: {count}', - 'count.live.other': 'Laufende Hintergrundaufgaben: {count}', 'count.idle.one': 'Hintergrundaufgaben: {count}', 'count.idle.other': 'Hintergrundaufgaben: {count}', 'list.aria': 'Hintergrundjobs', @@ -272,8 +260,6 @@ export const de = { /** Russian dictionary, checked against the English keys. */ export const ru = { - 'count.live.one': 'Фоновые задачи выполняются: {count}', - 'count.live.other': 'Фоновые задачи выполняются: {count}', 'count.idle.one': 'Фоновые задачи: {count}', 'count.idle.other': 'Фоновые задачи: {count}', 'list.aria': 'Фоновые задания', @@ -316,8 +302,6 @@ export const ru = { /** Portuguese dictionary, checked against the English keys. */ export const pt = { - 'count.live.one': 'Tarefas em segundo plano em execução: {count}', - 'count.live.other': 'Tarefas em segundo plano em execução: {count}', 'count.idle.one': 'Tarefas em segundo plano: {count}', 'count.idle.other': 'Tarefas em segundo plano: {count}', 'list.aria': 'Trabalhos em segundo plano', diff --git a/packages/client/ui-jobs/tests/job-list-action.client.spec.tsx b/packages/client/ui-jobs/tests/job-list-action.client.spec.tsx index d3a4c72053..c8ee1a0e6a 100644 --- a/packages/client/ui-jobs/tests/job-list-action.client.spec.tsx +++ b/packages/client/ui-jobs/tests/job-list-action.client.spec.tsx @@ -5,7 +5,23 @@ import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' import type { JobsSnapshot, JobView, ObservedJob } from '@deepseek-ai/dsh-api-job-controller/client' import type { SessionId } from '@deepseek-ai/dsh-session/types' import { JobListAction, type JobListActionProps } from '../src/client/JobListAction.tsx' -import { zh, ru } from '../src/client/locales.ts' +import { zh, en, ja, es, fr, de, ru, pt } from '../src/client/locales.ts' + +beforeEach(() => { + class TooltipObserver implements ResizeObserver { + constructor(private readonly callback: ResizeObserverCallback) {} + observe(target: Element): void { + const size = { inlineSize: 160, blockSize: 30 } + this.callback([{ + target, borderBoxSize: [size], contentBoxSize: [size], devicePixelContentBoxSize: [size], contentRect: new DOMRect(), + }], this) + } + unobserve(): void {} + disconnect(): void {} + } + vi.stubGlobal('ResizeObserver', TooltipObserver) + vi.stubGlobal('matchMedia', vi.fn(() => ({ matches: true }))) +}) beforeEach(() => { vi.stubGlobal('CSS', { supports: (property: string) => property === '-webkit-backdrop-filter' }) @@ -57,6 +73,28 @@ function openList(): void { fireEvent.click(screen.getAllByRole('button')[0]!) } +describe('JobListAction count copy', () => { + it.each([ + [zh, '1 个后台任务', '2 个后台任务'], + [en, '1 background job', '2 background jobs'], + [ja, 'バックグラウンドジョブ 1 件', 'バックグラウンドジョブ 2 件'], + [es, 'Tareas en segundo plano: 1', 'Tareas en segundo plano: 2'], + [fr, 'Tâches en arrière-plan : 1', 'Tâches en arrière-plan : 2'], + [de, 'Hintergrundaufgaben: 1', 'Hintergrundaufgaben: 2'], + [ru, 'Фоновые задачи: 1', 'Фоновые задачи: 2'], + [pt, 'Tarefas em segundo plano: 1', 'Tarefas em segundo plano: 2'], + ] as const)('uses neutral count copy for live and settled jobs in locale %#', (dictionary, one, two) => { + const translate: JobListActionProps['t'] = makeTranslate(dictionary) + const settled = job({ id: 'settled' as JobView['id'], status: 'completed', finishedAt: 1_700_000_003_000 }) + const { rerender } = render() + expect(screen.getByRole('button', { name: one, description: dictionary['section.live'] })).toBeDefined() + rerender() + expect(screen.getByRole('button', { name: one, description: dictionary['section.live'] })).toBeDefined() + rerender() + expect(screen.getByRole('button', { name: two, description: '' })).toBeDefined() + }) +}) + describe('JobListAction visibility', () => { it('renders nothing while the session sees no jobs', () => { const { container } = render() @@ -78,7 +116,24 @@ describe('JobListAction visibility', () => { outputJob(), job({ id: 'subagent-1' as JobView['id'], kind: 'subagent', label: 'explore' }), ])} />) - expect(screen.getByRole('button', { name: '2 个后台任务运行中' })).toBeDefined() + expect(screen.getByRole('button', { name: '2 个后台任务' })).toBeDefined() + }) + + it('retains the compact Layers icon through running, stopping, and idle states without a root chevron', () => { + const { rerender } = render() + const trigger = screen.getByRole('button') + expect(trigger.querySelector('[data-icon="dsw-icon-layers-outline-regular"]')).not.toBeNull() + expect(trigger.querySelector('[data-state="ongoing"]')).not.toBeNull() + expect(trigger.querySelector('[data-icon="dsw-icon-chevron-down-outline-regular"]')).toBeNull() + rerender() + expect(screen.getByRole('button')).toBe(trigger) + expect(trigger.querySelector('[data-state="ongoing"]')).not.toBeNull() + rerender() + expect(screen.getByRole('button')).toBe(trigger) + expect(trigger.querySelector('[data-state="ongoing"]')).toBeNull() + const icon = trigger.querySelector('[data-icon="dsw-icon-layers-outline-regular"]') + expect(icon?.getAttribute('width')).toBe('14') + expect(trigger.querySelectorAll('svg')).toHaveLength(1) }) it('falls back to the total when nothing is live', () => { @@ -89,6 +144,281 @@ describe('JobListAction visibility', () => { }) }) +describe('JobListAction hover', () => { + function pointer(target: Element, type: 'pointerover' | 'pointerout', pointerType = 'mouse'): void { + // jsdom lacks PointerEvent; preserve its pointer type on the native event. + const event = new MouseEvent(type, { bubbles: true }) + Object.defineProperty(event, 'pointerType', { value: pointerType }) + fireEvent(target, event) + } + + function advance(ms: number): void { + act(() => { vi.advanceTimersByTime(ms) }) + } + + function hoverOpen(): HTMLElement { + const trigger = screen.getByRole('button', { name: '1 个后台任务' }) + pointer(trigger, 'pointerover') + advance(149) + expect(screen.queryByRole('list')).toBeNull() + advance(1) + expect(trigger.getAttribute('aria-expanded')).toBe('true') + return trigger + } + + it('keeps the compact trigger click-only and shows its count tooltip after 500ms', () => { + vi.useFakeTimers() + render() + const trigger = screen.getByRole('button') + const count = trigger.querySelector('[data-job-count]')! + count.style.display = 'none' + pointer(trigger, 'pointerover') + advance(499) + expect(screen.queryByRole('list')).toBeNull() + expect(screen.queryByRole('tooltip')).toBeNull() + advance(1) + expect(screen.getByRole('tooltip').textContent).toBe('1 个后台任务') + fireEvent.click(trigger) + expect(screen.queryByRole('tooltip')).toBeNull() + pointer(trigger, 'pointerout') + advance(500) + expect(screen.getByRole('list')).toBeDefined() + fireEvent.click(trigger) + expect(screen.queryByRole('list')).toBeNull() + }) + + it('cancels pending wide hover when CSS hides the count and compact clicks close hover-opened lists', () => { + vi.useFakeTimers() + render() + const trigger = screen.getByRole('button') + const count = trigger.querySelector('[data-job-count]')! + pointer(trigger, 'pointerover') + advance(100) + count.style.display = 'none' + advance(50) + expect(screen.queryByRole('list')).toBeNull() + pointer(trigger, 'pointerout') + count.style.display = '' + hoverOpen() + count.style.display = 'none' + fireEvent.click(trigger) + expect(screen.queryByRole('list')).toBeNull() + }) + + it('shows count on keyboard focus immediately and opens and pins with ArrowDown', async () => { + vi.useFakeTimers() + render() + const trigger = screen.getByRole('button') + trigger.querySelector('[data-job-count]')!.style.display = 'none' + fireEvent.keyDown(document.body, { key: 'Tab' }) + act(() => { trigger.focus() }) + expect(screen.getByRole('tooltip').textContent).toBe('1 个后台任务') + fireEvent.keyDown(trigger, { key: 'ArrowDown' }) + await act(async () => { vi.runAllTicks() }) + expect(screen.queryByRole('tooltip')).toBeNull() + const row = screen.getByRole('button', { name: zh['row.expandAria'].replace('{label}', 'pnpm run build') }) + expect(document.activeElement).toBe(row) + pointer(trigger, 'pointerout') + advance(120) + expect(screen.getByRole('list')).toBeDefined() + fireEvent.keyDown(row, { key: 'Escape' }) + expect(screen.queryByRole('list')).toBeNull() + expect(document.activeElement).toBe(trigger) + }) + + it('waits for hover, bridges the portal gap, and closes after leaving both surfaces', () => { + vi.useFakeTimers() + const observe = vi.fn(() => () => {}) + const { container } = render() + const trigger = hoverOpen() + const menu = screen.getByRole('list').parentElement! + expect(container.contains(menu)).toBe(false) + expect(observe).not.toHaveBeenCalled() + pointer(trigger, 'pointerout') + advance(119) + pointer(menu, 'pointerover') + advance(120) + expect(screen.getByRole('list')).toBeDefined() + pointer(menu, 'pointerout') + advance(119) + expect(screen.getByRole('list')).toBeDefined() + advance(1) + expect(screen.queryByRole('list')).toBeNull() + }) + + it('cancels short hover and preserves a returning pointer during leave grace', () => { + vi.useFakeTimers() + render() + const trigger = screen.getByRole('button') + pointer(trigger, 'pointerover') + advance(149) + pointer(trigger, 'pointerout') + advance(150) + expect(screen.queryByRole('list')).toBeNull() + hoverOpen() + pointer(trigger, 'pointerout') + advance(100) + pointer(trigger, 'pointerover') + advance(120) + expect(screen.getByRole('list')).toBeDefined() + }) + + it('pins a hover-opened list on click until explicit dismissal', () => { + vi.useFakeTimers() + render() + const trigger = hoverOpen() + fireEvent.click(trigger) + pointer(trigger, 'pointerout') + advance(120) + expect(screen.getByRole('list')).toBeDefined() + fireEvent.click(trigger) + expect(screen.queryByRole('list')).toBeNull() + hoverOpen() + fireEvent.click(trigger) + fireEvent.pointerDown(document.body) + expect(screen.queryByRole('list')).toBeNull() + hoverOpen() + pointer(trigger, 'pointerout') + advance(120) + expect(screen.queryByRole('list')).toBeNull() + }) + + it('ignores touch hover while keeping tap usable', () => { + vi.useFakeTimers() + render() + const trigger = screen.getByRole('button') + pointer(trigger, 'pointerover', 'touch') + advance(150) + expect(screen.queryByRole('list')).toBeNull() + fireEvent.click(trigger) + pointer(trigger, 'pointerout', 'touch') + advance(120) + expect(screen.getByRole('list')).toBeDefined() + fireEvent.click(trigger) + expect(screen.queryByRole('list')).toBeNull() + }) + + it('pins keyboard focus and releases expanded output on Escape or hover close', () => { + vi.useFakeTimers() + const stop = vi.fn() + const observe = vi.fn(() => stop) + render() + const trigger = hoverOpen() + pointer(trigger, 'pointerout') + act(() => { trigger.focus() }) + fireEvent.keyDown(trigger, { key: 'Tab' }) + const row = screen.getByRole('button', { name: zh['row.expandAria'].replace('{label}', 'pnpm run build') }) + expect(document.activeElement).toBe(row) + advance(120) + fireEvent.click(row) + expect(observe).toHaveBeenCalledTimes(1) + fireEvent.keyDown(row, { key: 'Escape' }) + expect(document.activeElement).toBe(trigger) + expect(stop).toHaveBeenCalledTimes(1) + hoverOpen() + expect(observe).toHaveBeenCalledTimes(2) + pointer(trigger, 'pointerout') + advance(120) + expect(screen.queryByRole('list')).toBeNull() + expect(stop).toHaveBeenCalledTimes(2) + }) + + it('pins pointer interaction so leaving cannot interrupt stop confirmation', () => { + vi.useFakeTimers() + render() + const trigger = hoverOpen() + pointer(trigger, 'pointerout') + const stop = screen.getByTitle(zh['kill.stop'].replace('{label}', 'pnpm run build')) + fireEvent.pointerDown(stop) + fireEvent.click(stop) + pointer(screen.getByRole('list').parentElement!, 'pointerout') + advance(120) + expect(stop.getAttribute('data-kill-state')).toBe('armed') + expect(screen.getByRole('list')).toBeDefined() + }) + + it('dismisses a hover-opened list with Escape while focus is elsewhere', () => { + vi.useFakeTimers() + render() + hoverOpen() + fireEvent.keyDown(document.body, { key: 'a' }) + expect(screen.getByRole('list')).toBeDefined() + fireEvent.keyDown(document.body, { key: 'Escape' }) + expect(screen.queryByRole('list')).toBeNull() + }) + + it.each(['opening', 'closing'] as const)('cleans up a pending %s timer on unmount', (phase) => { + vi.useFakeTimers() + const { unmount } = render() + const trigger = screen.getByRole('button') + pointer(trigger, 'pointerover') + if (phase === 'closing') { + advance(150) + pointer(trigger, 'pointerout') + } + unmount() + expect(vi.getTimerCount()).toBe(0) + advance(150) + expect(screen.queryByRole('list')).toBeNull() + }) + + it('releases expanded output and pending hover close before showing another session', () => { + vi.useFakeTimers() + const stop = vi.fn() + const observe = vi.fn(() => stop) + const first = props([outputJob()], observe) + const { rerender } = render() + const trigger = hoverOpen() + const row = screen.getByRole('button', { name: zh['row.expandAria'].replace('{label}', 'pnpm run build') }) + fireEvent.click(row) + expect(observe).toHaveBeenCalledTimes(1) + pointer(trigger, 'pointerout') + const nextId = 'next-session' as SessionId + rerender( select({ rows: { [nextId]: [outputJob()] }, observed: {} })} + />) + expect(stop).toHaveBeenCalledTimes(1) + expect(observe).toHaveBeenCalledTimes(1) + expect(vi.getTimerCount()).toBe(0) + hoverOpen() + advance(120) + expect(screen.getByRole('list')).toBeDefined() + expect(observe).toHaveBeenLastCalledWith(nextId, 'bash-1') + }) + + it('cancels pending hover and clears pins on session change or an empty roster', () => { + vi.useFakeTimers() + const first = props([job()]) + const nextId = 'next-session' as SessionId + const next: JobListActionProps = { + ...first, + sessionId: nextId, + useJobs: select => select({ rows: { [nextId]: [job()] }, observed: {} }), + } + const { rerender } = render() + pointer(screen.getByRole('button'), 'pointerover') + rerender() + advance(150) + expect(screen.queryByRole('list')).toBeNull() + const trigger = hoverOpen() + fireEvent.click(trigger) + rerender() + expect(screen.queryByRole('list')).toBeNull() + pointer(screen.getByRole('button'), 'pointerover') + rerender() + advance(150) + expect(vi.getTimerCount()).toBe(0) + rerender() + hoverOpen() + fireEvent.click(screen.getByRole('button', { name: '1 个后台任务' })) + rerender() + rerender() + expect(screen.queryByRole('list')).toBeNull() + }) +}) + describe('JobListAction rows', () => { it('renders a settled job without retained output as a static row', () => { render( { const settled = [job({ id: 'bash-2' as JobView['id'], label: 'settled work', status: 'completed', finishedAt: 1_700_000_012_000 })] render() openList() - expect(screen.getByText(zh['section.live'])).toBeDefined() + expect(screen.getByText(zh['section.live'], { selector: 'li' })).toBeDefined() const toggle = screen.getByRole('button', { name: zh['section.settledCount'].replace('{count}', '1') }) // Folded by default while something runs; the toggle expands it. expect(toggle.getAttribute('aria-expanded')).toBe('false') @@ -293,7 +623,7 @@ describe('JobListAction observation', () => { vi.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockReturnValue(440) vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(120) const { container } = render() - const trigger = screen.getByRole('button', { name: '1 个后台任务运行中' }) + const trigger = screen.getByRole('button', { name: '1 个后台任务' }) const anchor = vi.spyOn(trigger.parentElement!, 'getBoundingClientRect') .mockReturnValue(new DOMRect(344, 40, 180, 28)) fireEvent.click(trigger) @@ -319,7 +649,7 @@ describe('JobListAction observation', () => { vi.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockReturnValue(600) vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(120) render() - const trigger = screen.getByRole('button', { name: '1 个后台任务运行中' }) + const trigger = screen.getByRole('button', { name: '1 个后台任务' }) vi.spyOn(trigger.parentElement!, 'getBoundingClientRect').mockReturnValue(new DOMRect(4, 540, 180, 28)) fireEvent.click(trigger) const menu = screen.getByRole('list', { name: zh['list.aria'] }) @@ -335,7 +665,7 @@ describe('JobListAction observation', () => { vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(80) const remove = vi.spyOn(viewport, 'removeEventListener') render() - const trigger = screen.getByRole('button', { name: '1 个后台任务运行中' }) + const trigger = screen.getByRole('button', { name: '1 个后台任务' }) vi.spyOn(trigger.parentElement!, 'getBoundingClientRect').mockReturnValue(new DOMRect(100, 300, 200, 20)) fireEvent.click(trigger) const menu = screen.getByRole('list', { name: zh['list.aria'] }).closest('[data-menu-material]')! @@ -362,7 +692,7 @@ describe('JobListAction observation', () => { const stop = vi.fn() const observe = vi.fn(() => stop) const { container, unmount } = render() - const trigger = screen.getByRole('button', { name: '1 个后台任务运行中' }) + const trigger = screen.getByRole('button', { name: '1 个后台任务' }) trigger.focus() fireEvent.click(trigger) const list = screen.getByRole('list', { name: zh['list.aria'] }) @@ -629,7 +959,7 @@ it.each([1, 2, 5, 21])('renders count-independent Russian labels for %i jobs', ( const jobs = Array.from({ length: count }, (_, i) => job({ id: `job-${i}` as JobView['id'] })) const translate: JobListActionProps['t'] = makeTranslate(ru) const view = render() - expect(screen.getByRole('button', { name: `Фоновые задачи выполняются: ${count}` })).toBeDefined() + expect(screen.getByRole('button', { name: `Фоновые задачи: ${count}` })).toBeDefined() view.rerender( ({ ...item, status: 'completed', finishedAt: 1_700_000_003_000 })))} t={translate} />) expect(screen.getByRole('button', { name: `Фоновые задачи: ${count}` })).toBeDefined() }) diff --git a/packages/client/ui-model-selection/README.i18n.yaml b/packages/client/ui-model-selection/README.i18n.yaml index 3b307be019..b4bb58e659 100644 --- a/packages/client/ui-model-selection/README.i18n.yaml +++ b/packages/client/ui-model-selection/README.i18n.yaml @@ -27,8 +27,8 @@ en: 7f20428c747ff052 zh: 975e6de728e02fa0 /deepseek-ai-dsh-client-ui-model-selection/use-this-package/selection-failures: - en: 261bbf42296243af - zh: 72779fb7b8fbc473 + en: 80fc237fa5c829f7 + zh: ecff5c8526f45996 /deepseek-ai-dsh-client-ui-model-selection/understand-the-implementation: en: 14870a55b8f4d4b7 zh: f498b469a785271e diff --git a/packages/client/ui-model-selection/README.md b/packages/client/ui-model-selection/README.md index dbffa12fef..1a53aa7c7b 100644 --- a/packages/client/ui-model-selection/README.md +++ b/packages/client/ui-model-selection/README.md @@ -61,6 +61,8 @@ Catalog availability does not block sending with a saved selection; request exec When another writer owns the Session, model-selection failures tell the user to quit other running DSH instances and retry. +The session-scoped `conversation.input.model.options` list slot appends controls below model and reasoning selection. Owner props expose `locked`, `reportFailure(message)`, and `dismiss()`, which closes the menu and synchronously focuses its trigger before another overlay opens; contributions read their own projections and supply narrow commands. Enabled switch buttons participate in the menu’s keyboard navigation. + ----- diff --git a/packages/client/ui-model-selection/README.zh.md b/packages/client/ui-model-selection/README.zh.md index 49c1d7ea7c..253370b97f 100644 --- a/packages/client/ui-model-selection/README.zh.md +++ b/packages/client/ui-model-selection/README.zh.md @@ -61,6 +61,8 @@ composer 菜单中的模型名称和推理等级均使用 400(regular)字重 当会话被其他写句柄占用时,模型选择失败提示用户退出其他正在运行的 DSH 后重试。 +会话级列表 slot `conversation.input.model.options` 在模型与推理等级选项下方追加控件。Owner props 提供 `locked`、`reportFailure(message)` 和 `dismiss()`,后者关闭菜单并同步聚焦触发按钮,供贡献方随后打开其他浮层;贡献方读取自身投影并提供专用命令。启用的开关按钮参与菜单键盘导航。 + ----- diff --git a/packages/client/ui-model-selection/src/client/ModelSelect.tsx b/packages/client/ui-model-selection/src/client/ModelSelect.tsx index ecc984ca76..0369578a08 100644 --- a/packages/client/ui-model-selection/src/client/ModelSelect.tsx +++ b/packages/client/ui-model-selection/src/client/ModelSelect.tsx @@ -66,7 +66,7 @@ const MEASURE_STYLE: CSSProperties = { visibility: 'hidden', left: 0, top: 0 } export function ModelSelect( { locked, available, directory, load, select, resolveText, t, renderSlot }: ModelSelectInjected & Pick, 'locked'> & PropsLocale<'model'> - & Omit, 'SessionProvider'>, + & Omit, 'SessionProvider'>, ) { const state = useSyncExternalStore( fn => directory.subscribe(fn), @@ -267,7 +267,9 @@ export function ModelSelect( } const moveFocus = (offset: number): void => { - const items = itemRefs.current.filter(item => item !== null) + const items = Array.from(menuRef.current?.querySelectorAll( + 'button[role="menuitem"]:not([disabled]), button[role="menuitemradio"]:not([disabled]), button[role="switch"]:not([disabled])', + ) ?? []) if (items.length === 0) return const active = items.findIndex(item => item === document.activeElement) // Focus outside the rows (the trigger, which keeps it while the menu @@ -321,7 +323,9 @@ export function ModelSelect( // control inside the card (a retry button) keeps the browser's traversal, // so the keystroke stays unconsumed there. const focused = document.activeElement - const rows = itemRefs.current.filter((item): item is HTMLButtonElement => item !== null) + const rows = Array.from(menuRef.current?.querySelectorAll( + 'button[role="menuitem"]:not([disabled]), button[role="menuitemradio"]:not([disabled]), button[role="switch"]:not([disabled])', + ) ?? []) if (focused instanceof HTMLButtonElement && rows.includes(focused)) { event.preventDefault() focused.click() @@ -507,6 +511,14 @@ export function ModelSelect( )} + {renderSlot('conversation.input.model.options', { + locked: locked || busy, + dismiss: () => { + close() + triggerRef.current?.focus() + }, + reportFailure: (message) => { setToast({ seq: ++toastSeq.current, text: message }) }, + })} )} diff --git a/packages/client/ui-model-selection/src/client/index.ts b/packages/client/ui-model-selection/src/client/index.ts index b91ee20b48..224990e848 100644 --- a/packages/client/ui-model-selection/src/client/index.ts +++ b/packages/client/ui-model-selection/src/client/index.ts @@ -174,6 +174,7 @@ export function apply(ctx: ClientContext): void { locale: NS, children: { 'conversation.input.model.label': { kind: 'single', scope: 'session', inline: true }, + 'conversation.input.model.options': { kind: 'list', scope: 'session' }, }, inject: (sessionId): ModelSelectInjected => { const directory = models.directoryFor(sessionId) diff --git a/packages/client/ui-model-selection/src/client/slots.ts b/packages/client/ui-model-selection/src/client/slots.ts index 4ffddee580..39a31b673f 100644 --- a/packages/client/ui-model-selection/src/client/slots.ts +++ b/packages/client/ui-model-selection/src/client/slots.ts @@ -19,6 +19,19 @@ export interface ModelLabelOwner { declare module '@deepseek-ai/dsh-client-ui-slots' { interface SlotMap { + /** Session options appended after model and reasoning controls in the root menu. */ + 'conversation.input.model.options': { + kind: 'list' + scope: 'session' + owner: { + /** Whether the composer currently refuses selection changes. */ + locked: boolean + /** Close the menu and focus its trigger synchronously before opening another overlay. */ + dismiss: () => void + /** Report a failed selection through the menu owner's persistent toast. */ + reportFailure: (message: string) => void + } + } /** * Non-interactive inline model label; selection controls remain owner-managed. * Without a contribution, the badge precedes the catalog name in the trigger diff --git a/packages/client/ui-model-selection/tests/model-select.client.spec.tsx b/packages/client/ui-model-selection/tests/model-select.client.spec.tsx index dbb5b247db..2807820dc5 100644 --- a/packages/client/ui-model-selection/tests/model-select.client.spec.tsx +++ b/packages/client/ui-model-selection/tests/model-select.client.spec.tsx @@ -7,6 +7,7 @@ import { SessionId } from '@deepseek-ai/dsh-session/types' import type { ModelSelection } from '@deepseek-ai/dsh-api-remotes/client' import { createSnapshotStore } from '@deepseek-ai/dsh-client-store' import type { ComponentProps } from 'react' +import type { OwnerOf } from '@deepseek-ai/dsh-client-ui-slots' import type { ModelDirectoryState } from '../src/client/directory.ts' import { ModelSelect } from '../src/client/ModelSelect.tsx' import { en, zh } from '../src/client/locales.ts' @@ -98,6 +99,7 @@ describe('ModelSelect label slot', () => { }) await runtime.root.declare({ 'conversation.input.model.label': { kind: 'single', scope: 'session', inline: true }, + 'conversation.input.model.options': { kind: 'list', scope: 'session' }, }, ({ renderSlot, SessionProvider }) => @@ -1121,3 +1123,20 @@ it('restores the account model name after login without changing the saved route expect(screen.getByRole('button', { name: /选择模型,当前/ }).textContent).toBe('DeepSeek FlashHigh') expect(directory.getSnapshot().current).toEqual(selected) }) + + +it('dismisses contributed options with focus on the trigger before opening another overlay', () => { + const renderSlot = vi.fn((_name: string, _owner: object) => null) + render() + const trigger = screen.getByRole('button', { name: /^选择模型/ }) + fireEvent.click(trigger) + const options = renderSlot.mock.calls.find(call => call[0] === 'conversation.input.model.options') + if (options?.[0] !== 'conversation.input.model.options') throw new Error('Missing model options') + act(() => { + screen.getAllByRole('menuitem')[0]!.focus() + ;(options[1] as OwnerOf<'conversation.input.model.options'>).dismiss() + expect(document.activeElement).toBe(trigger) + }) + expect(screen.queryByRole('menu')).toBeNull() +}) diff --git a/packages/client/ui-plugin-manager/tests/components.client.spec.tsx b/packages/client/ui-plugin-manager/tests/components.client.spec.tsx index 6b541f65a6..2f504fbc9e 100644 --- a/packages/client/ui-plugin-manager/tests/components.client.spec.tsx +++ b/packages/client/ui-plugin-manager/tests/components.client.spec.tsx @@ -735,14 +735,14 @@ describe('PluginManagerPage', () => { pkg({ name: '@acme/dsh-tool', enabled: false }), // Selected by the profile but not a bundle: a problem the person can switch off, in the profile's own group. pkg({ name: 'dsh-selected', installed: false, error: { code: 'not-bundle' } }), - pkg({ name: '@deepseek-ai/dsh-experimental-agent-team-profile', installed: false, optional: true, removable: false, enabled: false }), + pkg({ name: '@deepseek-ai/dsh-experimental-voice-input-bundle', installed: false, optional: true, removable: false, enabled: false }), ], busy: ['dsh-protected'], }) const cards = screen.getAllByRole('listitem') // Offered extensions precede installed bundles. expect(cards.map(card => card.getAttribute('data-plugin-package'))).toEqual([ - '@deepseek-ai/dsh-experimental-agent-team-profile', 'dsh-better-sidebar', 'dsh-broken', 'dsh-protected', '@acme/dsh-tool', 'dsh-selected', + '@deepseek-ai/dsh-experimental-voice-input-bundle', 'dsh-better-sidebar', 'dsh-broken', 'dsh-protected', '@acme/dsh-tool', 'dsh-selected', ]) expect(cards.map(card => card.getAttribute('data-plugin-status'))).toEqual(['disabled', 'running', 'problem', 'running', 'disabled', 'problem']) // Group titles have no counts; the official bundle retains its experimental marker. @@ -776,11 +776,11 @@ describe('PluginManagerPage', () => { ].map(name => pkg({ name })), pkg({ name: '@acme/dsh-base', readOnlyReason: 'management-required' }), pkg({ name: 'dsh-better-sidebar' }), - pkg({ name: '@deepseek-ai/dsh-experimental-agent-team-profile', installed: false, optional: true, removable: false }), + pkg({ name: '@deepseek-ai/dsh-experimental-voice-input-bundle', installed: false, optional: true, removable: false }), ], }) expect(screen.getAllByRole('listitem').map(card => card.getAttribute('data-plugin-package'))).toEqual([ - '@deepseek-ai/dsh-experimental-agent-team-profile', '@acme/dsh-base', 'dsh-better-sidebar', + '@deepseek-ai/dsh-experimental-voice-input-bundle', '@acme/dsh-base', 'dsh-better-sidebar', ]) expect(document.querySelector('[data-plugin-count]')).toBeNull() }) @@ -789,7 +789,7 @@ describe('PluginManagerPage', () => { const name = '@deepseek-ai/dsh-experimental-terminal-bundle' const b = renderTab({ packages: [ pkg({ name, official: true, enabled, rows: [row()] }), - pkg({ name: '@deepseek-ai/dsh-experimental-agent-team-profile', official: true }), + pkg({ name: '@deepseek-ai/dsh-experimental-voice-input-bundle', official: true }), ] }) expect(document.querySelector(`[data-plugin-package="${name}"]`)).toBeNull() fireEvent.click(screen.getByRole('button', { name: en.moreAction })) @@ -807,7 +807,7 @@ describe('PluginManagerPage', () => { it('keeps a shipped bundle out of both lists until one of its declared rows runs', () => { const hidden = '@deepseek-ai/dsh-experimental-liveshare' - const shown = '@deepseek-ai/dsh-experimental-agent-team-profile' + const shown = '@deepseek-ai/dsh-experimental-voice-input-bundle' const offer = (rows: readonly PackageRow[]): PackageView => pkg({ name: hidden, official: true, installed: false, optional: true, removable: false, enabled: false, listedByDefault: false, rows, }) @@ -881,16 +881,16 @@ describe('PluginManagerPage', () => { }) it('opens an official bundle\'s page with its beta tag and no uninstall, and switches it on', () => { - const title = 'Agent Teams' + const title = 'Voice input' const { actions } = renderTab({ - packages: [pkg({ name: '@deepseek-ai/dsh-experimental-agent-team-profile', meta: { title }, installed: false, optional: true, removable: false, enabled: false })], + packages: [pkg({ name: '@deepseek-ai/dsh-experimental-voice-input-bundle', meta: { title }, installed: false, optional: true, removable: false, enabled: false })], }) fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', title) })) const detail = document.querySelector('[data-plugin-detail]') as HTMLElement expect(within(detail).getByText(en.statusBeta)).toBeTruthy() expect(within(detail).queryByRole('button', { name: en.uninstallLabel.replace('{name}', title) })).toBeNull() fireEvent.click(within(detail).getByRole('switch', { name: en.enableToggle.replace('{name}', title) })) - expect(actions.setEnabled).toHaveBeenCalledExactlyOnceWith('@deepseek-ai/dsh-experimental-agent-team-profile', true) + expect(actions.setEnabled).toHaveBeenCalledExactlyOnceWith('@deepseek-ai/dsh-experimental-voice-input-bundle', true) }) it('keeps the persisted selection while marking a failed component as a problem', () => { @@ -915,7 +915,7 @@ describe('PluginManagerPage', () => { }) it.each([ - ['@deepseek-ai/dsh-experimental-agent-team-profile', true], + ['@deepseek-ai/dsh-experimental-voice-input-bundle', true], ['@deepseek-ai/dsh-experimental-auto-review', true], ['@deepseek-ai/dsh-experimental-fixture-input', false], ['@deepseek-ai/dsh-cua-bundle', true], @@ -1198,11 +1198,11 @@ describe('PluginManagerPage', () => { } it('keeps configuration and installed plugins on the main list and shows eligible official plugins on the secondary page', () => { - const team = '@deepseek-ai/dsh-experimental-agent-team-profile' + const team = '@deepseek-ai/dsh-experimental-voice-input-bundle' renderTab( { packages: [ pkg({ name: '@deepseek-ai/dsh-experimental-badge-skill-bundle', official: true, enabled: false }), - pkg({ name: '@deepseek-ai/dsh-experimental-agent-team-profile', installed: false, optional: true, removable: false, enabled: false }), + pkg({ name: '@deepseek-ai/dsh-experimental-voice-input-bundle', installed: false, optional: true, removable: false, enabled: false }), pkg({ name: '@deepseek-ai/dsh-subagent-claude-code', official: true, availability: 'missing', installed: false, enabled: false }), pkg({ name: '@deepseek-ai/dsh-subagent-codex', official: true, enabled: false }), pkg({ name: '@deepseek-ai/dsh-experimental-tool-worktree', official: true, enabled: false }), @@ -1223,7 +1223,7 @@ describe('PluginManagerPage', () => { expect(within(extensions).getByRole('heading', { name: en.extensionsTitle })).toBeTruthy() expect(within(extensions).getByRole('button', { name: en.moreAction }).textContent).toBe(en.moreAction) expect(within(extensions).getAllByRole('listitem').map(card => card.getAttribute('data-plugin-package'))) - .toEqual(['@deepseek-ai/dsh-experimental-agent-team-profile', '@acme/new-official']) + .toEqual(['@deepseek-ai/dsh-experimental-voice-input-bundle', '@acme/new-official']) expect(document.querySelector('[data-plugin-count]')).toBeNull() expect(within(basic).getByText('Limits every command.')).toBeTruthy() expect(within(basic).queryByRole('switch', { name: en.enableToggle.replace('{name}', 'Shell') })).toBeNull() @@ -1363,7 +1363,7 @@ describe('PluginManagerPage', () => { '@deepseek-ai/dsh-experimental-tool-worktree', '@deepseek-ai/dsh-personalized-recommendations', ])('pins enabled %s onto the main list and removes it when disabled', (first) => { - const team = '@deepseek-ai/dsh-experimental-agent-team-profile' + const team = '@deepseek-ai/dsh-experimental-voice-input-bundle' const second = '@deepseek-ai/dsh-experimental-cot-translation-bundle' const entry = (name: string, enabled: boolean): PackageView => pkg({ name, official: true, enabled }) const b = renderTab({ packages: [entry(second, true), entry(team, true), entry(first, false)] }) diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml index aa69892ac6..75b0d5e942 100644 --- a/packages/client/ui-primitives/README.i18n.yaml +++ b/packages/client/ui-primitives/README.i18n.yaml @@ -18,14 +18,14 @@ en: ead6be55c7c9d7cf zh: 125ad36db7328551 /deepseek-ai-dsh-client-ui-primitives/use-this-package/component-catalog: - en: cfdc59fa1a077ff7 - zh: 4e6dcca712ba6565 + en: d898628500d48051 + zh: 3187f5e88b5d7ba5 /deepseek-ai-dsh-client-ui-primitives/use-this-package/controls-and-icons: en: 0d756c1e8207181d zh: 921ce997c6b14823 /deepseek-ai-dsh-client-ui-primitives/use-this-package/rendering-agent-output: - en: 3271871a42bd52c8 - zh: 89b7a9f03018454d + en: e2649937f077d4e0 + zh: 6ad604cb5adfc16b /deepseek-ai-dsh-client-ui-primitives/use-this-package/localizing-copy: en: 81e58c7b342baf3c zh: 6cd47b16124b215c @@ -54,8 +54,8 @@ en: ca75c51c89c2c9b0 zh: de54f3467ef3b9be /deepseek-ai-dsh-client-ui-primitives/known-limitations-and-deferred-work: - en: 2f2db9565903b4e6 - zh: ca9d8fed836d857c + en: 433ff539bd3d16b3 + zh: fcb37c1fbe503620 /deepseek-ai-dsh-client-ui-primitives/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md index 12b4a76c65..13c8e21341 100644 --- a/packages/client/ui-primitives/README.md +++ b/packages/client/ui-primitives/README.md @@ -45,7 +45,7 @@ Check this table before writing a control in a feature package. A plugin cannot | `Button` | Clickable action; `variant` selects `primary`, `ghost`, `outline`, or `toolbar`. Outlined actions use an inset hairline. Its ref targets the native button for focus and overlay anchoring. | | `Switch` | Two-state toggle, 36×20. The off-state thumb reads `--dsw-alias-switch-thumb` so it stays light in both themes, and the on-state thumb contrasts with the brand track. Disabled controls use half opacity. `label` is required, so the control cannot ship unnamed. | | `SegmentedControl` | Tablist of two or more equal-width segments with one sliding indicator, for switching a card or panel between a few modes; the owner holds the selection and `label` names the list. `id` seeds each tab's id (`-`) and the panel it controls (`--panel`), which the owner renders and points back at the tab with `aria-labelledby`; a segment may be `disabled` with a `title`, and `disabled` on the control locks every segment while the shown panel has work in flight. | -| `Checkbox` | Labeled native checkbox with controlled state, keyboard interaction, and disabled styling; the caller supplies localized `label` text. | +| `Checkbox` | Labeled native checkbox with controlled state, keyboard interaction, and disabled styling; the caller supplies localized `label` text. `modalAutofocus` marks the input for its owning Modal's initial focus without moving focus itself. | | `Input` | Single-line text entry for search boxes and inline forms. Its ref targets the native input for focus and is cleared on unmount. | | `InlineEditor` | Autofocused multi-line inline editor for short drafts such as queued messages and goals. It grows with its text and its width up to six lines, then scrolls; Enter saves, Shift+Enter breaks the line, Enter during IME composition does nothing, and Escape cancels. The caller supplies the localized `label` and rebinds the scrollbar on an elevated surface. | | `ZoomControls`, `DOCUMENT_ZOOM` | Floating document zoom controls with localized labels, fixed presets, and visibility/activity callbacks. An omitted `fit` hides the fitting row. The toolbar carries `data-document-zoom-controls`, plus `data-document-zoom-visible` while visible. An owner `className` overrides `bottom` and `max-width` regardless of stylesheet order; `inset-inline-start` and `transform` center the toolbar and move it on enter and exit, `opacity` fades it, and all three stay with the primitive. Renderers share the published scale limits. | @@ -61,8 +61,8 @@ Check this table before writing a control in a feature package. A plugin cannot | `AnimatedRows` | Keyed list movement with 200 ms glides and 100 ms entry/exit fades. Starts after local pointer or keyboard input, honors reduced motion, and settles loading or view replacement. Mark rows with `data-row-key`, supply matching `rowKeys`, and position the parent for the inert exit overlay; `role` and `label` belong to the caller. | | `useRowDragPreview` | Native drag preview using an inert offscreen row copy. The optional `inheritedProperties` list names the CSS custom properties whose computed values the offscreen wrapper copies; omitting it copies none. Keep the list reference stable, because replacing it clears an active preview. Unlisted custom properties follow normal inheritance at the preview's location. Supply a class for the surface background and, for a child drag handle, pass the row to `start`; `clear` and unmount remove the copy. | | `TextShimmer` | One left-to-right highlight across a row’s title, separators, summary, and suffix, with a 300ms initial delay, a one-second sweep and a 500ms rest. The mask spans the content width, capped by the visible row and tilts 15° from vertical, with a flat peak and soft edges. The moving decoration stays clipped to the row without enlarging its scrollable area. Base text inherits the caller’s color, including hover changes; the theme supplies the translucent highlight. Place icons and chevrons outside `TextShimmer` so only text and separators receive the highlight. A string child uses the same retained text interface; nested `TextShimmer` instances share the outer animation. Composite children render twice while active and must have no effects or element ids. Mark separators with `data-shimmer-decoration` so their fill receives the highlight. `DisclosureRow` forwards `contentClassName` for the text area and `contentLayoutClassName` for its inner layout. The inert decorative copy is excluded from interaction, selection, and accessibility; text updates retain the animation, and reduced-motion mode keeps the static base. | -| `Modal` | Centered dialog over a page mask. A nested dialog can intercept keys with `onKeyDownCapture` before document Escape handlers. The tint and dialog fade in while backdrop blur stays fully applied, honoring reduced motion. Set `backdropBlur={false}` when the caller already blurs the source page. | -| `RiskConfirmation` | Sensitive action gated behind an explicit checkbox. | +| `Modal` | Centered dialog over a page mask. A nested dialog can intercept keys with `onKeyDownCapture` before document Escape handlers. The tint and dialog fade in while backdrop blur stays fully applied, honoring reduced motion. Set `backdropBlur={false}` when the caller already blurs the source page. `tone="warning"` places an amber warning icon in the title and removes the body's extra top margin; other dialogs retain their spacing. | +| `RiskConfirmation` | Sensitive action gated behind an explicit checkbox, using the shared warning-title layout. | | `Tooltip` | Hover and keyboard-focus text anchored to a cloned child; optional `delayMs` controls hover delay and `focusDelayMs` controls keyboard-focus delay, both defaulting to 0 ms. Optional `portal` rendering escapes clipping containers and ancestor stacking contexts that cap the bubble's z-index. | | `HoverCard`, `HoverCardGroup` | Readable, optionally copyable hover preview; a physical list can opt cards into shared first-display dwell and exclusive display. | | `DraggableImage` | Original-image gestures with explicit transfer and feedback callbacks. | @@ -115,7 +115,7 @@ The nearest `MarkdownDelegateProvider` supplies optional `openExternalLink` and Explicit `./`, `../`, slash/backslash-rooted paths (including UNC), and Windows drive paths such as `C:/work/file.ts` take precedence over URL parsing. Otherwise, destinations accepted by the absolute URL parser remain URLs: `index.ts:24`, `Makefile:12`, and `localhost:3000` are links, without adding a protocol or testing whether they can open. Use `./index.ts:24` for a file reference. Other destinations are paths. Authored links reject `javascript:`, `vbscript:`, `data:`, and `blob:`; other parseable protocols retain native navigation. HTTP(S) links retain delegated navigation and modified-click behavior. No click-time failure notice is added. -`MarkdownText` renders untrusted GFM and TeX math, blocks unsafe links and images, and can turn resolved file mentions into explicit controls. Strikethrough requires `~~`; a single `~`, as in `10~20`, stays literal. A bare URL ends before non-ASCII punctuation or symbols, such as `。` or `,`, so CJK prose that follows it stays outside the link. A surrounding `MarkdownDelegateProvider` receives sanitized HTTP(S) URLs from ordinary clicks; modified clicks and links outside a provider retain native external-anchor behavior. `omitHtml` renders raw HTML as nothing instead of literal text, for documents written for HTML renderers. When the owner passes a `pathImages` vocabulary, image destinations that are local media paths rewrite to displayable URLs on settled renders only (the same streaming gate as file mentions); without a vocabulary, local destinations remain inert alt text. Rewritten images accept HTTP(S), data, blob, and the Desktop `dsh-app://app/api/file`, `dsh-app://app/api/plugin-market/detail-file`, and `dsh-app://app/api/plugin-manager/detail-file` routes; authored Desktop URLs remain inert. A load or decode failure replaces the image with its authored alt text, or the original destination when alt is empty; `fileImages` additionally supplies a localized failure prefix. Changing the image source permits a fresh load. Each mounted image appends a `load` query parameter to a resolved Host `api/file` URL, because browsers reuse a same-URL image within one document despite `no-store`; a newly rendered image of an overwritten file therefore loads its current bytes, and every remount requests the file again; the Host file route redirects that request to a cacheable URL for the current file version. While a reply streams, it freezes completed blocks, advances a top-level open fence by completed lines, and highlights that fence from saved Shiki grammar state. Completed token lines enter fixed-size React groups, so later chunks reconcile only the growing group; an unchanged fence retains that DOM when the final full parse resolves cross-document syntax. `TerminalBlock`, `ReadBlock`, `DiffBlock`, `SearchBlock`, and `WebBlock` render the matching tool-result intent with copy controls, overflow handling, and ANSI processing where applicable. `JsonTree` and `JsonBlock` inspect JSON values read-only, while `UserTextRun` renders uninterpreted text and `ReferenceChip` renders business-resolved labels. An explicit `onActivate` makes a chip a keyboard-accessible button; repeated pointer clicks and an existing text selection do not activate it, while keyboard activation remains available. Labels inherit their host’s wrapping policy. Recognition and JSON transforms belong to [ui-conversation](../ui-conversation/README.md#sent-user-text). +`MarkdownText` renders untrusted GFM and TeX math, blocks unsafe links and images, and can turn resolved file mentions into explicit controls. Copying a selection that lies inside one `MarkdownText` document and includes rendered math puts each whole formula on the clipboard as TeX source in both plain text and HTML, delimited as `$…$` inline and `$$…$$` for display math. Strikethrough requires `~~`; a single `~`, as in `10~20`, stays literal. A bare URL ends before non-ASCII punctuation or symbols, such as `。` or `,`, so CJK prose that follows it stays outside the link. A surrounding `MarkdownDelegateProvider` receives sanitized HTTP(S) URLs from ordinary clicks; modified clicks and links outside a provider retain native external-anchor behavior. `omitHtml` renders raw HTML as nothing instead of literal text, for documents written for HTML renderers. When the owner passes a `pathImages` vocabulary, image destinations that are local media paths rewrite to displayable URLs on settled renders only (the same streaming gate as file mentions); without a vocabulary, local destinations remain inert alt text. Rewritten images accept HTTP(S), data, blob, and the Desktop `dsh-app://app/api/file`, `dsh-app://app/api/plugin-market/detail-file`, and `dsh-app://app/api/plugin-manager/detail-file` routes; authored Desktop URLs remain inert. A load or decode failure replaces the image with its authored alt text, or the original destination when alt is empty; `fileImages` additionally supplies a localized failure prefix. Changing the image source permits a fresh load. Each mounted image appends a `load` query parameter to a resolved Host `api/file` URL, because browsers reuse a same-URL image within one document despite `no-store`; a newly rendered image of an overwritten file therefore loads its current bytes, and every remount requests the file again; the Host file route redirects that request to a cacheable URL for the current file version. While a reply streams, it freezes completed blocks, advances a top-level open fence by completed lines, and highlights that fence from saved Shiki grammar state. Completed token lines enter fixed-size React groups, so later chunks reconcile only the growing group; an unchanged fence retains that DOM when the final full parse resolves cross-document syntax. `TerminalBlock`, `ReadBlock`, `DiffBlock`, `SearchBlock`, and `WebBlock` render the matching tool-result intent with copy controls, overflow handling, and ANSI processing where applicable. `JsonTree` and `JsonBlock` inspect JSON values read-only, while `UserTextRun` renders uninterpreted text and `ReferenceChip` renders business-resolved labels. An explicit `onActivate` makes a chip a keyboard-accessible button; repeated pointer clicks and an existing text selection do not activate it, while keyboard activation remains available. Labels inherit their host’s wrapping policy. Recognition and JSON transforms belong to [ui-conversation](../ui-conversation/README.md#sent-user-text). `MarkdownText` defaults to `variant="body"`. Use `variant="compact"` for secondary content: its 13px text and 20px line height follow the content-size setting, all heading levels use the same size with weight 600, and paragraphs and lists use tighter spacing. Text, links, and code keep the tertiary color; dotted underlines distinguish links. Code headers scroll with their blocks. Tables and math stay enabled at the surrounding text size and scroll horizontally within the available width. Both variants share the parser and streaming cache. `textInteractionSource` marks its existing root as a source for text selection interactions. @@ -230,6 +230,7 @@ These limits define how the atoms behave at the edges; they are current package - **Known-site marks are a fixed list** — only the named hosts resolve to their own mark, and every other external host keeps the globe; recognizing an arbitrary site would require fetching its icon over the network. - **Streaming defers cross-boundary reference resolution** — a reference-style link or footnote whose definition sits on the other side of the incremental freeze boundary renders as literal text while the reply streams; the settled full parse at finalize resolves it. - **A long highlighted fence retains its complete token DOM** — streaming avoids re-parsing, re-tokenizing, and reconciling the completed prefix, but it does not discard old colors or virtualize token spans. Final DOM cardinality therefore still follows the fence's token count; nested/container fences and a pathological single long line remain on the general tail path. +- **Math copies as TeX only within one Markdown document** — a selection that reaches outside its `MarkdownText` root, such as from a user message into a reply, keeps the browser's native copy, which joins KaTeX's hidden MathML text with the visible glyphs. Inside one document, the plain text of non-math content comes from `innerText`, so code-block language labels and copy-button text are included, as in native copy. - **Overlong lines stay uncolored** — a source line of 1,000 or more UTF-16 units renders as one plain run in every highlighted surface, because TextMate scanning cost grows superlinearly with line length and a single generated line can otherwise block the page for seconds. The grammar state skips that line, so a comment or template literal that opens or closes on it mis-colors the following lines. Highlighting still runs synchronously during render, so a file with thousands of ordinary lines costs proportionally more. - **Bare URLs end before non-ASCII punctuation** — the [tokenizer patch](../../../patches/micromark-extension-gfm-autolink-literal@2.1.0.patch) and the [transform patch](../../../patches/mdast-util-gfm-autolink-literal@2.0.1.patch) end a GFM autolink literal at non-ASCII punctuation, symbols, or whitespace, so a URL with an unencoded character such as `(` or `・` in its path links only up to it; write such a URL as `<…>` or `[…](…)`. A URL followed directly by CJK letters still links them, because paths such as `/wiki/中文` are valid. Both patches are keyed to exact versions: an upgrade of either package must re-apply or retire its patch, otherwise the boundary rule silently disappears. - **The Go struct-field rule is patched locally** — when the Go grammar loads, its repeated one-line struct-field rule is replaced with the rule [worlpaker/go-syntax#25](https://github.com/worlpaker/go-syntax/pull/25) merged, because the vendored rule backtracks exponentially on lines such as a brace-bearing comment after `struct {`. If a dependency upgrade removes that rule, loading the grammar throws and Go code renders as plain text; delete the patch once `@shikijs/langs` ships the upstream rule. @@ -238,8 +239,6 @@ These limits define how the atoms behave at the edges; they are current package - **Product icons need the ui-theme sprite** — each icon only references its symbol, so without ui-theme, or inside another document such as an iframe, the icon renders nothing. - **No `Active` `StateDot` variant** — the supported states are done, warning, ongoing, error, and idle. - **User-facing copy is required at the render site** — the atoms are zero-Cordis and cannot reach `ctx.locale`; each feature must supply complete localized labels through the primitive's typed props ([decision](../../../.agents/notes/implemented/architecture/2026-08-23-locale-owned-client-ui-copy.md)). -- **Display math keeps fallback shapes** — a `$$x$$ trailing` on the opening line resolves through upstream inline math and leaves the surrounding prose in place, while the same trailing text on a later line declines the block and upstream takes the prose up to the next lone `$$` or EOF; a block left open ends at the next lone `$$` or at EOF; a later fence behind a `%` comment that holds text closes nothing, so a block nothing else closes reaches the same fallback; a multi-line block whose opening line carries a solitary `$` stays in its paragraph unless a blank line separates them; and a container continuation line that leaves the item indentation hands the block to upstream, which drops the opening line. `$$` fence meta is no longer honored ([decision](../../../.agents/notes/implemented/bug-fix/2026-09-17-multiline-display-dollar-math.md)). -- **A fence after a `\verb` delimiter is content** — the tokenizer reads the six characters before a fence, so the `$` right after `\verb` or `\verb*` is that command's delimiter rather than a closing fence, and a region whose text ends in `\verb` (`\verb**\verb*$$`) reads the same way. A fence inside an unterminated region (`\verb|$$`) still closes the block there, because the region's content is not parsed. - **`TerminalBlock` is not a terminal emulator** — it renders settled or still-running command output, not an interactive session: SGR colors, carriage return, backspace, erase-in-line, tab stops, and character width are honored; absolute cursor positioning, screen clearing, and alternate-screen sequences are stripped. diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md index 3b7a12e58b..77bf13ebc9 100644 --- a/packages/client/ui-primitives/README.zh.md +++ b/packages/client/ui-primitives/README.zh.md @@ -45,7 +45,7 @@ kind: "package-library" | `Button` | 可点击操作;`variant` 选择 `primary`、`ghost`、`outline` 或 `toolbar`。描边操作使用内侧细线。ref 指向原生按钮,供焦点控制与浮层锚定使用。 | | `Switch` | 36×20 的双态开关。关闭态滑块读取 `--dsw-alias-switch-thumb`,在两种主题下均保持亮色;开启态滑块与品牌色轨道形成对比。禁用控件使用半透明样式。`label` 必填,控件不可能在没有名称的情况下发布。 | | `SegmentedControl` | 两段或更多等宽分段加一个滑动指示块的 tablist,用于在几种模式间切换一张卡片或面板;选中项由调用方持有,`label` 为列表命名。`id` 派生每个 tab 的 id(`-`)及其控制的面板 id(`--panel`),面板由调用方渲染并用 `aria-labelledby` 指回 tab;分段可 `disabled` 并带 `title`,控件级 `disabled` 在当前面板有进行中的操作时锁住全部分段。 | -| `Checkbox` | 带标签的原生复选框,支持受控状态、键盘交互和禁用样式;调用方提供本地化的 `label` 文本。 | +| `Checkbox` | 带标签的原生复选框,支持受控状态、键盘交互和禁用样式;调用方提供本地化的 `label` 文本。`modalAutofocus` 将输入标记为所属 Modal 的初始焦点,本身不会移动焦点。 | | `Input` | 单行文本输入,用于搜索框与行内表单。ref 指向原生输入框,供焦点控制使用,并在卸载时清空。 | | `InlineEditor` | 自动聚焦的多行行内编辑器,用于排队消息、goal 等短草稿。它随文本和宽度增高,最多六行,之后滚动;Enter 保存,Shift+Enter 换行,输入法组合中的 Enter 不生效,Escape 取消。调用方提供本地化 `label`,并在 elevated 表面上重绑滚动条。 | | `ZoomControls`, `DOCUMENT_ZOOM` | 悬浮文档缩放控件,接收本地化标签、固定预设,以及可见性和交互状态回调。省略 `fit` 时不显示适配项。工具栏带 `data-document-zoom-controls`,可见时另带 `data-document-zoom-visible`。owner 的 `className` 覆盖 `bottom` 与 `max-width`,不受样式表加载顺序影响;`inset-inline-start` 和 `transform` 负责居中与进出场位移,`opacity` 负责淡入淡出,三者都由原语保留。渲染器共用导出的缩放范围。 | @@ -61,8 +61,8 @@ kind: "package-library" | `AnimatedRows` | 带键列表的位移动画:移动为 200 ms,进出淡变为 100 ms。列表内发生指针或键盘输入后才启用,遵循减少动态效果设置,加载或替换视图时直接完成。为行标记 `data-row-key` 并传入对应的 `rowKeys`,为父容器设置定位以承载不可交互的退出层;`role` 和 `label` 由调用方提供。 | | `useRowDragPreview` | 使用不可交互的离屏行副本生成原生拖拽预览。可选的 `inheritedProperties` 列表指定离屏包装层需要复制其计算值的 CSS 自定义属性;省略时不复制。列表引用需保持稳定,因为替换列表会清除活动预览。未列出的自定义属性按预览所在位置的正常继承规则生效。传入设置表面底色的类名;拖拽来自子级手柄时,将行元素传给 `start`;`clear` 和卸载会移除副本。 | | `TextShimmer` | 同一道高光从左到右扫过一行的标题、分隔符、摘要与后缀:首次等待 300ms,扫动一秒,再静止 500ms。遮罩覆盖内容宽度,并限制在可见行宽内,相对垂直方向倾斜 15°,峰值区域平坦,两侧柔和渐隐。移动的装饰层始终裁剪在行内,不会扩大可滚动区域。底色继承调用方的颜色,包括悬停变化;主题提供半透明高光。图标和箭头放在 `TextShimmer` 外,只有文字和分隔符参与扫光。字符串子节点沿用稳定的文字接口;嵌套的 `TextShimmer` 共用外层动画。活动期间组合子节点渲染两次,因此不得带副作用或元素 id。分隔符标记 `data-shimmer-decoration`,让填色参与扫光。`DisclosureRow` 将 `contentClassName` 传给文字区域,将 `contentLayoutClassName` 传给内部布局。惰性装饰副本不参与交互、选区和无障碍访问;文字更新保留动画,减少动态效果模式保留静态底色。 | -| `Modal` | 页面遮罩之上的居中对话框。嵌套对话框可通过 `onKeyDownCapture` 在文档级 Escape 处理器之前拦截按键。 色层与弹窗淡入,背景模糊始终完整生效,并遵循减少动态效果偏好。调用方已模糊源页面时设置 `backdropBlur={false}`。 | -| `RiskConfirmation` | 以显式复选框把关的敏感操作确认。 | +| `Modal` | 页面遮罩之上的居中对话框。嵌套对话框可通过 `onKeyDownCapture` 在文档级 Escape 处理器之前拦截按键。 色层与弹窗淡入,背景模糊始终完整生效,并遵循减少动态效果偏好。调用方已模糊源页面时设置 `backdropBlur={false}`。`tone="warning"` 在标题中显示橙色警告图标,并移除正文额外的顶部间距;其他弹窗的间距保持不变。 | +| `RiskConfirmation` | 以显式复选框把关的敏感操作确认,采用共用的警告标题布局。 | | `Tooltip` | 锚定在克隆子元素上的悬停与键盘聚焦提示;可选的 `delayMs` 控制悬停延迟,`focusDelayMs` 控制键盘聚焦延迟,均默认为 0 毫秒。可通过 `portal` 渲染到外层,避免被容器裁剪,或受祖先层叠上下文限制其 z-index。 | | `HoverCard`、`HoverCardGroup` | 可阅读、可选带复制按钮的悬停预览;物理列表可选择让卡片共享首次显示等待,并互斥显示。 | | `DraggableImage` | 通过显式传递与反馈回调提供原图拖放。 | @@ -115,7 +115,7 @@ kind: "package-library" 明确的 `./`、`../`、斜杠或反斜杠开头的路径(包括 UNC),以及 `C:/work/file.ts` 这样的 Windows 盘符路径优先于 URL 解析。其余目标若能被绝对 URL 解析器接受,就保留为 URL:`index.ts:24`、`Makefile:12` 和 `localhost:3000` 均为链接,不补协议,也不预先检查能否打开。文件引用使用 `./index.ts:24`。其他目标视为路径。正文链接禁止 `javascript:`、`vbscript:`、`data:` 和 `blob:`;其余可解析的协议保留原生导航。HTTP(S) 链接保留委托导航和修饰键点击行为。不添加点击后的失败提示。 -`MarkdownText` 渲染不可信的 GFM 与 TeX 公式、阻止不安全的链接与图片,并可把已解析的文件提及转换为显式控件。删除线需要 `~~`;单个 `~`(如 `10~20`)保持字面文本。裸 URL 在非 ASCII 标点或符号(如 `。`、`,`)前结束,紧随其后的中文正文不会并入链接。外层 `MarkdownDelegateProvider` 会接收普通点击产生的已净化 HTTP(S) URL;带修饰键的点击和 Provider 外的链接保留原生外部 anchor 行为。`omitHtml` 不渲染原始 HTML,而不是将其显示为原样文本,用于为 HTML 渲染器编写的文档。当 owner 传入 `pathImages` 词表时,本地媒体路径的图片目标只在落定渲染阶段重写为可展示 URL(与 file mentions 相同的流式门);不传词表时本地目标保持惰性 alt 文本。重写后的图片支持 HTTP(S)、data、blob 及桌面端 `dsh-app://app/api/file`、`dsh-app://app/api/plugin-market/detail-file` 与 `dsh-app://app/api/plugin-manager/detail-file` 路由;正文直接书写的桌面端 URL 仍保持惰性。加载或解码失败后,图片替换为作者的 alt 文本;alt 为空时显示原始目标路径;`fileImages` 还提供本地化的失败提示前缀。图片源变化后可重新加载。每个挂载的图片会为解析得到的 Host `api/file` 地址追加 `load` 查询参数,因为浏览器在同一文档内会无视 `no-store` 复用相同 URL 的图片;因此覆盖后的文件在新渲染的图片中会加载当前内容,每次重新挂载都会重新请求该文件;Host 文件路由会把该请求重定向到当前文件版本的可缓存地址。回复流式输出时,它冻结已完成的块、按已完成行推进顶层未闭合 fence,并从保存的 Shiki grammar state 为该 fence 增量高亮。已完成的 token 行进入固定大小的 React 分组,后续分片只 reconcile 正在增长的分组;最终全量解析解决跨文档语法时,未变化的 fence 会保留该 DOM。`TerminalBlock`、`ReadBlock`、`DiffBlock`、`SearchBlock` 与 `WebBlock` 把对应的工具结果意图渲染为带复制控件、溢出处理及适用时 ANSI 处理的卡片。`JsonTree` 与 `JsonBlock` 以只读方式检查 JSON 值;`UserTextRun` 渲染不做解析的文本,`ReferenceChip` 渲染业务已解析的标签。显式传入 `onActivate` 后,胶囊成为可由键盘操作的按钮;重复指针点击和已有文本选区不触发激活,键盘激活仍然可用。标签继承所属容器的换行规则。识别和 JSON 变换由 [ui-conversation](../ui-conversation/README.zh.md#sent-user-text) 负责。 +`MarkdownText` 渲染不可信的 GFM 与 TeX 公式、阻止不安全的链接与图片,并可把已解析的文件提及转换为显式控件。复制位于同一个 `MarkdownText` 文档内且包含已渲染公式的选区时,每个完整公式会以 TeX 源码同时写入剪贴板的纯文本和 HTML 格式,行内公式用 `$…$` 包裹,展示公式用 `$$…$$` 包裹。删除线需要 `~~`;单个 `~`(如 `10~20`)保持字面文本。裸 URL 在非 ASCII 标点或符号(如 `。`、`,`)前结束,紧随其后的中文正文不会并入链接。外层 `MarkdownDelegateProvider` 会接收普通点击产生的已净化 HTTP(S) URL;带修饰键的点击和 Provider 外的链接保留原生外部 anchor 行为。`omitHtml` 不渲染原始 HTML,而不是将其显示为原样文本,用于为 HTML 渲染器编写的文档。当 owner 传入 `pathImages` 词表时,本地媒体路径的图片目标只在落定渲染阶段重写为可展示 URL(与 file mentions 相同的流式门);不传词表时本地目标保持惰性 alt 文本。重写后的图片支持 HTTP(S)、data、blob 及桌面端 `dsh-app://app/api/file`、`dsh-app://app/api/plugin-market/detail-file` 与 `dsh-app://app/api/plugin-manager/detail-file` 路由;正文直接书写的桌面端 URL 仍保持惰性。加载或解码失败后,图片替换为作者的 alt 文本;alt 为空时显示原始目标路径;`fileImages` 还提供本地化的失败提示前缀。图片源变化后可重新加载。每个挂载的图片会为解析得到的 Host `api/file` 地址追加 `load` 查询参数,因为浏览器在同一文档内会无视 `no-store` 复用相同 URL 的图片;因此覆盖后的文件在新渲染的图片中会加载当前内容,每次重新挂载都会重新请求该文件;Host 文件路由会把该请求重定向到当前文件版本的可缓存地址。回复流式输出时,它冻结已完成的块、按已完成行推进顶层未闭合 fence,并从保存的 Shiki grammar state 为该 fence 增量高亮。已完成的 token 行进入固定大小的 React 分组,后续分片只 reconcile 正在增长的分组;最终全量解析解决跨文档语法时,未变化的 fence 会保留该 DOM。`TerminalBlock`、`ReadBlock`、`DiffBlock`、`SearchBlock` 与 `WebBlock` 把对应的工具结果意图渲染为带复制控件、溢出处理及适用时 ANSI 处理的卡片。`JsonTree` 与 `JsonBlock` 以只读方式检查 JSON 值;`UserTextRun` 渲染不做解析的文本,`ReferenceChip` 渲染业务已解析的标签。显式传入 `onActivate` 后,胶囊成为可由键盘操作的按钮;重复指针点击和已有文本选区不触发激活,键盘激活仍然可用。标签继承所属容器的换行规则。识别和 JSON 变换由 [ui-conversation](../ui-conversation/README.zh.md#sent-user-text) 负责。 `MarkdownText` 默认为 `variant="body"`。次级内容使用 `variant="compact"`:其 13px 字号与 20px 行高跟随内容字号设置,各级标题保持同一字号并使用 600 字重,段落与列表采用更紧凑的间距。正文、链接和代码均保持 tertiary 颜色,以点状下划线区分链接。代码标题栏随代码块滚动。表格和公式仍然启用,使用周围文字的字号,并在可用宽度内横向滚动。两个变体共享解析器与流式缓存。 `textInteractionSource` 将已有根元素标记为文本选区交互的来源。 @@ -230,6 +230,7 @@ kind: "package-library" - **已知站点标记是固定列表**:只有列名的主机解析为自己的标记,其余外部主机仍使用地球;要识别任意站点需要通过网络抓取它的图标。 - **流式期间跨边界引用解析被推迟**:定义落在增量冻结边界另一侧的引用式链接或脚注,在回复流式输出期间渲染为字面文本;定稿时的全量解析会将其解析。 - **长高亮 fence 会保留完整 token DOM**:流式路径避免重新解析、重新 tokenize 和 reconcile 已完成前缀,但不会丢弃旧颜色或虚拟化 token span。因此最终 DOM 数量仍随 fence 的 token 数增长;嵌套/容器内 fence 与病态的单个超长行仍走通用尾部路径。 +- **公式仅在单个 Markdown 文档内复制为 TeX**:超出其 `MarkdownText` 根节点的选区(例如从用户消息延伸到回复)沿用浏览器原生复制,会将 KaTeX 隐藏的 MathML 文本与可见字形拼接在一起。在单个文档内,非公式内容的纯文本取自 `innerText`,因此与原生复制一样,会包含代码块语言标签和复制按钮文本。 - **超长行不着色**:所有高亮界面中,长度达到 1,000 个 UTF-16 单元的源码行渲染为一段纯文本,因为 TextMate 扫描成本随行长超线性增长,单个生成行否则会阻塞页面数秒。语法状态会跳过该行,因此在该行开始或结束的注释、模板字符串会使后续行着色错误。高亮仍在渲染期间同步执行,因此数千个普通行的文件仍按比例增加开销。 - **裸 URL 在非 ASCII 标点前结束**:[tokenizer 补丁](../../../patches/micromark-extension-gfm-autolink-literal@2.1.0.patch)和[转换补丁](../../../patches/mdast-util-gfm-autolink-literal@2.0.1.patch)让 GFM 自动链接在非 ASCII 标点、符号或空白处结束,因此路径中含未编码 `(`、`・` 等字符的 URL 只链接到该字符之前;这类 URL 请写成 `<…>` 或 `[…](…)`。URL 后直接跟中日韩文字时仍会并入链接,因为 `/wiki/中文` 这类路径是合法的。两个补丁都绑定精确版本:升级任一依赖时必须重新应用或撤销对应补丁,否则该边界规则会悄然失效。 - **Go 结构体字段规则在本地打了补丁**:Go 语法加载时,重复匹配单行结构体字段的规则会替换为 [worlpaker/go-syntax#25](https://github.com/worlpaker/go-syntax/pull/25) 合入的规则,因为内置规则在 `struct {` 之后含花括号注释等行上会指数级回溯。若依赖升级后该规则消失,加载语法会抛错,Go 代码以纯文本显示;`@shikijs/langs` 发布上游规则后删除此补丁。 @@ -238,8 +239,6 @@ kind: "package-library" - **产品图标依赖 ui-theme 的 sprite**:每个图标只引用自己的 symbol,因此没有 ui-theme 时,或在 iframe 等另一个文档中,图标不渲染任何内容。 - **`StateDot` 没有 `Active` 变体**:支持的状态为 done、warning、ongoing、error 和 idle。 - **面向用户的文案必须由渲染点提供**:这些原子组件是 zero-Cordis 的,拿不到 `ctx.locale`;各功能必须通过原子组件的带类型 prop 提供完整本地化 label(见[决策](../../../.agents/notes/implemented/architecture/2026-08-23-locale-owned-client-ui-copy.zh.md))。 -- **显示数学保留若干回退形状**:开围栏行上的 `$$x$$ trailing` 走上游行内数学,前后正文保持不变;同样形态出现在后续行时块被放弃,随后由上游把正文吞到下一个独占行 `$$` 或文件结尾;未闭合的块在下一个独占行 `$$` 或文件结尾结束;后续行的围栏若位于**含文本**的 `%` 注释之后则不闭合,若无其他围栏闭合,块走同一条回退;开围栏行含孤立 `$` 的多行块会留在段落内,除非与前文空一行;容器内续行若离开条目缩进,块交由上游 flow 处理并丢弃开围栏行。`$$` 围栏 meta 不再生效(见[决策](../../../.agents/notes/implemented/bug-fix/2026-09-17-multiline-display-dollar-math.zh.md))。 -- **紧跟 `\verb` 分隔符的围栏属于内容**:解析器只读取围栏之前的六个字符,因此紧跟 `\verb` 或 `\verb*` 的 `$` 是该命令的分隔符而不是闭合围栏,内容以 `\verb` 结尾的区域(`\verb**\verb*$$`)同理。未闭合区域内部的围栏(`\verb|$$`)仍会在该处闭合块,因为区域内容不被解析。 - **`TerminalBlock` 不是终端模拟器**:它渲染已结束或仍在运行的命令输出,而不是交互式会话:SGR 颜色、回车、退格、行内擦除、制表位与字符宽度会被遵循;绝对光标定位、清屏与备用屏幕序列会被剥离。 diff --git a/packages/client/ui-primitives/src/Checkbox.tsx b/packages/client/ui-primitives/src/Checkbox.tsx index bf1cbf9a65..e53456863d 100644 --- a/packages/client/ui-primitives/src/Checkbox.tsx +++ b/packages/client/ui-primitives/src/Checkbox.tsx @@ -11,19 +11,22 @@ import css from './Checkbox.module.css' * @param props.disabled - whether the control refuses changes. * @param props.title - optional localized hover text. * @param props.className - extra class for the label's placement. + * @param props.modalAutofocus - designate the input for the owning Modal's initial focus without React autoFocus. * @returns the label containing its checkbox. */ -export function Checkbox({ checked, onChange, label, disabled = false, title, className }: { +export function Checkbox({ checked, onChange, label, disabled = false, title, className, modalAutofocus = false }: { checked: boolean onChange: (next: boolean) => void label: string disabled?: boolean title?: string | undefined className?: string | undefined + modalAutofocus?: boolean }) { return ( diff --git a/packages/client/ui-primitives/src/Modal.module.css b/packages/client/ui-primitives/src/Modal.module.css index 6be4e62b0a..d1a67e6a61 100644 --- a/packages/client/ui-primitives/src/Modal.module.css +++ b/packages/client/ui-primitives/src/Modal.module.css @@ -84,6 +84,22 @@ color: var(--dsw-alias-label-primary); } +.warning .title { + display: flex; + align-items: flex-start; + gap: 8px; +} + +.warningIcon { + flex: none; + margin-top: 3px; + color: var(--dsw-alias-state-warn-primary); +} + +.warning .body { + margin-top: 0; +} + .close { flex: none; display: inline-flex; diff --git a/packages/client/ui-primitives/src/Modal.tsx b/packages/client/ui-primitives/src/Modal.tsx index e7fa7faa2e..3f47c7cbc8 100644 --- a/packages/client/ui-primitives/src/Modal.tsx +++ b/packages/client/ui-primitives/src/Modal.tsx @@ -2,7 +2,7 @@ import { useRef } from 'react' import type { KeyboardEventHandler, ReactNode } from 'react' import { createPortal } from 'react-dom' import clsx from 'clsx' -import { IconCloseOutlineRegular } from './icons/index.tsx' +import { IconCloseOutlineRegular, IconWarningOutlineRegular } from './icons/index.tsx' import { useModalLayer } from './useModalLayer.ts' import css from './Modal.module.css' @@ -10,6 +10,7 @@ interface ModalBaseProps { open: boolean onClose: () => void title: string + tone?: 'warning' description?: string children?: ReactNode footer?: ReactNode @@ -31,6 +32,7 @@ type ModalProps = ModalBaseProps & ( * @param props.onClose - application close command, Escape, or mask click; while a menu is open inside the * dialog, Escape belongs to that menu first. * @param props.title - dialog heading (aria-label in every mode). + * @param props.tone - warning adds an amber title icon and compact title-to-body spacing; ignored for headless dialogs. * @param props.closeLabel - localized accessible close-button label. * @param props.description - optional supporting sentence under the title. * @param props.children - dialog body; mark its initial-focus control with @@ -47,7 +49,7 @@ type ModalProps = ModalBaseProps & ( */ export function Modal({ open, onClose, title, closeLabel, description, children, footer, className, contentClassName, - onKeyDownCapture, headless = false, backdropBlur = true, shortcutModal, + onKeyDownCapture, headless = false, backdropBlur = true, shortcutModal, tone, }: ModalProps) { const dialog = useRef(null) useModalLayer(dialog, open, onClose) @@ -61,7 +63,7 @@ export function Modal({ ref={dialog} tabIndex={-1} data-shortcut-modal={shortcutModal} - className={clsx(css.dialog, className)} + className={clsx(css.dialog, !headless && tone === 'warning' && css.warning, className)} role="dialog" aria-modal="true" aria-label={title} @@ -72,7 +74,10 @@ export function Modal({ <>
      -

      {title}

      +

      + {tone === 'warning' &&

      diff --git a/packages/client/ui-primitives/src/RiskConfirmation.module.css b/packages/client/ui-primitives/src/RiskConfirmation.module.css index 3c29131429..6a7db49ffa 100644 --- a/packages/client/ui-primitives/src/RiskConfirmation.module.css +++ b/packages/client/ui-primitives/src/RiskConfirmation.module.css @@ -16,52 +16,26 @@ } } -.warning { - display: flex; - align-items: flex-start; - gap: 10px; +.description { + margin: 0; color: var(--dsw-alias-label-secondary); font-size: 14px; line-height: 22px; } -.warning p { - margin: 0; -} - -.warningIcon { - flex: none; - margin-top: 2px; - color: var(--dsw-alias-state-error-primary); -} - -.acknowledgement { - display: flex; +.confirmation .acknowledgement { align-items: flex-start; - gap: 10px; - margin-top: 20px; - color: var(--dsw-alias-label-primary); + margin-top: 16px; + color: var(--dsw-alias-label-secondary); font-size: 14px; - line-height: 22px; - cursor: pointer; + font-weight: 400; + line-height: 20px; } -.acknowledgement input { - flex: none; - width: 16px; - height: 16px; - margin: 3px 0 0; - accent-color: var(--dsw-alias-button-primary-fill); - cursor: pointer; -} - -.acknowledgement input:focus-visible { - outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); - outline-offset: 2px; -} - -.acknowledgement input:disabled { - cursor: default; +.confirmation .acknowledgement input { + width: 14px; + height: 14px; + margin-top: 3px; } .modalAction { diff --git a/packages/client/ui-primitives/src/RiskConfirmation.tsx b/packages/client/ui-primitives/src/RiskConfirmation.tsx index 7109d096b2..1ad24e8298 100644 --- a/packages/client/ui-primitives/src/RiskConfirmation.tsx +++ b/packages/client/ui-primitives/src/RiskConfirmation.tsx @@ -3,7 +3,7 @@ * must gate a sensitive action behind an explicit checkbox. */ import { Button } from './Button.tsx' -import { IconWarningOutlineRegular } from './icons/index.tsx' +import { Checkbox } from './Checkbox.tsx' import { Modal } from './Modal.tsx' import css from './RiskConfirmation.module.css' @@ -45,6 +45,7 @@ export function RiskConfirmation({ open={open} onClose={onCancel} title={title} + tone="warning" closeLabel={closeLabel} className={css.confirmation ?? ''} contentClassName={css.confirmationContent ?? ''} @@ -64,20 +65,15 @@ export function RiskConfirmation({ )} > -
      - -

      {description}

      -
      - +

      {description}

      + ) } diff --git a/packages/client/ui-primitives/src/icons/artwork.ts b/packages/client/ui-primitives/src/icons/artwork.ts index 073f9988d3..28a3819995 100644 --- a/packages/client/ui-primitives/src/icons/artwork.ts +++ b/packages/client/ui-primitives/src/icons/artwork.ts @@ -20,6 +20,7 @@ interface IconArtwork { /** Default artwork keyed by glyph name; symbol ids derive from these keys. */ export const ICON_ARTWORK = { + 'layers-outline': { size: 16, svg: strokeWidth => `` }, 'save-outline': { size: 16, svg: strokeWidth => `` }, 'new-chat-outline': { size: 16, svg: strokeWidth => `` }, 'search-outline': { size: 16, svg: strokeWidth => `` }, @@ -59,7 +60,8 @@ export const ICON_ARTWORK = { 'check-circle-fill': { size: 16, svg: strokeWidth => `` }, 'warning-triangle-outline': { size: 16, svg: strokeWidth => `` }, 'user-outline': { size: 16, svg: strokeWidth => `` }, - 'users-outline': { size: 16, svg: strokeWidth => `` }, + // Users has inset contours in the 16px box; only coordinates, never stroke widths, carry the inset. + 'users-outline': { size: 16, svg: strokeWidth => `` }, 'paper-plane-outline': { size: 14, svg: strokeWidth => `` }, 'stop-fill': { size: 16, svg: strokeWidth => `` }, 'paperclip-outline': { size: 16, svg: strokeWidth => `` }, diff --git a/packages/client/ui-primitives/src/icons/index.tsx b/packages/client/ui-primitives/src/icons/index.tsx index a8f86d71ab..b6f03f1299 100644 --- a/packages/client/ui-primitives/src/icons/index.tsx +++ b/packages/client/ui-primitives/src/icons/index.tsx @@ -24,6 +24,12 @@ function productIcon(glyph: IconGlyph, weight: IconWeight): (props: IconProps) = ) } +/** Regular one-pixel IconLayersOutline artwork. */ +export const IconLayersOutlineRegular = productIcon('layers-outline', 'regular') + +/** Medium IconLayersOutline artwork with a 1.3px stroke. */ +export const IconLayersOutlineMedium = productIcon('layers-outline', 'medium') + /** Regular one-pixel IconNewChatOutline artwork. */ export const IconNewChatOutlineRegular = productIcon('new-chat-outline', 'regular') diff --git a/packages/client/ui-primitives/src/markdown/MarkdownText.tsx b/packages/client/ui-primitives/src/markdown/MarkdownText.tsx index 004c962963..8f1c4c7d29 100644 --- a/packages/client/ui-primitives/src/markdown/MarkdownText.tsx +++ b/packages/client/ui-primitives/src/markdown/MarkdownText.tsx @@ -14,6 +14,7 @@ import { memo, useMemo, useRef } from 'react' import type { ReactNode } from 'react' import clsx from 'clsx' +import { copyKatexAsTex } from './copy-tex.ts' import { IncrementalMarkdownParser } from './incremental.ts' import { parseGfm, parseGfmWithMath } from './parse.ts' import { @@ -179,6 +180,8 @@ class StreamingRenderer { * including `#L24` / `#L24-L30` fragments and `:24` / `:24-30` / `:24:5` suffixes * (ranges open at their first line, columns are ignored, and invalid locations * open only the file; see the README for URL/path classification). + * Copying a selection inside the document writes selected formulas as + * `$…$` / `$$…$$` TeX source. * @returns A GFM document with TeX math rendered through KaTeX; raw HTML never * renders as markup, and unsafe protocols are disabled. Local links without an opener remain text; * absolute HTTP(S) images render directly. @@ -210,5 +213,6 @@ export const MarkdownText = memo(function MarkdownText({ }, [text, streaming, labels, fileMentions, pathImages, omitHtml]) return
      {children}
      + data-markdown-variant={variant === 'compact' ? variant : undefined} + onCopy={(event) => { copyKatexAsTex(event.nativeEvent, event.currentTarget) }}>{children}
      }) diff --git a/packages/client/ui-primitives/src/markdown/copy-tex.ts b/packages/client/ui-primitives/src/markdown/copy-tex.ts new file mode 100644 index 0000000000..0b600281ec --- /dev/null +++ b/packages/client/ui-primitives/src/markdown/copy-tex.ts @@ -0,0 +1,53 @@ +/** Clipboard projection that copies rendered KaTeX formulas as their TeX source. */ + +/** The whole formula around a node, including the display wrapper that marks display math. */ +function closestFormula(node: Node): Element | null { + const element = node instanceof Element ? node : node.parentElement + return element?.closest('.katex-display') ?? element?.closest('.katex') ?? null +} + +/** + * Rewrite a copy whose selection lies inside one rendered Markdown document and + * includes rendered math, so each formula becomes its trimmed TeX source, + * delimited as `$…$` inline and `$$…$$` for display math, in both the + * plain-text and HTML clipboard data. A selection endpoint inside a formula + * extends to the whole formula in the clipboard only; the visible selection is + * unchanged. A copy without math, a selection reaching outside `root`, and a + * copy whose default an inner listener already prevented keep their data. + * @param event - Native copy event. + * @param root - The rendered Markdown document root. + */ +export function copyKatexAsTex(event: ClipboardEvent, root: HTMLElement): void { + const selection = root.ownerDocument.getSelection() + if (event.defaultPrevented || event.clipboardData === null || selection === null + || selection.rangeCount !== 1 || selection.isCollapsed) return + const range = selection.getRangeAt(0).cloneRange() + if (!root.contains(range.commonAncestorContainer)) return + const startFormula = closestFormula(range.startContainer) + if (startFormula !== null) range.setStartBefore(startFormula) + const endFormula = closestFormula(range.endContainer) + if (endFormula !== null) range.setEndAfter(endFormula) + const fragment = range.cloneContents() + const formulas = [...fragment.querySelectorAll('.katex')] + if (formulas.length === 0) return + for (const formula of formulas) { + const tex = formula.querySelector('annotation[encoding="application/x-tex"]')?.textContent.trim() + if (tex === undefined) continue + const delimiter = formula.closest('.katex-display') === null ? '$' : '$$' + formula.replaceWith(`${delimiter}${tex}${delimiter}`) + } + // innerText applies the browser's own block and line-break serialization, + // which needs a rendered element; an off-screen child of the root keeps the + // Markdown styles that decide block layout while staying invisible. + const container = root.ownerDocument.createElement('div') + container.style.cssText = 'position:fixed;top:0;left:-100000px;pointer-events:none' + container.append(fragment) + root.append(container) + try { + event.clipboardData.setData('text/plain', container.innerText) + event.clipboardData.setData('text/html', container.innerHTML) + } finally { + container.remove() + } + event.preventDefault() +} diff --git a/packages/client/ui-primitives/src/markdown/mathCompatibility.ts b/packages/client/ui-primitives/src/markdown/mathCompatibility.ts index 15932c5e76..3edd9d1e63 100644 --- a/packages/client/ui-primitives/src/markdown/mathCompatibility.ts +++ b/packages/client/ui-primitives/src/markdown/mathCompatibility.ts @@ -1,26 +1,13 @@ -/** - * Extend upstream dollar-only math syntax with TeX delimiters while reusing its - * token vocabulary. The dollar construct is dispatched before upstream's - * `mathFlow`, so the shapes it accepts keep the opening line's text as math - * instead of fence meta, which the renderer does not read. A `$$` candidate it - * declines — a `$$` sequence the line does not end after, or a block left open - * — is left to that upstream flow. - */ +/** Extend upstream dollar-only math syntax with TeX delimiters while reusing its token vocabulary. */ import { factorySpace } from 'micromark-factory-space' import type {} from 'micromark-extension-math' -import { markdownLineEnding, markdownSpace } from 'micromark-util-character' +import { markdownLineEnding } from 'micromark-util-character' import { codes, constants, types } from 'micromark-util-symbol' import type { Construct, Extension, Previous, State, Tokenizer } from 'micromark-util-types' // oxlint-disable typescript/no-this-alias -- micromark binds tokenizer context only on the outer callback. -/** Prefixes whose following character is a `\verb` delimiter, not a fence. */ -const VERB_DELIMITER_PREFIXES = ['\\verb', '\\verb*'] as const - -/** Longest prefix above, in code units. */ -const VERB_DELIMITER_TAIL = 6 - const previousBackslash: Previous = function (code) { if (code !== codes.backslash) return true const tail = this.events.at(-1) @@ -133,16 +120,10 @@ const tokenizeBackslashMathText: Tokenizer = function (effects, ok, nok) { } } -function createMathFlow(marker: number, openMarker: number, closeMarker: number): Construct { +function createMathFlow(marker: number, openMarker: number, closeMarker: number, multiline: boolean): Construct { const tokenize: Tokenizer = function (effects, ok, nok) { const self = this let oddBackslashRun = false - let inComment = false - let commentHasContent = false - let crossedLineEnding = false - let sawClosingFenceSequence = false - let consumedTail = '' - let failedClose = false const tail = self.events.at(-1) const initialSize = tail?.[1].type === types.linePrefix ? tail[2].sliceSerialize(tail[1], true).length @@ -174,13 +155,7 @@ function createMathFlow(marker: number, openMarker: number, closeMarker: number) function content(code: number | null): State | undefined { if (code === codes.eof) return nok(code) - if (code === marker && marker === codes.dollarSign && !inComment && verbDelimiterAhead()) { - // The `$` here delimits a `\verb…` region rather than closing the block, - // so it is content, and a `$` right after it closes an empty region and - // is content too. A comment holds no commands. - return verbatimDelimiter(code) - } - if (code === marker && closingFenceAllowed()) { + if (code === marker && (marker !== codes.dollarSign || !oddBackslashRun)) { return effects.attempt( { partial: true, tokenize: tokenizeClosingFence }, closed, @@ -188,83 +163,17 @@ function createMathFlow(marker: number, openMarker: number, closeMarker: number) )(code) } if (markdownLineEnding(code)) { - // A close that failed earlier on this candidate ends it at the line - // ending, which is what the grammar this flow replaced did: a failed - // `$$x$$ trailing` never reaches the next line. - if (marker === codes.dollarSign && failedClose) return nok(code) - // A trailing backslash escapes the line ending, not the next line's - // first character, so the escape run ends here, and a TeX comment ends - // with the line. The comment exception covers only a closing fence on a - // later line, which is where the multi-line block can hide one; the - // same-line form keeps closing as it did before this flow spanned lines. - oddBackslashRun = false - inComment = false - commentHasContent = false - consumedTail = '' - if (marker === codes.dollarSign) crossedLineEnding = true - // A paragraph-interruption check on the dollar flow must not consume the - // line ending: that keeps the paragraph open across the line boundary, - // and the document layer then counts the next line lazy for every - // construct tried afterwards. Declining leaves the interruption decision - // to the upstream flow, whose own check short-circuits without consuming - // and claims only the fences it accepts. The backslash construct has no - // upstream counterpart and keeps consuming here. - if (self.interrupt && marker === codes.dollarSign) return nok(code) - return effects.attempt(nonLazyContinuation, afterContinuation, nok)(code) + return multiline + ? effects.attempt(nonLazyContinuation, afterContinuation, nok)(code) + : nok(code) } return valueStart(code) } - /** - * Whether the marker under the scanner may close the block. The dollar flow - * keeps an escaped `$` and a `%` comment that holds text from closing. - */ - function closingFenceAllowed(): boolean { - if (marker !== codes.dollarSign) return true - return !oddBackslashRun && !(inComment && commentHasContent && crossedLineEnding) - } - - /** Whether the marker under the scanner is a `\verb` region's delimiter. */ - function verbDelimiterAhead(): boolean { - return VERB_DELIMITER_PREFIXES.some(prefix => consumedTail.endsWith(prefix)) - } - - function verbatimDelimiter(code: number | null): State | undefined { - effects.enter('mathFlowValue') - oddBackslashRun = false - rememberConsumed(code) - effects.consume(code) - return verbatimDelimiterClose - } - - function verbatimDelimiterClose(code: number | null): State | undefined { - if (code !== marker) return value(code) - rememberConsumed(code) - effects.consume(code) - return value - } - - /** Record one consumed code unit so a fence can be told from a delimiter. */ - function rememberConsumed(code: number | null): void { - /* v8 ignore next 2 -- every call site passes a code unit, never EOF. */ - if (code === null) return - consumedTail = `${consumedTail}${String.fromCharCode(code)}`.slice(-VERB_DELIMITER_TAIL) - } - function afterClosingFenceAttempt(code: number | null): State | undefined { - if (marker === codes.backslash) { - return effects.check({ partial: true, tokenize: tokenizeOpeningFence }, nok, markerValueStart)(code) - } - // A full `$$` that the line does not end after closes nothing there. On - // the opening line the scan continues, so `$$x$$ trailing` declines at - // that line's ending while a later `$$` on the same line still closes; on - // any later line it ends the candidate outright, so a following block is - // not swallowed. A solitary `$` is content, not a failed fence. - if (sawClosingFenceSequence) { - if (crossedLineEnding) return nok(code) - failedClose = true - } - return markerValueStart(code) + return marker === codes.backslash + ? effects.check({ partial: true, tokenize: tokenizeOpeningFence }, nok, markerValueStart)(code) + : markerValueStart(code) } function afterContinuation(code: number | null): State | undefined { @@ -279,17 +188,7 @@ function createMathFlow(marker: number, openMarker: number, closeMarker: number) function valueStart(code: number | null): State | undefined { effects.enter('mathFlowValue') - // An unescaped `%` opens a TeX comment wherever it is the first character - // of a value run, such as a line that starts inside the block. The - // backslash construct reads neither this state nor the escape run. oddBackslashRun = code === codes.backslash - if (code === codes.percentSign && marker === codes.dollarSign && !inComment) { - inComment = true - commentHasContent = false - } else if (inComment && !markdownSpace(code)) { - commentHasContent = true - } - rememberConsumed(code) effects.consume(code) return value } @@ -297,15 +196,12 @@ function createMathFlow(marker: number, openMarker: number, closeMarker: number) function markerValueStart(code: number | null): State | undefined { effects.enter('mathFlowValue') oddBackslashRun = false - if (inComment) commentHasContent = true - rememberConsumed(code) effects.consume(code) return valueAfterMarker } function valueAfterMarker(code: number | null): State | undefined { if (code === marker) { - rememberConsumed(code) effects.consume(code) return value } @@ -317,25 +213,7 @@ function createMathFlow(marker: number, openMarker: number, closeMarker: number) effects.exit('mathFlowValue') return content(code) } - const escaped = oddBackslashRun - const doubleBackslash = code === codes.backslash && escaped - oddBackslashRun = code === codes.backslash ? !escaped : false - // An unescaped `%` starts a TeX comment that runs to the line ending, so - // a `$$` inside it closes nothing. A `%` the fence follows immediately - // holds no comment text, which is how writers use it mid-formula - // (`95%$$`, and `95 %$$` alike); only a comment holding a non-whitespace - // character suppresses the close, and a `%` inside an open comment is - // comment text like any other character. - if (!escaped && code === codes.percentSign && marker === codes.dollarSign && !inComment) { - inComment = true - commentHasContent = false - } else if (inComment && !markdownSpace(code)) { - commentHasContent = true - } - // `\\verb` is a line break followed by letters, not the command, so the - // pair's second backslash never reaches the tail the command match reads. - if (doubleBackslash) consumedTail = '' - else rememberConsumed(code) + oddBackslashRun = code === codes.backslash ? !oddBackslashRun : false effects.consume(code) return value } @@ -350,7 +228,6 @@ function createMathFlow(marker: number, openMarker: number, closeMarker: number) closeOk: State, closeNok: State, ): State { - sawClosingFenceSequence = false return factorySpace(closeEffects, sequenceStart, types.linePrefix, constants.tabSize) function sequenceStart(code: number | null): State | undefined { @@ -365,7 +242,6 @@ function createMathFlow(marker: number, openMarker: number, closeMarker: number) if (code !== closeMarker) return closeNok(code) closeEffects.consume(code) closeEffects.exit('mathFlowFenceSequence') - sawClosingFenceSequence = true return factorySpace(closeEffects, after, types.whitespace) } @@ -402,7 +278,7 @@ function createMathFlow(marker: number, openMarker: number, closeMarker: number) return { concrete: true, - name: marker === codes.dollarSign ? 'dollarMathFlow' : 'backslashMathFlow', + name: marker === codes.dollarSign ? 'sameLineDollarMathFlow' : 'backslashMathFlow', tokenize, } } @@ -443,29 +319,29 @@ const backslashMathFlow = createMathFlow( codes.backslash, codes.leftSquareBracket, codes.rightSquareBracket, + true, ) -const dollarMathFlow = createMathFlow( +const sameLineDollarMathFlow = createMathFlow( codes.dollarSign, codes.dollarSign, codes.dollarSign, + false, ) const backslashMath: Extension = { flow: { [codes.backslash]: backslashMathFlow, - [codes.dollarSign]: dollarMathFlow, + [codes.dollarSign]: sameLineDollarMathFlow, }, text: { [codes.backslash]: backslashMathText }, } /** - * TeX backslash delimiters and display-dollar blocks as a micromark syntax - * extension reusing `micromark-extension-math`'s token vocabulary; the caller - * must also register `math()` on the same parse so the emitted tokens compile - * to standard math nodes. Both `$$$$` on one line and a `$$` block - * closed by a trailing `$$` parse as one math node, and a closing `$$` may sit - * at the end of a content line. + * TeX backslash delimiters and same-line display-dollar blocks as a micromark + * syntax extension reusing `micromark-extension-math`'s token vocabulary; the + * caller must also register `math()` on the same parse so the emitted tokens + * compile to standard math nodes. * @returns The micromark syntax extension. */ export function mathCompatibility(): Extension { diff --git a/packages/client/ui-primitives/src/markdown/parse.ts b/packages/client/ui-primitives/src/markdown/parse.ts index 11cc358c33..463e1003ba 100644 --- a/packages/client/ui-primitives/src/markdown/parse.ts +++ b/packages/client/ui-primitives/src/markdown/parse.ts @@ -43,10 +43,7 @@ export function parseGfm(text: string): Root { */ export function parseGfmWithMath(text: string): Root { return recoverLocalImages(fromMarkdown(text, { - // `mathCompatibility()` MUST stay last: micromark dispatches the extension - // registered last first, so its dollar flow takes precedence over `math()` - // and keeps the opening line's text as math instead of upstream's fence meta. - extensions: [gfm(gfmOptions), cjkFriendlyStrong(), math(), mathCompatibility()], + extensions: [gfm(gfmOptions), cjkFriendlyStrong(), mathCompatibility(), math()], mdastExtensions: [gfmFromMarkdown(), mathFromMarkdown(), windowsPathDestinations()], }), text) } diff --git a/packages/client/ui-primitives/tests/atoms.client.spec.tsx b/packages/client/ui-primitives/tests/atoms.client.spec.tsx index 3e7033474b..1bd9692b64 100644 --- a/packages/client/ui-primitives/tests/atoms.client.spec.tsx +++ b/packages/client/ui-primitives/tests/atoms.client.spec.tsx @@ -898,6 +898,22 @@ describe('Modal', () => { expect(onClose).toHaveBeenCalledTimes(2) }) + it('places a decorative warning icon in the title only for warning dialogs', () => { + const view = render( {}}> +

      Review the consequences.

      + ) + const heading = screen.getByRole('heading', { name: 'Confirm change' }) + const icon = heading.querySelector('svg') + expect(icon).not.toBeNull() + expect(icon?.getAttribute('aria-hidden')).toBe('true') + expect(screen.getByText('Review the consequences.').querySelector('svg')).toBeNull() + view.rerender( {}}>Body) + expect(screen.getByRole('heading', { name: 'Confirm change' }).querySelector('svg')).toBeNull() + view.rerender( {}}>Body) + expect(screen.queryByRole('heading')).toBeNull() + expect(screen.getByRole('dialog').querySelector('svg')).toBeNull() + }) + it('renders headless content without the default close chrome', () => { render( {}} title="Custom surface" headless backdropBlur={false}> diff --git a/packages/client/ui-primitives/tests/checkbox.client.spec.tsx b/packages/client/ui-primitives/tests/checkbox.client.spec.tsx index 838a589dab..1386decedc 100644 --- a/packages/client/ui-primitives/tests/checkbox.client.spec.tsx +++ b/packages/client/ui-primitives/tests/checkbox.client.spec.tsx @@ -25,6 +25,14 @@ it('refuses label activation while disabled', () => { expect(screen.getByRole('checkbox').checked).toBe(true) }) +it('marks initial modal focus only when requested', () => { + const view = render( {}} />) + expect(screen.getByRole('checkbox').hasAttribute('data-modal-autofocus')).toBe(false) + view.rerender( {}} modalAutofocus />) + expect(screen.getByRole('checkbox').hasAttribute('data-modal-autofocus')).toBe(true) + expect(document.activeElement).not.toBe(screen.getByRole('checkbox')) +}) + it('preserves caller hover text', () => { const onChange = vi.fn() render() diff --git a/packages/client/ui-primitives/tests/fixtures/markdown-dom/math-multiline-display.settled.txt b/packages/client/ui-primitives/tests/fixtures/markdown-dom/math-multiline-display.settled.txt deleted file mode 100644 index cb3815748b..0000000000 --- a/packages/client/ui-primitives/tests/fixtures/markdown-dom/math-multiline-display.settled.txt +++ /dev/null @@ -1,89 +0,0 @@ -
      -

      - #text "Opening line carries math:" - - - - - - - - #text "a" - - #text "+" - - #text "b" - - #text "c" - - #text "+" - - #text "d" - - #text "a+b\nc+d" -