From 61dea35bd29ef3a08dab16a84ad3447bc94655fa Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Wed, 19 Aug 2026 21:51:15 +0800 Subject: [PATCH] refactor: let Team own browser remotes --- ...08-18-experimental-agent-teams-packages.md | 4 +- ...18-experimental-agent-teams-packages.zh.md | 4 +- .../2026-08-06-agent-teams-web.i18n.yaml | 4 +- .../feature/2026-08-06-agent-teams-web.md | 12 +- .../feature/2026-08-06-agent-teams-web.zh.md | 12 +- ...solate-agent-team-browser-remote.i18n.yaml | 6 - ...08-19-isolate-agent-team-browser-remote.md | 31 ---- ...19-isolate-agent-team-browser-remote.zh.md | 31 ---- apps/web/tests/agent-team-panel.overlay.yml | 3 - knip.json | 6 +- packages/experimental/README.md | 1 - packages/experimental/README.zh.md | 1 - .../agent-team-remotes/README.i18n.yaml | 6 - .../experimental/agent-team-remotes/README.md | 22 --- .../agent-team-remotes/README.zh.md | 22 --- .../agent-team-remotes/package.json | 70 -------- .../agent-team-remotes/src/index.ts | 98 ----------- .../agent-team-remotes/src/invariant.ts | 23 --- .../agent-team-remotes/src/types.ts | 79 --------- .../tests/invariant.spec.ts | 16 -- .../tests/remote-adapter.spec.ts | 103 ------------ .../agent-team-remotes/tsconfig.json | 15 -- .../agent-team-remotes/tsdown.config.ts | 25 --- .../agent-team-web-profile/README.i18n.yaml | 4 +- .../agent-team-web-profile/README.md | 4 +- .../agent-team-web-profile/README.zh.md | 4 +- .../agent-team-web-profile/cordis.patch.yml | 5 +- .../agent-team-web-profile/package.json | 1 - .../tests/profile.spec.ts | 4 +- packages/experimental/agent-team/README.md | 4 +- packages/experimental/agent-team/README.zh.md | 4 +- packages/experimental/agent-team/package.json | 20 ++- packages/experimental/agent-team/src/index.ts | 53 +++++- packages/experimental/agent-team/src/types.ts | 17 ++ .../tests/built-lib.e2e.ts | 22 +-- .../agent-team/tests/team.spec.ts | 46 ++++++ .../experimental/agent-team/tsconfig.json | 1 + .../client-ui-agent-team/README.i18n.yaml | 4 +- .../client-ui-agent-team/README.md | 2 +- .../client-ui-agent-team/README.zh.md | 2 +- .../client-ui-agent-team/package.json | 4 +- .../src/client/TeamAction.tsx | 55 ++++--- .../client-ui-agent-team/src/client/index.ts | 39 +---- .../tests/browser-plugin.client.spec.ts | 31 ++-- .../tests/team-action.client.spec.tsx | 155 +++++++++++------- .../client-ui-agent-team/tsconfig.json | 2 +- scripts/gen-doc-graphs.ts | 12 +- scripts/run-gates.spec.ts | 2 +- scripts/run-gates.ts | 2 +- .../verify-package-readme-model-experience.ts | 1 - tsconfig.base.json | 1 - tsconfig.host.json | 1 - 52 files changed, 345 insertions(+), 751 deletions(-) delete mode 100644 .agents/notes/implemented/simplification/2026-08-19-isolate-agent-team-browser-remote.i18n.yaml delete mode 100644 .agents/notes/implemented/simplification/2026-08-19-isolate-agent-team-browser-remote.md delete mode 100644 .agents/notes/implemented/simplification/2026-08-19-isolate-agent-team-browser-remote.zh.md delete mode 100644 packages/experimental/agent-team-remotes/README.i18n.yaml delete mode 100644 packages/experimental/agent-team-remotes/README.md delete mode 100644 packages/experimental/agent-team-remotes/README.zh.md delete mode 100644 packages/experimental/agent-team-remotes/package.json delete mode 100644 packages/experimental/agent-team-remotes/src/index.ts delete mode 100644 packages/experimental/agent-team-remotes/src/invariant.ts delete mode 100644 packages/experimental/agent-team-remotes/src/types.ts delete mode 100644 packages/experimental/agent-team-remotes/tests/invariant.spec.ts delete mode 100644 packages/experimental/agent-team-remotes/tests/remote-adapter.spec.ts delete mode 100644 packages/experimental/agent-team-remotes/tsconfig.json delete mode 100644 packages/experimental/agent-team-remotes/tsdown.config.ts rename packages/experimental/{agent-team-remotes => agent-team}/tests/built-lib.e2e.ts (69%) diff --git a/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md b/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md index 144484deee..b769791d52 100644 --- a/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md +++ b/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md @@ -12,13 +12,13 @@ An experimental directory without a current package previously imposed placement ## Decision -`packages/experimental/agent-team`, `packages/experimental/tool-agent-team`, `packages/experimental/agent-team-profile`, `packages/experimental/agent-team-remotes`, `packages/experimental/client-ui-agent-team`, and `packages/experimental/agent-team-web-profile` are private workspace packages. The [experimental package naming decision](2026-08-19-experimental-package-name-prefix.md) owns their npm names and promotion rename; this note owns their placement, release exclusion, and dependency isolation. +`packages/experimental/agent-team`, `packages/experimental/tool-agent-team`, `packages/experimental/agent-team-profile`, `packages/experimental/client-ui-agent-team`, and `packages/experimental/agent-team-web-profile` are private workspace packages. The [experimental package naming decision](2026-08-19-experimental-package-name-prefix.md) owns their npm names and promotion rename; this note owns their placement, release exclusion, and dependency isolation. The dsh pack and publish set and the local baseline publisher exclude every manifest below `packages/experimental/`. `release:dsh` still advances their manifest versions with the shared dsh version without creating release tags. Workspace constraints require each experimental package to set `private: true` and omit `publishConfig`. The same top-level check rejects `dependencies`, `optionalDependencies`, and `peerDependencies` from release packages, release apps, or the Python runtime to an experimental package. Experimental packages may depend on release packages and each other; tests may use them through `devDependencies`, and examples may load them explicitly. The generic caller-reserved continuable child identity and selective direct-child drain remain in the stable Subagent service. They own Subagent identity and Activation lifecycle without importing or naming Agent Teams; the experimental Team service consumes them in the permitted direction. -The private Host-side Agent Teams profile bundle depends on the Team packages and applies after `dsh-base`. It inserts the Team rows and disables the global continuable-child controls whose model-visible names overlap the Team tools. The separate private Web profile applies after `dsh-web-app` and the Host profile; it inserts a Team-only Client Remote assembly and the Team UI. Both layers leave the shipped base, CLI, Web, and Python runtime dependency graphs unchanged. +The private Host-side Agent Teams profile bundle depends on the Team packages and applies after `dsh-base`. It inserts the Team rows and disables the global continuable-child controls whose model-visible names overlap the Team tools. The separate private Web profile applies after `dsh-web-app` and the Host profile; it inserts the Team UI, which mounts the Remote contribution generated by the Team package. Both layers leave the shipped base, CLI, Web, and Python runtime dependency graphs unchanged. Profile startup resolves selected bundles before healing module fallbacks. The shared fallback retains the dsh installation's carrier-specific entries: symlinks under plain Node and ESM proxies in a packaged executable. Missing packages from selected bundle closures are linked under the current profile's own `node_modules`, while pnpm-managed profile entries remain authoritative. Closure discovery starts from each explicit external bundle's real package directory and traverses every listed root even when an earlier dependency has the same package name. It excludes dsh-owned profile projections from later discovery, so a projected dependency cannot feed back into its own closure. Link ownership compares canonical parent paths so junction-normalized targets remain removable. A private profile layer can therefore carry experimental plugin rows without adding those plugins to a release app, requiring profile users to install transitive packages directly, weakening packaged-runtime module identity, or changing another profile's resolution. diff --git a/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.zh.md b/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.zh.md index 01e9e94319..25227770b5 100644 --- a/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.zh.md @@ -12,13 +12,13 @@ Agent Teams 的服务与工具约定仍在变化,但它需要使用真实 Sess ## 决策 -`packages/experimental/agent-team`、`packages/experimental/tool-agent-team`、`packages/experimental/agent-team-profile`、`packages/experimental/agent-team-remotes`、`packages/experimental/client-ui-agent-team` 与 `packages/experimental/agent-team-web-profile` 是私有 workspace 包。[实验性包命名决策](2026-08-19-experimental-package-name-prefix.zh.md)负责其 npm 名和 promotion 重命名;本记录负责其目录归属、发布排除与依赖隔离。 +`packages/experimental/agent-team`、`packages/experimental/tool-agent-team`、`packages/experimental/agent-team-profile`、`packages/experimental/client-ui-agent-team` 与 `packages/experimental/agent-team-web-profile` 是私有 workspace 包。[实验性包命名决策](2026-08-19-experimental-package-name-prefix.zh.md)负责其 npm 名和 promotion 重命名;本记录负责其目录归属、发布排除与依赖隔离。 dsh pack 与 publish 集合以及本地 baseline 发布器均排除 `packages/experimental/` 下的所有 manifest。`release:dsh` 仍会让这些 manifest 跟随 dsh 共享版本递增,但不会创建发布 tag。workspace 约束要求每个实验性包设置 `private: true` 并省略 `publishConfig`。同一个顶层检查会拒绝发布包、发布 app 或 Python runtime 通过 `dependencies`、`optionalDependencies` 或 `peerDependencies` 依赖实验性包。实验性包可以依赖发布包和其他实验性包;测试可以通过 `devDependencies` 使用它们,示例可以显式加载它们。 通用的调用方预留 continuable child 身份和精确 direct-child drain 仍属于稳定 Subagent 服务。它们负责 Subagent 身份与 Activation 生命周期,不 import 或命名 Agent Teams;实验性 Team 服务沿允许的方向消费这些能力。 -私有 Host 侧 Agent Teams profile bundle 依赖 Team 包,并在 `dsh-base` 之后应用。它会插入 Team 配置行,并禁用模型可见名称与 Team 工具重叠的全局 continuable-child control。独立的私有 Web profile 在 `dsh-web-app` 与 Host profile 之后应用;它会插入 Team 专用 Client Remote assembly 与 Team UI。两个层都保持已发布 base、CLI、Web 与 Python runtime 的依赖图不变。 +私有 Host 侧 Agent Teams profile bundle 依赖 Team 包,并在 `dsh-base` 之后应用。它会插入 Team 配置行,并禁用模型可见名称与 Team 工具重叠的全局 continuable-child control。独立的私有 Web profile 在 `dsh-web-app` 与 Host profile 之后应用;它会插入 Team UI,后者挂载 Team package 生成的 Remote contribution。两个层都保持已发布 base、CLI、Web 与 Python runtime 的依赖图不变。 profile 启动会先解析所选 bundle,再修复模块 fallback。共享 fallback 保留 dsh 安装的载体专用条目:普通 Node 下使用 symlink,打包 executable 中使用 ESM proxy。仅由所选 bundle 闭包携带的缺失包会链接到当前 profile 自己的 `node_modules` 下,而 pnpm 管理的 profile 条目仍具有优先权。闭包发现从每个显式外部 bundle 的真实包目录开始;即使前一个依赖具有相同包名,也会遍历所有列出的根。后续发现会排除 dsh 所有的 profile projection,避免投影后的依赖重新进入自己的闭包。link ownership 通过规范化父路径比较,使 junction 规范化后的 target 仍可删除。因此,私有 profile 层可以携带实验性 plugin 配置行,而无需把这些 plugin 加入发布 app、要求 profile 用户直接安装传递依赖、破坏 packaged-runtime 的模块身份,或改变其他 profile 的解析结果。 diff --git a/.agents/notes/implemented/feature/2026-08-06-agent-teams-web.i18n.yaml b/.agents/notes/implemented/feature/2026-08-06-agent-teams-web.i18n.yaml index cc197d7559..2ab9171008 100644 --- a/.agents/notes/implemented/feature/2026-08-06-agent-teams-web.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-06-agent-teams-web.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-06-agent-teams-web.md -2026-08-06-agent-teams-web.md: 94ac6ea9c03985e21e060bd3af25a0455a9d2ebc -2026-08-06-agent-teams-web.zh.md: a9cc1cc352e37d8ea6585d43908efd7e5cdabe1e +2026-08-06-agent-teams-web.md: fca3ee6a79f292b21c44a168e499e1be2179d7e9 +2026-08-06-agent-teams-web.zh.md: 5f3e0333aed08be831e6cfd653dc476f6ca6a374 diff --git a/.agents/notes/implemented/feature/2026-08-06-agent-teams-web.md b/.agents/notes/implemented/feature/2026-08-06-agent-teams-web.md index 94ac6ea9c0..fca3ee6a79 100644 --- a/.agents/notes/implemented/feature/2026-08-06-agent-teams-web.md +++ b/.agents/notes/implemented/feature/2026-08-06-agent-teams-web.md @@ -10,13 +10,13 @@ The durable Agent Teams runtime owns roster, mailbox, and task state but exposes ## Decision -The [browser Remote adapter decision](../simplification/2026-08-19-isolate-agent-team-browser-remote.md) places `teams/view`, `teams/createTask`, and `teams/updateTask` on the private `ctx.teamRemote` service, whose wire namespace remains `teams`. The adapter delegates to `ctx.teams` and owns browser-safe view and mutation-result types. Views contain roster and current task state but omit pending mailbox content and deleted task tombstones. Task conflicts cross Remote as a closed business result so the browser can preserve `team-task-conflict`; transport and lookup failures remain ordinary `RemoteResult` failures. +The private `ctx.teams` service owns generated `teams/view`, `teams/createTask`, and `teams/updateTask` Remote methods beside its domain operations. The Team package owns the browser-safe view and mutation-result types. Views contain roster and current task state but omit pending mailbox content and deleted task tombstones. Task conflicts cross Remote as a closed business result so the browser can preserve `team-task-conflict`; transport and lookup failures remain ordinary `RemoteResult` failures. -`@deepseek-ai/dsh-client-ui-agent-team` mounts the adapter's generated contribution through the stable `ctx.remote` service, then consumes `ctx.remote.teams`, Client Session navigation, locale, and slots. It displays roster status, model and diagnostics and supports task create, edit, dependency update, assignment, completion, reopen, and deletion. Every mutation sends the displayed revision. A conflict reloads the complete Team view and asks the user to review only after the reload succeeds; a reload failure remains visible. Overlapping refreshes publish only the latest request for the selected Session, and a successful mutation invalidates older refresh snapshots. +`@deepseek-ai/dsh-client-ui-agent-team` mounts the `@deepseek-ai/dsh-team/remote` contribution through the stable `ctx.remote` service, then consumes the generated `ctx.remote.teams` methods without an additional Client result wrapper. It displays roster status, model and diagnostics and supports task create, edit, dependency update, assignment, completion, reopen, and deletion. Every mutation sends the displayed revision. A conflict reloads the complete Team view and asks the user to review only after the reload succeeds; a reload failure remains visible. Overlapping refreshes publish only the latest request for the selected Session, and a successful mutation invalidates older refresh snapshots. Teammate navigation uses the existing `{ parentSessionId, childSessionId, mode: 'continuable' }` Subagent address without a Team tag. The UI refreshes the direct-child catalog, rechecks the selected Session, and opens the addressed conversation. History and later human prompts follow the stable Subagent path; the Team mailbox remains reserved for Team peer delivery from Team tools. -`@deepseek-ai/dsh-agent-team-web-profile` inserts the private Host Remote adapter and UI after the stable Web bundle. It is applied alongside the Host-side `@deepseek-ai/dsh-agent-team-profile`. Neither stable bundle contains disabled Team rows or dependencies. +`@deepseek-ai/dsh-agent-team-web-profile` inserts only the UI after the stable Web bundle. It is applied alongside the Host-side `@deepseek-ai/dsh-agent-team-profile`, which already inserts `ctx.teams` and the model tools. Neither stable bundle contains disabled Team rows or dependencies. ## Boundaries @@ -26,14 +26,16 @@ The Web UI has no mailbox timeline, worktree or Git controls, teammate creation, **Extend the legacy API Proxy Team RPC map.** Rejected because it would put an experimental domain in a stable wire package and duplicate the generated Remote vocabulary and validation. +**Introduce a separate browser Remote service.** Rejected because the methods have no state, lifecycle, or policy owner distinct from `ctx.teams`; a second Cordis service would duplicate Team injection and require another package for the same Typert namespace. + **Add Team metadata to the stable Subagent address and prompt routing.** Rejected because ordinary child navigation already identifies the conversation. A Team tag would couple stable Client and Subagent contracts to experimental mailbox policy. **Put disabled Team rows in the stable Web bundle.** Rejected because a disabled row still creates release dependencies and makes the experimental package part of shipped composition. ## Testing -Remote-adapter unit tests, generation, and a plain-Node built-artifact smoke verify delegation, error mapping, and typed methods. Client typechecking and browser component tests cover the mounted namespace, Lead routing, every task action, successful and failed conflict reloads, stale async results, navigation, disposal, and status or error presentation. A Web end-to-end test composes both experimental profile layers over the real Host Remote flow. +Team-service unit tests, generation, and a plain-Node built-artifact smoke verify the direct Remote methods, error mapping, and exported descriptors. Client typechecking and browser component tests cover the mounted namespace, Lead routing, raw generated results, every task action, successful and failed conflict reloads, stale async results, navigation, disposal, and status or error presentation. A Web end-to-end test composes both experimental profile layers over the real Host Remote flow. ## Consequences -The Team service remains the only state machine and exposes no browser-specific methods or result types. The stable API Proxy, Client runtime, Subagent UI, and Web bundle remain Team-agnostic. Source-checkout users must add two ordered experimental profile layers to a Web profile, and promotion can move those packages without changing their npm names or generated namespace. +The Team service is the single Cordis owner for both domain state and the Remote operations that expose selected Team values. The stable API Proxy, Client runtime, Subagent UI, and Web bundle remain Team-agnostic. Source-checkout users must add two ordered experimental profile layers to a Web profile, and promotion can move those packages without changing their npm names or generated namespace. diff --git a/.agents/notes/implemented/feature/2026-08-06-agent-teams-web.zh.md b/.agents/notes/implemented/feature/2026-08-06-agent-teams-web.zh.md index a9cc1cc352..5f3e0333ae 100644 --- a/.agents/notes/implemented/feature/2026-08-06-agent-teams-web.zh.md +++ b/.agents/notes/implemented/feature/2026-08-06-agent-teams-web.zh.md @@ -10,13 +10,13 @@ ## 决策 -[浏览器 Remote adapter 决策](../simplification/2026-08-19-isolate-agent-team-browser-remote.md)将 `teams/view`、`teams/createTask` 与 `teams/updateTask` 放在私有 `ctx.teamRemote` service 上,其 wire namespace 仍为 `teams`。Adapter 委托给 `ctx.teams`,并拥有浏览器安全的 view 与 mutation-result type。View 包含 roster 与当前 task 状态,但不包含 pending mailbox 内容或已删除 task tombstone。Task conflict 通过封闭 business result 跨越 Remote,使浏览器保留 `team-task-conflict`;transport 与 lookup failure 仍是普通 `RemoteResult` failure。 +私有 `ctx.teams` service 除 domain operation 外,还直接负责生成式 `teams/view`、`teams/createTask` 与 `teams/updateTask` Remote method。Team package 负责浏览器安全的 view 与 mutation-result type。View 包含 roster 与当前 task 状态,但不包含 pending mailbox 内容或已删除 task tombstone。Task conflict 通过封闭 business result 跨越 Remote,使浏览器保留 `team-task-conflict`;transport 与 lookup failure 仍是普通 `RemoteResult` failure。 -`@deepseek-ai/dsh-client-ui-agent-team` 通过稳定 `ctx.remote` service 挂载 adapter 的生成式 contribution,随后消费 `ctx.remote.teams`、Client Session navigation、locale 与 slot。它展示 roster status、model 与 diagnostics,并支持 task create、edit、dependency update、assignment、completion、reopen 与 deletion。每次 mutation 都发送当前显示的 revision。Conflict 仅在重新读取完整 Team view 成功后要求用户检查;如果重新读取失败,则保留该错误。重叠 refresh 只发布所选 Session 的最新请求,成功 mutation 会让更早的 refresh snapshot 失效。 +`@deepseek-ai/dsh-client-ui-agent-team` 通过稳定 `ctx.remote` service 挂载 `@deepseek-ai/dsh-team/remote` contribution,随后直接消费生成式 `ctx.remote.teams` method,不增加 Client result 包装层。它展示 roster status、model 与 diagnostics,并支持 task create、edit、dependency update、assignment、completion、reopen 与 deletion。每次 mutation 都发送当前显示的 revision。Conflict 仅在重新读取完整 Team view 成功后要求用户检查;如果重新读取失败,则保留该错误。重叠 refresh 只发布所选 Session 的最新请求,成功 mutation 会让更早的 refresh snapshot 失效。 Teammate navigation 使用既有 `{ parentSessionId, childSessionId, mode: 'continuable' }` Subagent address,不带 Team tag。UI 刷新直接 child catalog、再次检查所选 Session,然后打开 addressed conversation。History 与后续人类 prompt 使用稳定 Subagent 路径;Team mailbox 只用于 Team 工具发起的 Team peer delivery。 -`@deepseek-ai/dsh-agent-team-web-profile` 在稳定 Web bundle 之后插入私有 Host Remote adapter 与 UI。它与 Host 侧 `@deepseek-ai/dsh-agent-team-profile` 一起应用。两个稳定 bundle 都不包含禁用的 Team row 或依赖。 +`@deepseek-ai/dsh-agent-team-web-profile` 在稳定 Web bundle 之后只插入 UI。它与 Host 侧 `@deepseek-ai/dsh-agent-team-profile` 一起应用,后者已经插入 `ctx.teams` 与模型工具。两个稳定 bundle 都不包含禁用的 Team row 或依赖。 ## 边界 @@ -26,14 +26,16 @@ Web UI 不提供 mailbox timeline、worktree 或 Git control、teammate creation **扩展 legacy API Proxy Team RPC map。** 拒绝,因为这会把实验性 domain 放入稳定 wire package,并重复生成式 Remote vocabulary 与 validation。 +**引入独立的浏览器 Remote service。** 拒绝,因为这些 method 没有区别于 `ctx.teams` 的状态、lifecycle 或 policy owner;第二个 Cordis service 会重复 Team injection,并要求另一个 package 提供同一个 Typert namespace。 + **向稳定 Subagent address 与 prompt routing 添加 Team metadata。** 拒绝,因为普通 child navigation 已经标识会话;Team tag 会让稳定 Client 与 Subagent contract 耦合实验性 mailbox policy。 **在稳定 Web bundle 中加入禁用 Team row。** 拒绝,因为禁用 row 仍会产生 release 依赖,并让实验性 package 成为随附 composition 的一部分。 ## 测试 -Remote adapter 单元测试、生成流程与 plain-Node built-artifact smoke 校验 delegation、error mapping 与 typed method。Client typecheck 与浏览器 component test 覆盖挂载 namespace、Lead routing、所有 task action、成功及失败的 conflict reload、陈旧 async result、navigation、dispose 与状态或错误呈现。Web 端到端测试在真实 Host Remote flow 上组合两个实验性 profile 层。 +Team service 单元测试、生成流程与 plain-Node built-artifact smoke 校验直接 Remote method、error mapping 与导出 descriptor。Client typecheck 与浏览器 component test 覆盖挂载 namespace、Lead routing、原始生成式 result、所有 task action、成功及失败的 conflict reload、陈旧 async result、navigation、dispose 与状态或错误呈现。Web 端到端测试在真实 Host Remote flow 上组合两个实验性 profile 层。 ## 后果 -Team service 仍是唯一状态机,且不公开浏览器专用 method 或 result type。稳定 API Proxy、Client runtime、Subagent UI 和 Web bundle 保持 Team 无关。源码 checkout 用户必须向 Web profile 添加两个有序 experimental profile 层;promotion 可以移动这些 package,而无需修改 npm name 或生成式 namespace。 +Team service 是 domain state 与公开选定 Team value 的 Remote operation 的唯一 Cordis owner。稳定 API Proxy、Client runtime、Subagent UI 和 Web bundle 保持 Team 无关。源码 checkout 用户必须向 Web profile 添加两个有序 experimental profile 层;promotion 可以移动这些 package,而无需修改 npm name 或生成式 namespace。 diff --git a/.agents/notes/implemented/simplification/2026-08-19-isolate-agent-team-browser-remote.i18n.yaml b/.agents/notes/implemented/simplification/2026-08-19-isolate-agent-team-browser-remote.i18n.yaml deleted file mode 100644 index 05f956f10e..0000000000 --- a/.agents/notes/implemented/simplification/2026-08-19-isolate-agent-team-browser-remote.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# side as of the last confirmed-consistent state. Both languages carry equal authority; -# after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-19-isolate-agent-team-browser-remote.md -2026-08-19-isolate-agent-team-browser-remote.md: 59cc23c25a434539a60eaa500b088383c039f84a -2026-08-19-isolate-agent-team-browser-remote.zh.md: feb4fcbb5c1f588988bb891c756b1eed7c9855fc diff --git a/.agents/notes/implemented/simplification/2026-08-19-isolate-agent-team-browser-remote.md b/.agents/notes/implemented/simplification/2026-08-19-isolate-agent-team-browser-remote.md deleted file mode 100644 index 59cc23c25a..0000000000 --- a/.agents/notes/implemented/simplification/2026-08-19-isolate-agent-team-browser-remote.md +++ /dev/null @@ -1,31 +0,0 @@ -# Agent Note: Isolate the Agent Teams browser Remote adapter - -Status: implemented - -English | [中文](2026-08-19-isolate-agent-team-browser-remote.zh.md) - -## Problem - -The Agent Teams domain service owned browser-specific Remote methods, view composition, and transport error mapping in addition to roster, mailbox, task, and lifecycle behavior. Mounting its generated contribution also required a separate package with an inert Host entry and both Host and Client compiler faces. Those responsibilities widened `ctx.teams` for one consumer and created a compiler-layout exception without an independent runtime owner. - -## Decision - -`@deepseek-ai/dsh-team` is a domain-only `ctx.teams` service. `@deepseek-ai/dsh-agent-team-remotes` provides a stateless Host adapter registered as `ctx.teamRemote` with the distinct Typert wire namespace `teams`. Its `view`, `createTask`, and `updateTask` methods delegate to the exact `ctx.teams` instance selected by Cordis injection. Browser view types and the closed task-mutation result belong to the adapter package; Team errors are mapped there, while unexpected failures remain rejected. - -The adapter package registers in the Host aggregate only. It generates the `ctx.remote.teams` Client contribution but has no Client plugin entry or inert Host half. `@deepseek-ai/dsh-client-ui-agent-team` mounts that contribution through the stable `ctx.remote` service and returns the generated disposer from its own plugin lifecycle. - -## Alternatives considered - -**Keep Remote methods on `TeamService`.** Rejected because view composition and carrier-facing error mapping serve only the browser consumer and make the domain service's public API depend on one presentation. - -**Keep a separate Client assembly package.** Rejected because the assembly had no Host behavior, yet its package required Host and Client compiler faces solely to provide an inert root export and one `$mount()` call. - -**Add the Team contribution to stable API Remotes.** Rejected because stable release packages cannot depend on private experimental packages, and doing so would make the Team namespace part of the shipped Client assembly. - -## Testing - -Adapter unit tests verify service-key separation, delegation, business-error mapping, and unexpected rejection propagation. Generated-artifact and plain-Node build checks verify the exported `teams` descriptors. Browser tests verify contribution mounting and disposal, and the Web composition test exercises the Host adapter through the real gateway. - -## Consequences - -`ctx.teams` is the only owner of Team state and exposes no browser-only operations or values. The Web profile carries one additional stateless Host service, and the UI package owns the contribution mount. Adding a browser operation changes the adapter and its generated artifacts without changing the Team domain interface. diff --git a/.agents/notes/implemented/simplification/2026-08-19-isolate-agent-team-browser-remote.zh.md b/.agents/notes/implemented/simplification/2026-08-19-isolate-agent-team-browser-remote.zh.md deleted file mode 100644 index feb4fcbb5c..0000000000 --- a/.agents/notes/implemented/simplification/2026-08-19-isolate-agent-team-browser-remote.zh.md +++ /dev/null @@ -1,31 +0,0 @@ -# Agent Note: 隔离 Agent Teams 浏览器 Remote adapter - -Status: implemented - -[English](2026-08-19-isolate-agent-team-browser-remote.md) | 中文 - -## 问题 - -Agent Teams domain service 除 roster、mailbox、task 与 lifecycle 行为外,还负责浏览器专用 Remote method、view composition 与 transport error mapping。挂载其生成式 contribution 还需要一个具有 inert Host entry 及 Host、Client 两个 compiler face 的独立 package。这些职责为单一 consumer 扩大了 `ctx.teams`,并在没有独立 runtime owner 的情况下引入 compiler layout 例外。 - -## 决策 - -`@deepseek-ai/dsh-team` 是仅负责 domain 的 `ctx.teams` service。`@deepseek-ai/dsh-agent-team-remotes` 提供无状态 Host adapter,注册为 `ctx.teamRemote`,并使用独立 Typert wire namespace `teams`。其 `view`、`createTask` 与 `updateTask` method 委托给 Cordis injection 选择的同一个 `ctx.teams` instance。浏览器 view type 与封闭 task-mutation result 归 adapter package 所有;Team error 在此映射,意外 failure 仍保持 rejection。 - -Adapter package 只注册到 Host aggregate。它生成 `ctx.remote.teams` Client contribution,但没有 Client plugin entry 或 inert Host half。`@deepseek-ai/dsh-client-ui-agent-team` 通过稳定 `ctx.remote` service 挂载该 contribution,并从自身 plugin lifecycle 返回生成式 disposer。 - -## 考虑过的替代方案 - -**在 `TeamService` 上保留 Remote method。** 拒绝,因为 view composition 与 carrier-facing error mapping 只服务浏览器 consumer,会使 domain service 的公共 API 依赖单一 presentation。 - -**保留独立 Client assembly package。** 拒绝,因为该 assembly 没有 Host 行为,却仅为 inert root export 与一次 `$mount()` call 要求 Host 和 Client 两个 compiler face。 - -**将 Team contribution 加入稳定 API Remotes。** 拒绝,因为稳定 release package 不能依赖私有 experimental package,且这样会使 Team namespace 成为随附 Client assembly 的一部分。 - -## 测试 - -Adapter 单元测试校验 service-key 分离、delegation、business-error mapping 与意外 rejection propagation。生成式 artifact 与 plain-Node build check 校验导出的 `teams` descriptor。浏览器测试校验 contribution mount 与 disposal,Web composition test 通过真实 gateway 执行 Host adapter。 - -## 后果 - -`ctx.teams` 是 Team state 的唯一 owner,不公开浏览器专用 operation 或 value。Web profile 增加一个无状态 Host service,UI package 负责 contribution mount。增加浏览器 operation 时,只需修改 adapter 及其生成式 artifact,无需改变 Team domain interface。 diff --git a/apps/web/tests/agent-team-panel.overlay.yml b/apps/web/tests/agent-team-panel.overlay.yml index 73c7b949c5..b08727e4cc 100644 --- a/apps/web/tests/agent-team-panel.overlay.yml +++ b/apps/web/tests/agent-team-panel.overlay.yml @@ -37,8 +37,5 @@ freshProvider: spawn forkProvider: fork - - id: agent-team-remotes - name: '@deepseek-ai/dsh-agent-team-remotes' - - id: ui-agent-team name: '@deepseek-ai/dsh-client-ui-agent-team' diff --git a/knip.json b/knip.json index 4dc0d3dcc5..334b633f73 100644 --- a/knip.json +++ b/knip.json @@ -756,18 +756,14 @@ "@deepseek-ai/.+" ] }, - "packages/experimental/agent-team-remotes": { + "packages/experimental/team": { "entry": [ "tests/**/*.spec.ts", "tests/**/*.e2e.ts" - ], - "ignoreDependencies": [ - "zod" ] }, "packages/experimental/agent-team-web-profile": { "ignoreDependencies": [ - "@deepseek-ai/dsh-agent-team-remotes", "@deepseek-ai/dsh-client-ui-agent-team" ] }, diff --git a/packages/experimental/README.md b/packages/experimental/README.md index 10a5017567..2812388377 100644 --- a/packages/experimental/README.md +++ b/packages/experimental/README.md @@ -26,7 +26,6 @@ The experimental group contains prototype capabilities that are not part of any |---|---|---| | [`agent-team-profile`](agent-team-profile/README.md) | Explicit source-checkout profile layer for Agent Teams | — | | [`agent-team`](agent-team/README.md) | Named teammates with durable messages and a shared task board | `ctx.agentTeams` | -| [`agent-team-remotes`](agent-team-remotes/README.md) | Client assembly for the generated Agent Teams Remote contribution | `ctx.remote.agentTeams` | | [`agent-team-web-profile`](agent-team-web-profile/README.md) | Explicit source-checkout Web layer for Agent Teams | — | | [`client-ui-agent-team`](client-ui-agent-team/README.md) | Team roster, task board, and teammate navigation for Web | — | | [`tool-agent-team`](tool-agent-team/README.md) | Ten tools that let the model create, message, and coordinate teammates | registers scoped tools on `ctx.tools` | diff --git a/packages/experimental/README.zh.md b/packages/experimental/README.zh.md index dbd1605996..3dcf13d9ef 100644 --- a/packages/experimental/README.zh.md +++ b/packages/experimental/README.zh.md @@ -26,7 +26,6 @@ kind: "package-group" |---|---|---| | [`agent-team-profile`](agent-team-profile/README.zh.md) | Agent Teams 的显式源码 checkout profile 层 | — | | [`agent-team`](agent-team/README.zh.md) | 具名 teammate,成员之间持久消息与共享任务板 | `ctx.agentTeams` | -| [`agent-team-remotes`](agent-team-remotes/README.zh.md) | 生成式 Agent Teams Remote contribution 的 Client assembly | `ctx.remote.agentTeams` | | [`agent-team-web-profile`](agent-team-web-profile/README.zh.md) | Agent Teams 的显式源码 checkout Web 层 | — | | [`client-ui-agent-team`](client-ui-agent-team/README.zh.md) | Web Team roster、任务板与 teammate 导航 | — | | [`tool-agent-team`](tool-agent-team/README.zh.md) | 让模型创建、发消息与协调 teammate 的十个工具 | 按作用域注册工具到 `ctx.tools` | diff --git a/packages/experimental/agent-team-remotes/README.i18n.yaml b/packages/experimental/agent-team-remotes/README.i18n.yaml deleted file mode 100644 index bd4614e5e6..0000000000 --- a/packages/experimental/agent-team-remotes/README.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# side as of the last confirmed-consistent state. Both languages carry equal authority; -# after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write packages/experimental/agent-team-remotes/README.md -README.md: 7b936c7a0ae713fe730630dcfa858a0b9272b146 -README.zh.md: baf9547fc032b6f8dc127e59ad7ca4c981087d97 diff --git a/packages/experimental/agent-team-remotes/README.md b/packages/experimental/agent-team-remotes/README.md deleted file mode 100644 index 7b936c7a0a..0000000000 --- a/packages/experimental/agent-team-remotes/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# @deepseek-ai/dsh-agent-team-remotes - -English | [中文](README.zh.md) - -Private browser Remote adapter for Agent Teams. Its Host service is registered as `ctx.teamRemote` and exports the separate `teams` wire namespace, so it cannot replace or widen the domain-owned `ctx.teams` service. The adapter delegates every read and mutation to `ctx.teams` and owns no roster, mailbox, task, or lifecycle state. - -The generated contribution exposes `teams/view`, `teams/createTask`, and `teams/updateTask`. Views omit mailbox contents and deleted task tombstones. Task conflicts cross Remote as a closed business result; other Team rejections remain distinguishable from carrier or Agent-lookup failures. `@deepseek-ai/dsh-api-remotes` supplies the stable Remote carrier and Agent identity policy. - -[`@deepseek-ai/dsh-client-ui-agent-team`](../client-ui-agent-team/README.md) mounts the generated Client contribution through `ctx.remote.$mount()`. [`@deepseek-ai/dsh-agent-team-web-profile`](../agent-team-web-profile/README.md) inserts this Host adapter before that UI. - -## Model Experience - -None, as this browser adapter delegates typed Remote methods and registers no model-facing input. - -#### KV Cache effect - -No direct effect; invoked Team methods and their model-facing consumers own any later effect. - -## Known Limitations and Deferred Work - -- **Fixed contribution set** — adding a browser operation requires changing this adapter and regenerating its Remote artifacts. -- **Source-checkout only** — this private package is excluded from official releases. diff --git a/packages/experimental/agent-team-remotes/README.zh.md b/packages/experimental/agent-team-remotes/README.zh.md deleted file mode 100644 index baf9547fc0..0000000000 --- a/packages/experimental/agent-team-remotes/README.zh.md +++ /dev/null @@ -1,22 +0,0 @@ -# @deepseek-ai/dsh-agent-team-remotes - -[English](README.md) | 中文 - -Agent Teams 的私有浏览器 Remote adapter。其 Host service 注册为 `ctx.teamRemote`,并导出独立的 `teams` wire namespace,因此不会替换或扩大 domain 所有的 `ctx.teams` service。Adapter 将所有读取与 mutation 委托给 `ctx.teams`,不持有 roster、mailbox、task 或 lifecycle state。 - -生成式 contribution 提供 `teams/view`、`teams/createTask` 与 `teams/updateTask`。View 不包含 mailbox 内容或已删除 task tombstone。Task conflict 通过封闭 business result 跨越 Remote;其他 Team rejection 与 carrier 或 Agent lookup failure 保持可区分。`@deepseek-ai/dsh-api-remotes` 提供稳定 Remote carrier 与 Agent identity policy。 - -[`@deepseek-ai/dsh-client-ui-agent-team`](../client-ui-agent-team/README.md) 通过 `ctx.remote.$mount()` 挂载生成式 Client contribution。[`@deepseek-ai/dsh-agent-team-web-profile`](../agent-team-web-profile/README.md) 会先于该 UI 插入此 Host adapter。 - -## 模型体验 - -无直接影响,因为该浏览器 adapter 只委托 typed Remote method,不注册面向模型的输入。 - -#### KV Cache 影响 - -无直接影响;被调用的 Team method 及其面向模型的 consumer 负责后续任何影响。 - -## 已知限制与暂缓事项 - -- **固定 contribution 集合**:增加浏览器 operation 时,需要修改该 adapter 并重新生成其 Remote artifact。 -- **仅限源码 checkout**:正式发布会排除这个私有包。 diff --git a/packages/experimental/agent-team-remotes/package.json b/packages/experimental/agent-team-remotes/package.json deleted file mode 100644 index 0709991289..0000000000 --- a/packages/experimental/agent-team-remotes/package.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "name": "@deepseek-ai/dsh-agent-team-remotes", - "description": "Private browser Remote adapter for the Agent Teams domain service", - "version": "0.1.0-rc.7", - "private": true, - "repository": { - "type": "git", - "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", - "directory": "packages/experimental/agent-team-remotes" - }, - "type": "module", - "main": "lib/index.js", - "types": "lib/types/index.d.ts", - "exports": { - ".": { - "types": "./lib/types/index.d.ts", - "default": "./lib/index.js" - }, - "./invariant": { - "types": "./lib/types/invariant.d.ts", - "default": "./lib/invariant.js" - }, - "./types": { - "types": "./lib/types/types.d.ts", - "default": "./lib/types/types.js" - }, - "./typert": { - "types": "./lib/typert.host.d.ts", - "default": "./lib/typert.host.js" - }, - "./remote": { - "types": "./lib/typert.remote-client.d.ts", - "default": "./lib/typert.remote-client.js" - }, - "./src/*": "./src/*", - "./package.json": "./package.json" - }, - "scripts": { - "bundle": "tsdown", - "watch": "tsdown --watch" - }, - "license": "MIT", - "dependencies": { - "zod": "^4.4.3" - }, - "peerDependencies": { - "@deepseek-ai/dsh-agent": "workspace:^", - "@deepseek-ai/dsh-invariants": "workspace:^", - "@deepseek-ai/dsh-team": "workspace:^", - "@deepseek-ai/dsh-typert-protocol": "workspace:^", - "@deepseek-ai/cordis": "workspace:^" - }, - "devDependencies": { - "@deepseek-ai/dsh-agent": "workspace:^", - "@deepseek-ai/dsh-invariants": "workspace:^", - "@deepseek-ai/dsh-team": "workspace:^", - "@deepseek-ai/dsh-typert-protocol": "workspace:^", - "@deepseek-ai/cordis": "workspace:^" - }, - "files": [ - "lib/index.js", - "lib/invariant.js", - "lib/types/**/*.js", - "lib/types/**/*.d.ts", - "lib/typert.host.js", - "lib/typert.host.d.ts", - "lib/typert.remote-client.js", - "lib/typert.remote-client.d.ts" - ] -} diff --git a/packages/experimental/agent-team-remotes/src/index.ts b/packages/experimental/agent-team-remotes/src/index.ts deleted file mode 100644 index 83005fe5d7..0000000000 --- a/packages/experimental/agent-team-remotes/src/index.ts +++ /dev/null @@ -1,98 +0,0 @@ -/** Browser-facing Remote adapter over the Agent Teams domain service. */ - -import type { Context } from '@deepseek-ai/cordis' -import type { Agent } from '@deepseek-ai/dsh-agent' -import { TeamError, TeamTaskId } from '@deepseek-ai/dsh-team' -import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol' -import type { - CreateTeamTaskRequest, - TeamTaskMutationResult, - TeamTaskView, - TeamView, - UpdateTeamTaskRequest, -} from './types.ts' - -export type * from './types.ts' - -declare module '@deepseek-ai/cordis' { - interface Context { - teamRemote: AgentTeamRemoteService - } -} - -/** Stateless browser projection and command adapter for `ctx.teams`. */ -export class AgentTeamRemoteService extends TypertRemoteService { - static inject = ['teams'] - - /** - * @param ctx - Host context carrying the Agent Teams domain service. - */ - constructor(ctx: Context) { - super(ctx, 'teamRemote', { namespace: 'teams' }) - } - - /** - * Read the current roster and non-deleted task board. - * @param agent - exact live Team member used as the authority credential. - * @returns detached current roster and task views. - */ - @Remote('view') - view(agent: Agent): TeamView { - return { - members: this.ctx.teams.listMembers(agent), - tasks: this.ctx.teams.listTasks(agent), - } - } - - /** - * Create one shared task. - * @param agent - exact live Team member creating the task. - * @param request - task text, blockers, and advisory write scopes. - * @returns the revision-one task view. - */ - @Remote('createTask') - createTask(agent: Agent, request: CreateTeamTaskRequest): Promise { - return this.ctx.teams.createTask(agent, { - subject: request.subject, - description: request.description, - ...(request.blockedBy === undefined ? {} : { blockedBy: request.blockedBy.map(TeamTaskId) }), - ...(request.writeScopes === undefined ? {} : { writeScopes: request.writeScopes }), - }) - } - - /** - * Apply one task mutation while preserving CAS conflicts as business results. - * @param agent - exact live Team member authorizing the mutation. - * @param request - task identity, expected revision, action, and action fields. - * @returns the committed task or a browser-safe Team rejection. - */ - @Remote('updateTask') - async updateTask(agent: Agent, request: UpdateTeamTaskRequest): Promise { - try { - return { - ok: true, - value: await this.ctx.teams.updateTask(agent, { - taskId: TeamTaskId(request.taskId), - expectedRevision: request.expectedRevision, - action: request.action, - ...(request.subject === undefined ? {} : { subject: request.subject }), - ...(request.description === undefined ? {} : { description: request.description }), - ...(request.blockedBy === undefined ? {} : { blockedBy: request.blockedBy.map(TeamTaskId) }), - ...(request.writeScopes === undefined ? {} : { writeScopes: request.writeScopes }), - ...(request.owner === undefined ? {} : { owner: request.owner }), - }), - } - } catch (error) { - if (!(error instanceof TeamError)) throw error - return { - ok: false, - error: { - code: error.code === 'TEAM_TASK_STALE_REVISION' ? 'team-task-conflict' : 'team-rejected', - message: error.message, - }, - } - } - } -} - -export default AgentTeamRemoteService diff --git a/packages/experimental/agent-team-remotes/src/invariant.ts b/packages/experimental/agent-team-remotes/src/invariant.ts deleted file mode 100644 index 560e5ff6a6..0000000000 --- a/packages/experimental/agent-team-remotes/src/invariant.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** Package-owned invariant companion for the Agent Teams Remote adapter. */ - -import type { Context } from '@deepseek-ai/cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' - -const PACKAGE_NAME = '@deepseek-ai/dsh-agent-team-remotes' - -/** Cordis companion plugin name. */ -export const name = 'agent-team-remotes-invariant' -/** Service required before the companion can register. */ -export const inject = ['invariants'] - -// No runtime invariant: generated codecs validate the adapter's requests and -// results, while the Team service owns every mutable relationship. -const install: InvariantInstaller = () => {} - -/** - * Register this package's invariant companion. - * @param ctx - Cordis context carrying the invariant service. - * @returns the installed registration's disposer after setup succeeds. - */ -export const apply = (ctx: Context): Promise<() => void> => - Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) diff --git a/packages/experimental/agent-team-remotes/src/types.ts b/packages/experimental/agent-team-remotes/src/types.ts deleted file mode 100644 index edef7e071b..0000000000 --- a/packages/experimental/agent-team-remotes/src/types.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** Browser-safe Agent Teams Remote values. */ - -/** Browser projection of one Team member. */ -export interface TeamMemberView { - readonly id: string - readonly name: string - readonly role: 'lead' | 'teammate' - readonly status: 'running' | 'idle' | 'inactive' | 'provisioning' | 'failed' - readonly description?: string - readonly provider?: string - readonly context?: 'fresh' | 'fork' - readonly model?: string - readonly diagnostics: string[] -} - -/** Browser task identifier serialized as a JSON string. */ -export type TeamTaskId = string - -/** Browser-visible task mutation actions. */ -export type TeamTaskAction = - | 'claim' - | 'release' - | 'edit' - | 'set_dependencies' - | 'complete' - | 'reopen' - | 'reassign' - | 'delete' - -/** Browser projection of one shared task. */ -export interface TeamTaskView { - readonly id: TeamTaskId - readonly revision: number - readonly subject: string - readonly description: string - readonly status: 'pending' | 'in_progress' | 'completed' | 'deleted' - readonly blockedBy: TeamTaskId[] - readonly writeScopes: string[] - readonly ownerName?: string - readonly ready: boolean - readonly writeScopeWarnings: string[] -} - -/** Browser request for creating one shared task. */ -export interface CreateTeamTaskRequest { - readonly subject: string - readonly description: string - readonly blockedBy?: readonly TeamTaskId[] - readonly writeScopes?: readonly string[] -} - -/** Browser request for one compare-and-set task mutation. */ -export interface UpdateTeamTaskRequest { - readonly taskId: TeamTaskId - readonly expectedRevision: number - readonly action: TeamTaskAction - readonly subject?: string - readonly description?: string - readonly blockedBy?: readonly TeamTaskId[] - readonly writeScopes?: readonly string[] - readonly owner?: string -} - -/** Point-in-time roster and task-board projection. */ -export interface TeamView { - readonly members: TeamMemberView[] - readonly tasks: TeamTaskView[] -} - -/** Task mutation result preserving stale-revision recovery across Remote. */ -export type TeamTaskMutationResult = - | { readonly ok: true; readonly value: TeamTaskView } - | { - readonly ok: false - readonly error: { - readonly code: 'team-task-conflict' | 'team-rejected' - readonly message: string - } - } diff --git a/packages/experimental/agent-team-remotes/tests/invariant.spec.ts b/packages/experimental/agent-team-remotes/tests/invariant.spec.ts deleted file mode 100644 index c8a8ea33ad..0000000000 --- a/packages/experimental/agent-team-remotes/tests/invariant.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** Package invariant registration for the Agent Teams Remote assembly. */ - -import { describe, expect, it, vi } from 'vitest' -import * as invariant from '../src/invariant.ts' - -describe('Agent Teams Remote invariant', () => { - it('reserves package ownership with an empty invariant installer', async () => { - const register = vi.fn().mockReturnValue(() => {}) - const ctx = { invariants: { register } } as never - - const dispose = await invariant.apply(ctx) - expect(register).toHaveBeenCalledWith('@deepseek-ai/dsh-agent-team-remotes', expect.any(Function)) - expect(() => { (register.mock.calls[0]![1] as () => void)() }).not.toThrow() - expect(dispose).toBeTypeOf('function') - }) -}) diff --git a/packages/experimental/agent-team-remotes/tests/remote-adapter.spec.ts b/packages/experimental/agent-team-remotes/tests/remote-adapter.spec.ts deleted file mode 100644 index 065dc2d85e..0000000000 --- a/packages/experimental/agent-team-remotes/tests/remote-adapter.spec.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Context } from '@deepseek-ai/cordis' -import { describe, expect, it, vi } from 'vitest' -import type { Agent } from '@deepseek-ai/dsh-agent' -import { TeamError, TeamTaskId, type TeamTaskView } from '@deepseek-ai/dsh-team' -import AgentTeamRemoteService from '../src/index.ts' - -const agent = { id: 'lead' } as Agent -const task: TeamTaskView = { - id: TeamTaskId('task-1'), - revision: 1, - subject: 'Task', - description: 'Description', - status: 'pending', - blockedBy: [], - writeScopes: [], - ready: true, - writeScopeWarnings: [], -} - -function bench() { - const ctx = new Context() - const teams = { - listMembers: vi.fn(() => [{ - id: agent.id, name: 'lead', role: 'lead' as const, status: 'idle' as const, diagnostics: [], - }]), - listTasks: vi.fn(() => [task]), - createTask: vi.fn(() => Promise.resolve(task)), - updateTask: vi.fn(() => Promise.resolve({ ...task, revision: 2 })), - } - ctx.provide('teams', teams as never) - const adapter = new AgentTeamRemoteService(ctx) - return { ctx, teams, adapter } -} - -describe('Agent Teams Remote adapter', () => { - it('owns only the browser adapter service and delegates state operations to ctx.teams', async () => { - const { ctx, teams, adapter } = bench() - expect(ctx.teamRemote).toBeInstanceOf(AgentTeamRemoteService) - expect(ctx.teamRemote).not.toBe(ctx.teams) - expect(ctx.teams).toMatchObject(teams) - expect(adapter.typertRemote).toMatchObject({ serviceKey: 'teamRemote', namespace: 'teams' }) - expect(adapter.view(agent)).toEqual({ - members: [{ id: agent.id, name: 'lead', role: 'lead', status: 'idle', diagnostics: [] }], - tasks: [task], - }) - expect(teams.listMembers).toHaveBeenCalledWith(agent) - expect(teams.listTasks).toHaveBeenCalledWith(agent) - await expect(adapter.createTask(agent, { - subject: 'Task', description: 'Description', - })).resolves.toEqual(task) - await expect(adapter.createTask(agent, { - subject: 'Blocked task', - description: 'Description', - blockedBy: ['task-0'], - writeScopes: ['src/team'], - })).resolves.toEqual(task) - expect(teams.createTask).toHaveBeenLastCalledWith(agent, { - subject: 'Blocked task', - description: 'Description', - blockedBy: ['task-0'], - writeScopes: ['src/team'], - }) - await expect(adapter.updateTask(agent, { - taskId: task.id, expectedRevision: 1, action: 'claim', - })).resolves.toMatchObject({ ok: true, value: { revision: 2 } }) - await expect(adapter.updateTask(agent, { - taskId: task.id, - expectedRevision: 2, - action: 'edit', - subject: 'Edited', - description: 'Edited description', - blockedBy: ['task-0'], - writeScopes: ['src/team'], - owner: 'worker', - })).resolves.toMatchObject({ ok: true, value: { revision: 2 } }) - expect(teams.updateTask).toHaveBeenLastCalledWith(agent, { - taskId: task.id, - expectedRevision: 2, - action: 'edit', - subject: 'Edited', - description: 'Edited description', - blockedBy: ['task-0'], - writeScopes: ['src/team'], - owner: 'worker', - }) - }) - - it('maps Team task failures and preserves unexpected rejections', async () => { - const { teams, adapter } = bench() - teams.updateTask - .mockRejectedValueOnce(new TeamError('stale', 'TEAM_TASK_STALE_REVISION')) - .mockRejectedValueOnce(new TeamError('denied', 'TEAM_TASK_FORBIDDEN')) - .mockRejectedValueOnce(new Error('unexpected mutation failure')) - const request = { taskId: task.id, expectedRevision: 1, action: 'delete' as const } - await expect(adapter.updateTask(agent, request)).resolves.toEqual({ - ok: false, error: { code: 'team-task-conflict', message: 'stale' }, - }) - await expect(adapter.updateTask(agent, request)).resolves.toEqual({ - ok: false, error: { code: 'team-rejected', message: 'denied' }, - }) - await expect(adapter.updateTask(agent, request)).rejects.toThrow('unexpected mutation failure') - }) -}) diff --git a/packages/experimental/agent-team-remotes/tsconfig.json b/packages/experimental/agent-team-remotes/tsconfig.json deleted file mode 100644 index 69152cb0d4..0000000000 --- a/packages/experimental/agent-team-remotes/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "lib/types" - }, - "include": ["src"], - "references": [ - { "path": "../../../vendor/cordis" }, - { "path": "../../core/agent" }, - { "path": "../team" }, - { "path": "../../typert/protocol" }, - { "path": "../../runtime-diagnostics/invariants" } - ] -} diff --git a/packages/experimental/agent-team-remotes/tsdown.config.ts b/packages/experimental/agent-team-remotes/tsdown.config.ts deleted file mode 100644 index e49647c648..0000000000 --- a/packages/experimental/agent-team-remotes/tsdown.config.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { defineConfig } from 'tsdown' - -/** Build the Host adapter and invariant companion as independent bundles. */ -export default defineConfig([ - { - entry: ['lib/types/index.js'], - outDir: 'lib', - format: ['esm'], - platform: 'node', - target: 'es2024', - fixedExtension: false, - dts: false, - clean: false, - }, - { - entry: ['lib/types/invariant.js'], - outDir: 'lib', - format: ['esm'], - platform: 'node', - target: 'es2024', - fixedExtension: false, - dts: false, - clean: false, - }, -]) diff --git a/packages/experimental/agent-team-web-profile/README.i18n.yaml b/packages/experimental/agent-team-web-profile/README.i18n.yaml index 1d4469984b..32b81b75cb 100644 --- a/packages/experimental/agent-team-web-profile/README.i18n.yaml +++ b/packages/experimental/agent-team-web-profile/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/experimental/agent-team-web-profile/README.md -README.md: 089ab4e29c978bb537f0be5b374a3b5d7c95e484 -README.zh.md: d9c3b2180dd6f2a039211ec8de1fb68cdfcf93b5 +README.md: 696be051ab754ae157109fb9d69ba11bd6d47f16 +README.zh.md: 07470c5fedbeee30a17b26d08709a2ac1f45ab89 diff --git a/packages/experimental/agent-team-web-profile/README.md b/packages/experimental/agent-team-web-profile/README.md index 089ab4e29c..696be051ab 100644 --- a/packages/experimental/agent-team-web-profile/README.md +++ b/packages/experimental/agent-team-web-profile/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Private Web profile layer for Agent Teams. Apply it after `@deepseek-ai/dsh-web-app` and [`@deepseek-ai/dsh-agent-team-profile`](../agent-team-profile/README.md). The patch inserts the experimental Host Remote adapter followed by the Team conversation-header UI; it does not modify the stable Web bundle. +Private Web profile layer for Agent Teams. Apply it after `@deepseek-ai/dsh-web-app` and [`@deepseek-ai/dsh-agent-team-profile`](../agent-team-profile/README.md). The patch inserts the Team conversation-header UI; it does not modify the stable Web bundle. From a source checkout, add both Agent Teams layers to an initialized Web profile: @@ -11,7 +11,7 @@ pnpm dsh plugin --profile web add ./packages/experimental/agent-team-profile pnpm dsh plugin --profile web add ./packages/experimental/agent-team-web-profile ``` -The Host profile supplies the Team domain and model tools. This Web layer supplies the browser adapter, generated Client Remote namespace, and presentation. Removing either experimental bundle leaves the stable base and Web composition unchanged. +The Host profile supplies the Team domain, generated Remote methods, and model tools. This Web layer mounts the generated Client Remote namespace and supplies the presentation. Removing either experimental bundle leaves the stable base and Web composition unchanged. ## Model Experience diff --git a/packages/experimental/agent-team-web-profile/README.zh.md b/packages/experimental/agent-team-web-profile/README.zh.md index d9c3b2180d..07470c5fed 100644 --- a/packages/experimental/agent-team-web-profile/README.zh.md +++ b/packages/experimental/agent-team-web-profile/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -Agent Teams 的私有 Web profile 层。应当在 `@deepseek-ai/dsh-web-app` 与 [`@deepseek-ai/dsh-agent-team-profile`](../agent-team-profile/README.md) 之后应用。本 patch 先插入实验性 Host Remote adapter,再插入 Team 会话页头 UI;它不修改稳定 Web bundle。 +Agent Teams 的私有 Web profile 层。应当在 `@deepseek-ai/dsh-web-app` 与 [`@deepseek-ai/dsh-agent-team-profile`](../agent-team-profile/README.md) 之后应用。本 patch 插入 Team 会话页头 UI;它不修改稳定 Web bundle。 在源码 checkout 中,将两个 Agent Teams 层添加到已初始化的 Web profile: @@ -11,7 +11,7 @@ pnpm dsh plugin --profile web add ./packages/experimental/agent-team-profile pnpm dsh plugin --profile web add ./packages/experimental/agent-team-web-profile ``` -Host profile 提供 Team domain 与模型工具。本 Web 层提供浏览器 adapter、生成式 Client Remote namespace 与呈现。移除任一实验性 bundle 后,稳定 base 与 Web composition 保持不变。 +Host profile 提供 Team domain、生成式 Remote method 与模型工具。本 Web 层挂载生成式 Client Remote namespace 并提供呈现。移除任一实验性 bundle 后,稳定 base 与 Web composition 保持不变。 ## 模型体验 diff --git a/packages/experimental/agent-team-web-profile/cordis.patch.yml b/packages/experimental/agent-team-web-profile/cordis.patch.yml index 14984af370..bde50d45a6 100644 --- a/packages/experimental/agent-team-web-profile/cordis.patch.yml +++ b/packages/experimental/agent-team-web-profile/cordis.patch.yml @@ -1,9 +1,6 @@ # Private Agent Teams Web layer. Apply after dsh-web-app and the host-side -# dsh-agent-team-profile so the browser mounts only when both seams are present. +# dsh-agent-team-profile so the browser mounts only when the Team service is present. - insert: - - id: agent-team-remotes - name: '@deepseek-ai/dsh-agent-team-remotes' - - id: ui-agent-team name: '@deepseek-ai/dsh-client-ui-agent-team' diff --git a/packages/experimental/agent-team-web-profile/package.json b/packages/experimental/agent-team-web-profile/package.json index 0fec9d1577..a793cbd3d0 100644 --- a/packages/experimental/agent-team-web-profile/package.json +++ b/packages/experimental/agent-team-web-profile/package.json @@ -37,7 +37,6 @@ } }, "dependencies": { - "@deepseek-ai/dsh-agent-team-remotes": "workspace:^", "@deepseek-ai/dsh-client-ui-agent-team": "workspace:^" }, "peerDependencies": { diff --git a/packages/experimental/agent-team-web-profile/tests/profile.spec.ts b/packages/experimental/agent-team-web-profile/tests/profile.spec.ts index f8efa92c14..e76d8c827c 100644 --- a/packages/experimental/agent-team-web-profile/tests/profile.spec.ts +++ b/packages/experimental/agent-team-web-profile/tests/profile.spec.ts @@ -8,7 +8,7 @@ import * as yaml from 'js-yaml' import { entryListSchema } from '@deepseek-ai/cordis-plugin-include' describe('Agent Teams Web profile bundle', () => { - it('declares a private parseable layer with the Remote assembly before the UI', () => { + it('declares a private parseable layer containing the Team UI', () => { const root = fileURLToPath(new URL('..', import.meta.url)) const manifest = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as { private?: boolean @@ -20,7 +20,6 @@ describe('Agent Teams Web profile bundle', () => { expect(manifest.publishConfig).toBeUndefined() expect(manifest.dsh?.bundle?.patch).toBe('./cordis.patch.yml') expect(manifest.dependencies).toEqual({ - '@deepseek-ai/dsh-agent-team-remotes': 'workspace:^', '@deepseek-ai/dsh-client-ui-agent-team': 'workspace:^', }) @@ -29,7 +28,6 @@ describe('Agent Teams Web profile bundle', () => { { schema: entryListSchema }, ) as { insert?: { id?: string; name?: string }[] }[] expect(parsed.flatMap(patch => patch.insert ?? [])).toEqual([ - { id: 'agent-team-remotes', name: '@deepseek-ai/dsh-agent-team-remotes' }, { id: 'ui-agent-team', name: '@deepseek-ai/dsh-client-ui-agent-team' }, ]) }) diff --git a/packages/experimental/agent-team/README.md b/packages/experimental/agent-team/README.md index e2f317c253..3f42646fc9 100644 --- a/packages/experimental/agent-team/README.md +++ b/packages/experimental/agent-team/README.md @@ -165,9 +165,9 @@ Read these pages when the package-level contract is not enough. They move from t -### Browser adapter +### Browser Remote -`TeamService` exposes only roster, mailbox, task, and lifecycle operations. The private [`@deepseek-ai/dsh-agent-team-remotes`](../agent-team-remotes/README.md) package owns browser projections, Remote methods, and transport error mapping. `./client` re-exports the domain request and view types that are safe in a browser compilation face. +`TeamService` owns the generated `teams/view`, `teams/createTask`, and `teams/updateTask` Remote methods beside the roster, mailbox, task, and lifecycle operations. The `./remote` export supplies the Client contribution mounted by the Web UI, while `./client` re-exports the request, view, and task-mutation result types that are safe in a browser compilation face. Typert retains transport failures in its outer `RemoteResult`; task conflicts and other Team rejections remain explicit domain results inside a successful transport response. ## Model Experience diff --git a/packages/experimental/agent-team/README.zh.md b/packages/experimental/agent-team/README.zh.md index 7d66ddbe99..33f7b6c3a5 100644 --- a/packages/experimental/agent-team/README.zh.md +++ b/packages/experimental/agent-team/README.zh.md @@ -165,9 +165,9 @@ dispose 会关闭准入、中止并等待已获准的创建与 mailbox dispatch -### 浏览器适配器 +### 浏览器 Remote -`TeamService` 只提供 roster、mailbox、task 与 lifecycle operation。私有 [`@deepseek-ai/dsh-agent-team-remotes`](../agent-team-remotes/README.md) 包负责浏览器 projection、Remote method 与 transport error mapping。`./client` 重新导出可在浏览器 compilation face 中安全使用的 domain request 与 view type。 +`TeamService` 除了 roster、mailbox、task 与 lifecycle operation,还直接负责生成式 `teams/view`、`teams/createTask` 与 `teams/updateTask` Remote method。`./remote` 导出由 Web UI 挂载的 Client contribution,`./client` 则重新导出可在浏览器 compilation face 中安全使用的 request、view 与 task mutation result type。Typert 在外层 `RemoteResult` 中保留 transport failure;task conflict 与其他 Team rejection 则作为 transport 成功响应中的显式 domain result。 ## 模型体验 diff --git a/packages/experimental/agent-team/package.json b/packages/experimental/agent-team/package.json index 962a84c625..1aa07e6c1e 100644 --- a/packages/experimental/agent-team/package.json +++ b/packages/experimental/agent-team/package.json @@ -24,6 +24,18 @@ "types": "./lib/types/client.d.ts", "default": "./lib/types/client.js" }, + "./types": { + "types": "./lib/types/types.d.ts", + "default": "./lib/types/types.js" + }, + "./typert": { + "types": "./lib/typert.host.d.ts", + "default": "./lib/typert.host.js" + }, + "./remote": { + "types": "./lib/typert.remote-client.d.ts", + "default": "./lib/typert.remote-client.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, @@ -31,7 +43,11 @@ "lib/index.js", "lib/invariant.js", "lib/types/**/*.js", - "lib/types/**/*.d.ts" + "lib/types/**/*.d.ts", + "lib/typert.host.js", + "lib/typert.host.d.ts", + "lib/typert.remote-client.js", + "lib/typert.remote-client.d.ts" ], "license": "MIT", "dependencies": { @@ -46,6 +62,7 @@ "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-persistence": "workspace:^", "@deepseek-ai/dsh-subagent": "workspace:^", + "@deepseek-ai/dsh-typert-protocol": "workspace:^", "@deepseek-ai/cordis": "workspace:^" }, "devDependencies": { @@ -63,6 +80,7 @@ "@deepseek-ai/dsh-subagent": "workspace:^", "@deepseek-ai/dsh-subagent-fork-in-process": "workspace:^", "@deepseek-ai/dsh-subagent-spawn-in-process": "workspace:^", + "@deepseek-ai/dsh-typert-protocol": "workspace:^", "@deepseek-ai/cordis": "workspace:^" } } diff --git a/packages/experimental/agent-team/src/index.ts b/packages/experimental/agent-team/src/index.ts index c66e2ce3f6..4faf9b37ac 100644 --- a/packages/experimental/agent-team/src/index.ts +++ b/packages/experimental/agent-team/src/index.ts @@ -1,9 +1,10 @@ /** Agent Teams service façade over roster, mailbox, task, and runtime lifecycle owners. */ -import { Context, Service } from '@deepseek-ai/cordis' +import { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import type { Agent } from '@deepseek-ai/dsh-agent' import type {} from '@deepseek-ai/dsh-session-persistence' +import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol' import { TeamActivity } from './activity.ts' import { errorMessage, TeamError } from './error.ts' import { TeamJournal } from './journal.ts' @@ -21,7 +22,9 @@ import type { SpawnTeammateRequest, SpawnTeammateResult, TeamMemberView, + TeamTaskMutationResult, TeamTaskView, + TeamView, TeamWaitResult, UpdateTeamTaskRequest, } from './types.ts' @@ -53,7 +56,7 @@ function positiveLimit(name: string, value: number): number { } /** Agent Teams service backed by the exact live Lead Session log. */ -export class TeamService extends Service { +export class TeamService extends TypertRemoteService { static inject = ['agents', 'sessions', 'sessionPersistence', 'subagents'] static Config: z = z.object({ @@ -222,6 +225,52 @@ export class TeamService extends Service { return this.roster.tryMembership(agent) } + /** + * Read the current roster and non-deleted task board through the generated Remote API. + * @param agent - exact live Team member used as the authority credential. + * @returns detached current roster and task views. + */ + @Remote('view') + remoteView(agent: Agent): TeamView { + return { + members: this.listMembers(agent), + tasks: this.listTasks(agent), + } + } + + /** + * Create one shared task through the generated Remote API. + * @param agent - exact live Team member creating the task. + * @param request - task text, blockers, and advisory write scopes. + * @returns the revision-one task view. + */ + @Remote('createTask') + remoteCreateTask(agent: Agent, request: CreateTeamTaskRequest): Promise { + return this.createTask(agent, request) + } + + /** + * Apply one task mutation and preserve Team rejections as business results. + * @param agent - exact live Team member authorizing the mutation. + * @param request - task identity, expected revision, action, and action fields. + * @returns the committed task or a typed Team rejection. + */ + @Remote('updateTask') + async remoteUpdateTask(agent: Agent, request: UpdateTeamTaskRequest): Promise { + try { + return { ok: true, value: await this.updateTask(agent, request) } + } catch (error) { + if (!(error instanceof TeamError)) throw error + return { + ok: false, + error: { + code: error.code === 'TEAM_TASK_STALE_REVISION' ? 'team-task-conflict' : 'team-rejected', + message: error.message, + }, + } + } + } + /** Queue one contained recovery pass after publication has unwound. */ private scheduleRecovery(agent: Agent): void { queueMicrotask(() => { diff --git a/packages/experimental/agent-team/src/types.ts b/packages/experimental/agent-team/src/types.ts index cee2b1064a..767e59a3e2 100644 --- a/packages/experimental/agent-team/src/types.ts +++ b/packages/experimental/agent-team/src/types.ts @@ -96,6 +96,12 @@ export interface TeamTaskView { readonly writeScopeWarnings: string[] } +/** Point-in-time roster and task-board projection returned to browser clients. */ +export interface TeamView { + readonly members: TeamMemberView[] + readonly tasks: TeamTaskView[] +} + /** One peer message retained until its target Session records it. */ export interface TeamMessageSnapshot { readonly id: TeamMessageId @@ -195,6 +201,17 @@ export interface UpdateTeamTaskRequest { readonly owner?: string } +/** Browser task mutation result with stale revisions kept distinct from other Team rejections. */ +export type TeamTaskMutationResult = + | { readonly ok: true; readonly value: TeamTaskView } + | { + readonly ok: false + readonly error: { + readonly code: 'team-task-conflict' | 'team-rejected' + readonly message: string + } + } + /** Result of waiting for Team activity. */ export interface TeamWaitResult { readonly timedOut: boolean diff --git a/packages/experimental/agent-team-remotes/tests/built-lib.e2e.ts b/packages/experimental/agent-team/tests/built-lib.e2e.ts similarity index 69% rename from packages/experimental/agent-team-remotes/tests/built-lib.e2e.ts rename to packages/experimental/agent-team/tests/built-lib.e2e.ts index f34dad730d..dfa9862181 100644 --- a/packages/experimental/agent-team-remotes/tests/built-lib.e2e.ts +++ b/packages/experimental/agent-team/tests/built-lib.e2e.ts @@ -1,4 +1,4 @@ -/** Plain-Node smoke for the built Agent Teams Remote adapter artifacts. */ +/** Plain-Node smoke for the built Agent Teams service and Remote contribution. */ import { execFile } from 'node:child_process' import { existsSync } from 'node:fs' @@ -12,15 +12,15 @@ const artifact = (path: string): string => join(root, path) const artifactUrl = (path: string): string => pathToFileURL(artifact(path)).href const requiredArtifacts = [ - 'packages/experimental/agent-team-remotes/lib/index.js', - 'packages/experimental/agent-team-remotes/lib/typert.remote-client.js', + 'packages/experimental/team/lib/index.js', + 'packages/experimental/team/lib/typert.remote-client.js', ].every(path => existsSync(artifact(path))) -describe.skipIf(!requiredArtifacts)('Agent Teams Remote built LIB adapter', () => { - it('loads the Host adapter and its generated browser contribution under plain Node', async () => { +describe.skipIf(!requiredArtifacts)('Agent Teams built LIB service', () => { + it('loads the Host service and its generated browser contribution under plain Node', async () => { const urls = { - host: artifactUrl('packages/experimental/agent-team-remotes/lib/index.js'), - remote: artifactUrl('packages/experimental/agent-team-remotes/lib/typert.remote-client.js'), + host: artifactUrl('packages/experimental/team/lib/index.js'), + remote: artifactUrl('packages/experimental/team/lib/typert.remote-client.js'), } const script = ` const host = await import(${JSON.stringify(urls.host)}) @@ -38,11 +38,11 @@ describe.skipIf(!requiredArtifacts)('Agent Teams Remote built LIB adapter', () = methods: string[] } expect(output).toEqual({ - className: 'AgentTeamRemoteService', + className: 'TeamService', methods: [ - '@deepseek-ai/dsh-agent-team-remotes#teams/createTask', - '@deepseek-ai/dsh-agent-team-remotes#teams/updateTask', - '@deepseek-ai/dsh-agent-team-remotes#teams/view', + '@deepseek-ai/dsh-team#teams/createTask', + '@deepseek-ai/dsh-team#teams/updateTask', + '@deepseek-ai/dsh-team#teams/view', ], }) }) diff --git a/packages/experimental/agent-team/tests/team.spec.ts b/packages/experimental/agent-team/tests/team.spec.ts index 9b606a3de3..4b0b5125b4 100644 --- a/packages/experimental/agent-team/tests/team.spec.ts +++ b/packages/experimental/agent-team/tests/team.spec.ts @@ -837,6 +837,52 @@ describe('Team shared task DAG', () => { }) }) +describe('Team Remote API', () => { + it('exports Team views and task mutations from the owning service', async () => { + const { ctx, lead } = await setup([]) + expect(ctx.teams.typertRemote).toMatchObject({ serviceKey: 'teams', namespace: 'teams' }) + expect(ctx.teams.remoteView(lead)).toEqual({ + members: [expect.objectContaining({ name: 'lead', role: 'lead', status: 'idle' })], + tasks: [], + }) + + const created = await ctx.teams.remoteCreateTask(lead, { + subject: 'Remote task', + description: 'Created through the generated API', + blockedBy: [], + writeScopes: ['packages/experimental/team'], + }) + await expect(ctx.teams.remoteUpdateTask(lead, { + taskId: created.id, + expectedRevision: created.revision, + action: 'claim', + })).resolves.toMatchObject({ + ok: true, + value: { id: created.id, revision: 2, ownerName: 'lead' }, + }) + expect(ctx.teams.remoteView(lead).tasks).toHaveLength(1) + }) + + it('preserves Team task rejections and propagates unexpected failures', async () => { + const { ctx, lead } = await setup([]) + const request = { taskId: TeamTaskId('task-1'), expectedRevision: 1, action: 'delete' as const } + vi.spyOn(ctx.teams, 'updateTask') + .mockRejectedValueOnce(new TeamError('stale', 'TEAM_TASK_STALE_REVISION')) + .mockRejectedValueOnce(new TeamError('denied', 'TEAM_TASK_FORBIDDEN')) + .mockRejectedValueOnce(new Error('unexpected mutation failure')) + + await expect(ctx.teams.remoteUpdateTask(lead, request)).resolves.toEqual({ + ok: false, + error: { code: 'team-task-conflict', message: 'stale' }, + }) + await expect(ctx.teams.remoteUpdateTask(lead, request)).resolves.toEqual({ + ok: false, + error: { code: 'team-rejected', message: 'denied' }, + }) + await expect(ctx.teams.remoteUpdateTask(lead, request)).rejects.toThrow('unexpected mutation failure') + }) +}) + describe('Team mailbox and waiting', () => { it('injects a quiet message addressed to the Lead and checkpoints its receipt', async () => { const { ctx, lead } = await setup([]) diff --git a/packages/experimental/agent-team/tsconfig.json b/packages/experimental/agent-team/tsconfig.json index d0defa8242..22d7ade886 100644 --- a/packages/experimental/agent-team/tsconfig.json +++ b/packages/experimental/agent-team/tsconfig.json @@ -13,6 +13,7 @@ { "path": "../../llm/llm" }, { "path": "../../core/session" }, { "path": "../../core/agent" }, + { "path": "../../typert/protocol" }, { "path": "../../subagent/subagent" }, { "path": "../../session/session-persistence" }, { "path": "../../runtime-diagnostics/invariants" } diff --git a/packages/experimental/client-ui-agent-team/README.i18n.yaml b/packages/experimental/client-ui-agent-team/README.i18n.yaml index 7b2f094b86..e5de3820ee 100644 --- a/packages/experimental/client-ui-agent-team/README.i18n.yaml +++ b/packages/experimental/client-ui-agent-team/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/experimental/client-ui-agent-team/README.md -README.md: f64b2878f58455173e9899f6ee007706e9ca1ff2 -README.zh.md: a25ce13978456a349a23af82d53f89a01f4cecdf +README.md: 7d26e38b7baca2234e1a0305aa2298b2718c0dc4 +README.zh.md: 2d4ac6b09a8a130d16fa8d18f5d9d8591fe260fb diff --git a/packages/experimental/client-ui-agent-team/README.md b/packages/experimental/client-ui-agent-team/README.md index f64b2878f5..7d26e38b7b 100644 --- a/packages/experimental/client-ui-agent-team/README.md +++ b/packages/experimental/client-ui-agent-team/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Private Web Agent Teams presentation. It contributes one conversation-header action containing the current roster and shared task board. The Client plugin mounts the generated `ctx.remote.teams` contribution supplied by [`@deepseek-ai/dsh-agent-team-remotes`](../agent-team-remotes/README.md); it does not extend the stable API Proxy or store authoritative Team state. +Private Web Agent Teams presentation. It contributes one conversation-header action containing the current roster and shared task board. The Client plugin mounts the generated `ctx.remote.teams` contribution from [`@deepseek-ai/dsh-team/remote`](../team/README.md); it does not extend the stable API Proxy or store authoritative Team state. Opening the panel calls `teams/view`. Roster rows show durable names, runtime status, model, and diagnostics. Selecting a healthy teammate refreshes the existing direct-child catalog and opens the ordinary `{ parentSessionId, childSessionId, mode: 'continuable' }` address. History and later human prompts continue through the stable addressed-subagent conversation path; this package adds no Team-specific field to that address. diff --git a/packages/experimental/client-ui-agent-team/README.zh.md b/packages/experimental/client-ui-agent-team/README.zh.md index a25ce13978..2d4ac6b09a 100644 --- a/packages/experimental/client-ui-agent-team/README.zh.md +++ b/packages/experimental/client-ui-agent-team/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -私有 Web Agent Teams 呈现包。它向会话页头提供一个包含当前 roster 与共享任务板的 action。Client plugin 挂载由 [`@deepseek-ai/dsh-agent-team-remotes`](../agent-team-remotes/README.md) 提供的生成式 `ctx.remote.teams` contribution;它不扩展稳定 API Proxy,也不存储权威 Team 状态。 +私有 Web Agent Teams 呈现包。它向会话页头提供一个包含当前 roster 与共享任务板的 action。Client plugin 挂载来自 [`@deepseek-ai/dsh-team/remote`](../team/README.md) 的生成式 `ctx.remote.teams` contribution;它不扩展稳定 API Proxy,也不存储权威 Team 状态。 打开 panel 会调用 `teams/view`。Roster row 展示持久 name、运行时 status、model 与 diagnostics。选择健康 teammate 时,系统刷新既有直接 child catalog,并打开普通的 `{ parentSessionId, childSessionId, mode: 'continuable' }` address。History 与后续人类 prompt 继续使用稳定 addressed-subagent 会话路径;本包不会向该 address 添加 Team 专用字段。 diff --git a/packages/experimental/client-ui-agent-team/package.json b/packages/experimental/client-ui-agent-team/package.json index fa3e7bc4ef..5898ef2c00 100644 --- a/packages/experimental/client-ui-agent-team/package.json +++ b/packages/experimental/client-ui-agent-team/package.json @@ -48,7 +48,6 @@ "react": "^18.2.0" }, "peerDependencies": { - "@deepseek-ai/dsh-agent-team-remotes": "workspace:^", "@deepseek-ai/dsh-api-remotes": "workspace:^", "@deepseek-ai/dsh-client-locale": "workspace:^", "@deepseek-ai/dsh-client-runtime": "workspace:^", @@ -56,11 +55,11 @@ "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", "@deepseek-ai/dsh-client-ui-slots": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/dsh-team": "workspace:^", "@deepseek-ai/dsh-typert-protocol": "workspace:^", "@deepseek-ai/cordis": "workspace:^" }, "devDependencies": { - "@deepseek-ai/dsh-agent-team-remotes": "workspace:^", "@deepseek-ai/dsh-api-remotes": "workspace:^", "@deepseek-ai/dsh-client-locale": "workspace:^", "@deepseek-ai/dsh-client-runtime": "workspace:^", @@ -69,6 +68,7 @@ "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", "@deepseek-ai/dsh-client-ui-slots": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/dsh-team": "workspace:^", "@deepseek-ai/dsh-typert-protocol": "workspace:^", "@testing-library/react": "^16.1.0", "@types/react": "~18.3.1", diff --git a/packages/experimental/client-ui-agent-team/src/client/TeamAction.tsx b/packages/experimental/client-ui-agent-team/src/client/TeamAction.tsx index 95c9c6ec9b..7ed1a57cac 100644 --- a/packages/experimental/client-ui-agent-team/src/client/TeamAction.tsx +++ b/packages/experimental/client-ui-agent-team/src/client/TeamAction.tsx @@ -4,9 +4,11 @@ import type { TeamMemberView as TeamRosterMember, TeamTaskAction, TeamTaskId, + TeamTaskMutationResult, TeamTaskView as TeamTask, TeamView, -} from '@deepseek-ai/dsh-agent-team-remotes/types' +} from '@deepseek-ai/dsh-team/client' +import type { RemoteFailure, RemoteResult } from '@deepseek-ai/dsh-typert-protocol' import { IconCheckOutline14, IconCloseOutline16, IconEditOutline16, IconPlusOutline16, IconRefreshOutline14, IconTrashOutline16, IconUserOutline16, StateDot, @@ -16,10 +18,11 @@ import type {} from '@deepseek-ai/dsh-client-ui-conversation/client' import type { TeamKey } from './locales.ts' import css from './TeamAction.module.css' -/** Settled Team UI action result, including the stale-revision discriminator. */ -export type TeamActionResult = - | { ok: true; value: T } - | { ok: false; error: string; conflict: boolean } +/** Generated Remote result consumed directly by the Team UI. */ +export type TeamActionResult = RemoteResult + +/** Generated Remote result whose business value preserves Team task rejections. */ +export type TeamTaskActionResult = RemoteResult /** Business actions injected by the browser plugin. */ export interface TeamActionInjected { @@ -39,7 +42,7 @@ export interface TeamActionInjected { blockedBy?: TeamTaskId[] writeScopes?: string[] owner?: string - }) => Promise> + }) => Promise openTeammate: (sessionId: SessionId, member: TeamRosterMember) => Promise } @@ -61,7 +64,11 @@ function items(value: string): string[] { } function taskIds(value: string): TeamTaskId[] { - return items(value) + return items(value) as TeamTaskId[] +} + +function failureText(error: Pick): string { + return `${error.message} (${error.code})` } function statusKey(status: TeamTask['status']): TeamKey { @@ -129,7 +136,7 @@ export function TeamAction({ setError(null) return true } else { - setError(result.error) + setError(failureText(result.error)) return false } }, [load, sessionId]) @@ -141,7 +148,7 @@ export function TeamAction({ const settleTask = useCallback(async ( taskId: string, - operation: Promise>, + operation: Promise, ): Promise => { const requestedSession = sessionId setPendingTask(taskId) @@ -149,24 +156,29 @@ export function TeamAction({ if (sessionRef.current !== requestedSession) return undefined setPendingTask(null) if (!result.ok) { - if (result.conflict) { + setError(failureText(result.error)) + return undefined + } + if (!result.value.ok) { + if (result.value.error.code === 'team-task-conflict') { const reloaded = await refresh() if (sessionRef.current !== requestedSession) return undefined if (reloaded) setError(t('conflict')) } else { - setError(result.error) + setError(failureText(result.value.error)) } return undefined } + const task = result.value.value invalidateRefresh() setError(null) setView(current => loadedView(current, loaded => ({ ...loaded, - tasks: result.value.status === 'deleted' - ? loaded.tasks.filter(task => task.id !== result.value.id) - : replaceTask(loaded.tasks, result.value), + tasks: task.status === 'deleted' + ? loaded.tasks.filter(candidate => candidate.id !== task.id) + : replaceTask(loaded.tasks, task), }))) - return result.value + return task }, [invalidateRefresh, refresh, sessionId, t]) const submitCreate = async (): Promise => { @@ -185,7 +197,7 @@ export function TeamAction({ if (sessionRef.current !== requestedSession) return setPendingTask(null) if (!result.ok) { - setError(result.error) + setError(failureText(result.error)) return } invalidateRefresh() @@ -235,19 +247,24 @@ export function TeamAction({ if (sessionRef.current !== requestedSession) return setPendingTask(null) if (!dependencyResult.ok) { - if (dependencyResult.conflict) { + setError(failureText(dependencyResult.error)) + return + } + if (!dependencyResult.value.ok) { + if (dependencyResult.value.error.code === 'team-task-conflict') { const reloaded = await refresh() if (sessionRef.current !== requestedSession) return if (reloaded) setError(t('conflict')) } else { - setError(dependencyResult.error) + setError(failureText(dependencyResult.value.error)) } return } + const dependencyTask = dependencyResult.value.value invalidateRefresh() setView(current => loadedView(current, loaded => ({ ...loaded, - tasks: replaceTask(loaded.tasks, dependencyResult.value), + tasks: replaceTask(loaded.tasks, dependencyTask), }))) setEditing(null) } diff --git a/packages/experimental/client-ui-agent-team/src/client/index.ts b/packages/experimental/client-ui-agent-team/src/client/index.ts index 4e28bf4645..10c09bb255 100644 --- a/packages/experimental/client-ui-agent-team/src/client/index.ts +++ b/packages/experimental/client-ui-agent-team/src/client/index.ts @@ -1,18 +1,16 @@ /** Browser plugin for the Agent Teams roster, task board, and Team-routed teammate navigation. */ -import teamsRemote from '@deepseek-ai/dsh-agent-team-remotes/remote' +import teamsRemote from '@deepseek-ai/dsh-team/remote' import type { TeamMemberView as TeamRosterMember, - TeamTaskMutationResult, TeamTaskView as TeamTask, TeamView, -} from '@deepseek-ai/dsh-agent-team-remotes/types' -import type {} from '@deepseek-ai/dsh-agent-team-remotes/remote' +} from '@deepseek-ai/dsh-team/client' +import type {} from '@deepseek-ai/dsh-team/remote' import type {} from '@deepseek-ai/dsh-api-remotes/client' import type { ClientContext, SessionId } from '@deepseek-ai/dsh-client-runtime/client' import type {} from '@deepseek-ai/dsh-client-ui-conversation/client' import type {} from '@deepseek-ai/dsh-client-locale/client' -import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol' import { TeamAction, type TeamActionInjected, type TeamActionResult } from './TeamAction.tsx' import { en, zh, type TeamKey } from './locales.ts' @@ -29,25 +27,6 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { /** Required browser services for RPC, navigation, slots, and localized copy. */ export const inject = ['sessions', 'remote', 'slots', 'locale'] -function settle(result: RemoteResult): TeamActionResult { - if (result.ok) return { ok: true, value: result.value } - return { - ok: false, - error: `${result.error.message} (${result.error.code})`, - conflict: result.error.code === 'team-task-conflict', - } -} - -function settleMutation(result: RemoteResult): TeamActionResult { - if (!result.ok) return settle(result) - if (result.value.ok) return { ok: true, value: result.value.value } - return { - ok: false, - error: `${result.value.error.message} (${result.value.error.code})`, - conflict: result.value.error.code === 'team-task-conflict', - } -} - function registerUi(ctx: ClientContext): void { ctx.effect(() => ctx.locale.register('team', { zh, en }), 'ui-team: dictionaries') const sessions = ctx.sessions @@ -58,17 +37,17 @@ function registerUi(ctx: ClientContext): void { const actions: TeamActionInjected = { async load(sessionId): Promise> { - return settle(await ctx.remote.teams.view(leadSessionId(sessionId))) + return await ctx.remote.teams.view(leadSessionId(sessionId)) }, async createTask(sessionId, input): Promise> { - return settle(await ctx.remote.teams.createTask(leadSessionId(sessionId), input)) + return await ctx.remote.teams.createTask(leadSessionId(sessionId), input) }, - async updateTask(sessionId, input): Promise> { + async updateTask(sessionId, input) { const { owner, ...rest } = input - return settleMutation(await ctx.remote.teams.updateTask(leadSessionId(sessionId), { + return await ctx.remote.teams.updateTask(leadSessionId(sessionId), { ...rest, ...owner === undefined ? {} : { owner }, - })) + }) }, async openTeammate(sessionId: SessionId, member: TeamRosterMember): Promise { if (member.role !== 'teammate') return @@ -77,7 +56,7 @@ function registerUi(ctx: ClientContext): void { if (sessions.list.getSnapshot().current !== sessionId) return sessions.openSubagent({ parentSessionId, - childSessionId: member.id as SessionId, + childSessionId: member.id, mode: 'continuable', }) }, diff --git a/packages/experimental/client-ui-agent-team/tests/browser-plugin.client.spec.ts b/packages/experimental/client-ui-agent-team/tests/browser-plugin.client.spec.ts index 0e40504b75..ebfec797fe 100644 --- a/packages/experimental/client-ui-agent-team/tests/browser-plugin.client.spec.ts +++ b/packages/experimental/client-ui-agent-team/tests/browser-plugin.client.spec.ts @@ -3,14 +3,15 @@ import { describe, expect, it, vi } from 'vitest' import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client' import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client' import { SlotRegistry } from '@deepseek-ai/dsh-client-runtime/client' -import type { TeamMemberView as TeamRosterMember } from '@deepseek-ai/dsh-agent-team-remotes/types' -import type {} from '@deepseek-ai/dsh-agent-team-remotes/remote' +import type { TeamMemberView as TeamRosterMember, TeamTaskId } from '@deepseek-ai/dsh-team/client' +import type {} from '@deepseek-ai/dsh-team/remote' import { TeamAction, type TeamActionInjected } from '../src/client/TeamAction.tsx' import { apply, inject } from '../src/client/index.ts' import { apply as nodeApply } from '../src/index.ts' const SESSION = 'team-session' as SessionId const CHILD = 'team-child' as SessionId +const TASK_ID = 'task-1' as TeamTaskId async function bench(options: { addressed?: boolean @@ -149,10 +150,10 @@ describe('ui-team browser plugin', () => { subject: 'Task', description: 'Description', blockedBy: [], writeScopes: [], })).ok).toBe(true) expect((await actions.updateTask(SESSION, { - taskId: 'task-1', expectedRevision: 1, action: 'complete', + taskId: TASK_ID, expectedRevision: 1, action: 'complete', })).ok).toBe(true) expect((await actions.updateTask(SESSION, { - taskId: 'task-1', expectedRevision: 2, action: 'reassign', owner: 'worker', + taskId: TASK_ID, expectedRevision: 2, action: 'reassign', owner: 'worker', })).ok).toBe(true) expect(b.calls.map(call => call.method)).toEqual([ 'teams/view', 'teams/createTask', 'teams/updateTask', 'teams/updateTask', @@ -180,35 +181,35 @@ describe('ui-team browser plugin', () => { expect(b.remote.disposeMount).toHaveBeenCalledOnce() }) - it('marks stale revisions as conflicts and retains non-conflict errors as ordinary failures', async () => { + it('returns the generated task business result without a Client transport wrapper', async () => { const b = await bench({ conflict: true }) const actions = (b.entry()!.inject as unknown as () => TeamActionInjected)() await expect(actions.updateTask(SESSION, { - taskId: 'task-1', expectedRevision: 1, action: 'delete', + taskId: TASK_ID, expectedRevision: 1, action: 'delete', })).resolves.toEqual({ - ok: false, - error: 'stale (team-task-conflict)', - conflict: true, + ok: true, + value: { + ok: false, + error: { code: 'team-task-conflict', message: 'stale' }, + }, }) }) - it('surfaces Remote carrier failures without classifying them as task conflicts', async () => { + it('returns Remote carrier failures unchanged', async () => { const view = await bench({ remoteFailure: 'view' }) const viewActions = (view.entry()!.inject as unknown as () => TeamActionInjected)() await expect(viewActions.load(SESSION)).resolves.toEqual({ ok: false, - error: 'offline (internal)', - conflict: false, + error: { code: 'internal', message: 'offline', details: {} }, }) const update = await bench({ remoteFailure: 'update' }) const updateActions = (update.entry()!.inject as unknown as () => TeamActionInjected)() await expect(updateActions.updateTask(SESSION, { - taskId: 'task-1', expectedRevision: 1, action: 'delete', + taskId: TASK_ID, expectedRevision: 1, action: 'delete', })).resolves.toEqual({ ok: false, - error: 'offline (internal)', - conflict: false, + error: { code: 'internal', message: 'offline', details: {} }, }) }) diff --git a/packages/experimental/client-ui-agent-team/tests/team-action.client.spec.tsx b/packages/experimental/client-ui-agent-team/tests/team-action.client.spec.tsx index ecee0d8fe3..8351578df4 100644 --- a/packages/experimental/client-ui-agent-team/tests/team-action.client.spec.tsx +++ b/packages/experimental/client-ui-agent-team/tests/team-action.client.spec.tsx @@ -3,20 +3,22 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client' -import type { TeamView } from '@deepseek-ai/dsh-agent-team-remotes/types' -import type { TeamTaskView as TeamTask } from '@deepseek-ai/dsh-agent-team-remotes/types' +import type { TeamTaskId, TeamTaskView as TeamTask, TeamView } from '@deepseek-ai/dsh-team/client' import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts' import { TeamAction, type TeamActionInjected, type TeamActionProps, type TeamActionResult, + type TeamTaskActionResult, } from '../src/client/TeamAction.tsx' import { zh } from '../src/client/locales.ts' afterEach(cleanup) const SESSION = 'lead' as SessionId +const TASK_1 = 'task-1' as TeamTaskId +const TASK_2 = 'task-2' as TeamTaskId const task: TeamTask = { - id: 'task-1', + id: TASK_1, revision: 1, subject: 'Implement runtime', description: 'Build the Team runtime', @@ -31,7 +33,7 @@ const view: TeamView = { members: [ { id: SESSION, name: 'lead', role: 'lead', status: 'idle', model: 'model-a', diagnostics: [] }, { - id: 'worker-id', + id: 'worker-id' as SessionId, name: 'worker', role: 'teammate', status: 'inactive', @@ -42,6 +44,28 @@ const view: TeamView = { tasks: [task], } +function taskSuccess(value: TeamTask): TeamTaskActionResult { + return { ok: true, value: { ok: true, value } } +} + +function taskConflict(message: string): TeamTaskActionResult { + return { + ok: true, + value: { ok: false, error: { code: 'team-task-conflict', message } }, + } +} + +function taskRejected(message: string): TeamTaskActionResult { + return { + ok: true, + value: { ok: false, error: { code: 'team-rejected', message } }, + } +} + +function remoteFailure(message: string): { ok: false; error: { code: 'internal'; message: string; details: {} } } { + return { ok: false, error: { code: 'internal', message, details: {} } } +} + function props(actions: TeamActionInjected, sessionId: SessionId = SESSION): TeamActionProps { return { sessionId, @@ -53,8 +77,11 @@ function props(actions: TeamActionInjected, sessionId: SessionId = SESSION): Tea function actions(overrides: Partial = {}): TeamActionInjected { return { load: () => Promise.resolve({ ok: true, value: view }), - createTask: () => Promise.resolve({ ok: true, value: { ...task, id: 'task-2', subject: 'New task' } }), - updateTask: () => Promise.resolve({ ok: true, value: { ...task, revision: 2 } }), + createTask: () => Promise.resolve({ ok: true, value: { ...task, id: TASK_2, subject: 'New task' } }), + updateTask: () => Promise.resolve({ + ok: true, + value: { ok: true, value: { ...task, revision: 2 } }, + }), openTeammate: () => Promise.resolve(), ...overrides, } @@ -67,7 +94,7 @@ describe('TeamAction', () => { const nextView: TeamView = { ...view, members: [{ id: nextSession, name: 'lead', role: 'lead', status: 'idle', diagnostics: [] }], - tasks: [{ ...task, id: 'task-next', subject: 'Next session task' }], + tasks: [{ ...task, id: 'task-next' as TeamTaskId, subject: 'Next session task' }], } const load = vi.fn((sessionId: SessionId) => sessionId === SESSION ? firstLoad.promise @@ -104,7 +131,7 @@ describe('TeamAction', () => { const newer = Promise.withResolvers>() const newestView = { ...view, - tasks: [{ ...task, id: 'newest-task', subject: 'Newest task' }], + tasks: [{ ...task, id: 'newest-task' as TeamTaskId, subject: 'Newest task' }], } const load = vi.fn() .mockResolvedValueOnce({ ok: true, value: view }) @@ -131,10 +158,9 @@ describe('TeamAction', () => { const load = vi.fn() .mockResolvedValueOnce({ ok: true, value: view }) .mockImplementationOnce(() => stale.promise) - const updateTask = vi.fn(() => Promise.resolve({ - ok: true as const, - value: { ...task, revision: 2, status: 'completed' as const }, - })) + const updateTask = vi.fn(() => Promise.resolve( + taskSuccess({ ...task, revision: 2, status: 'completed' }), + )) render() fireEvent.click(screen.getByRole('button', { name: /Agent Team/u })) await screen.findByText('Implement runtime') @@ -233,7 +259,7 @@ describe('TeamAction', () => { default: throw new Error(`unexpected action ${input.action}`) } - return Promise.resolve({ ok: true as const, value: current }) + return Promise.resolve(taskSuccess(current)) }) render() fireEvent.click(screen.getByRole('button', { name: /Agent Team/u })) @@ -283,11 +309,7 @@ describe('TeamAction', () => { const load = vi.fn() .mockResolvedValueOnce({ ok: true, value: view }) .mockResolvedValueOnce({ ok: true, value: { ...view, tasks: [{ ...task, revision: 2 }] } }) - const updateTask = vi.fn(() => Promise.resolve({ - ok: false as const, - error: 'stale (team-task-conflict)', - conflict: true, - })) + const updateTask = vi.fn(() => Promise.resolve(taskConflict('stale'))) render() fireEvent.click(screen.getByRole('button', { name: /Agent Team/u })) await screen.findByText('Implement runtime') @@ -300,24 +322,24 @@ describe('TeamAction', () => { it('keeps reload failures visible after task and dependency conflicts', async () => { const taskLoad = vi.fn() .mockResolvedValueOnce({ ok: true, value: view }) - .mockResolvedValueOnce({ ok: false, error: 'task reload failed', conflict: false }) + .mockResolvedValueOnce(remoteFailure('task reload failed')) const first = render( Promise.resolve({ ok: false, error: 'stale task', conflict: true }), + updateTask: () => Promise.resolve(taskConflict('stale task')), }))} />) fireEvent.click(screen.getByRole('button', { name: /Agent Team/u })) await screen.findByText('Implement runtime') fireEvent.click(screen.getByRole('button', { name: /完成/u })) - expect(await screen.findByText('task reload failed')).toBeTruthy() + expect(await screen.findByText('task reload failed (internal)')).toBeTruthy() expect(screen.queryByText(zh.conflict)).toBeNull() first.unmount() const dependencyLoad = vi.fn() .mockResolvedValueOnce({ ok: true, value: view }) - .mockResolvedValueOnce({ ok: false, error: 'dependency reload failed', conflict: false }) + .mockResolvedValueOnce(remoteFailure('dependency reload failed')) const dependencyUpdate = vi.fn() - .mockResolvedValueOnce({ ok: true, value: { ...task, revision: 2, subject: 'Edited' } }) - .mockResolvedValueOnce({ ok: false, error: 'stale dependency', conflict: true }) + .mockResolvedValueOnce(taskSuccess({ ...task, revision: 2, subject: 'Edited' })) + .mockResolvedValueOnce(taskConflict('stale dependency')) render() fireEvent.click(screen.getByRole('button', { name: /Agent Team/u })) await screen.findByText('Implement runtime') @@ -325,7 +347,7 @@ describe('TeamAction', () => { fireEvent.change(screen.getByPlaceholderText('任务标题'), { target: { value: 'Edited' } }) fireEvent.change(screen.getByPlaceholderText(zh.blockers), { target: { value: 'task-2' } }) fireEvent.click(screen.getByRole('button', { name: '保存' })) - expect(await screen.findByText('dependency reload failed')).toBeTruthy() + expect(await screen.findByText('dependency reload failed (internal)')).toBeTruthy() expect(screen.queryByText(zh.conflict)).toBeNull() }) @@ -337,14 +359,14 @@ describe('TeamAction', () => { view.members[0]!, { ...view.members[1]!, status: 'running' }, { - id: 'failed-id', + id: 'failed-id' as SessionId, name: 'failed-worker', role: 'teammate', status: 'failed', diagnostics: ['provider failed'], }, { - id: 'provisioning-id', + id: 'provisioning-id' as SessionId, name: 'provisioning-worker', role: 'teammate', status: 'provisioning', @@ -352,9 +374,9 @@ describe('TeamAction', () => { }, ], tasks: [ - { ...unownedTask, id: 'ready-task', status: 'pending', ready: true }, - { ...unownedTask, id: 'blocked-task', status: 'pending', ready: false }, - { ...task, id: 'completed-task', status: 'completed' }, + { ...unownedTask, id: 'ready-task' as TeamTaskId, status: 'pending', ready: true }, + { ...unownedTask, id: 'blocked-task' as TeamTaskId, status: 'pending', ready: false }, + { ...task, id: 'completed-task' as TeamTaskId, status: 'completed' }, ], } const load = vi.fn(() => Promise.resolve({ ok: true as const, value: richView })) @@ -381,16 +403,14 @@ describe('TeamAction', () => { it('shows load and create failures and ignores a create result after a session switch', async () => { const failedLoad = actions({ - load: () => Promise.resolve({ ok: false, error: 'load failed', conflict: false }), + load: () => Promise.resolve(remoteFailure('load failed')), }) const first = render() fireEvent.click(screen.getByRole('button', { name: /Agent Team/u })) - expect(await screen.findByText('load failed')).toBeTruthy() + expect(await screen.findByText('load failed (internal)')).toBeTruthy() first.unmount() - const createTask = vi.fn(() => Promise.resolve({ - ok: false as const, error: 'create failed', conflict: false, - })) + const createTask = vi.fn(() => Promise.resolve(remoteFailure('create failed'))) const second = render() fireEvent.click(screen.getByRole('button', { name: /Agent Team/u })) await screen.findByText('Implement runtime') @@ -398,7 +418,7 @@ describe('TeamAction', () => { fireEvent.change(screen.getByPlaceholderText('任务标题'), { target: { value: 'Task' } }) fireEvent.change(screen.getByPlaceholderText('任务描述'), { target: { value: 'Description' } }) fireEvent.click(screen.getByRole('button', { name: '保存' })) - expect(await screen.findByText('create failed')).toBeTruthy() + expect(await screen.findByText('create failed (internal)')).toBeTruthy() second.unmount() const pending = Promise.withResolvers>() @@ -410,30 +430,30 @@ describe('TeamAction', () => { fireEvent.change(screen.getByPlaceholderText('任务描述'), { target: { value: 'Late description' } }) fireEvent.click(screen.getByRole('button', { name: '保存' })) third.rerender() - pending.resolve({ ok: true, value: { ...task, id: 'late-task' } }) + pending.resolve({ ok: true, value: { ...task, id: 'late-task' as TeamTaskId } }) await Promise.resolve() expect(screen.queryByText('Late task')).toBeNull() }) it('contains stale-session and ordinary task failures without retrying', async () => { - const pending = Promise.withResolvers>() + const pending = Promise.withResolvers() const rendered = render( pending.promise }))} />) fireEvent.click(screen.getByRole('button', { name: /Agent Team/u })) await screen.findByText('Implement runtime') fireEvent.click(screen.getByRole('button', { name: /完成/u })) rendered.rerender() - pending.resolve({ ok: true, value: { ...task, revision: 2, status: 'completed' } }) + pending.resolve(taskSuccess({ ...task, revision: 2, status: 'completed' })) await Promise.resolve() expect(screen.queryByText('Implement runtime')).toBeNull() rendered.unmount() render( Promise.resolve({ ok: false, error: 'update failed', conflict: false }), + updateTask: () => Promise.resolve(taskRejected('update failed')), }))} />) fireEvent.click(screen.getByRole('button', { name: /Agent Team/u })) await screen.findByText('Implement runtime') fireEvent.click(screen.getByRole('button', { name: /完成/u })) - expect(await screen.findByText('update failed')).toBeTruthy() + expect(await screen.findByText('update failed (team-rejected)')).toBeTruthy() }) it('does not publish a task conflict after its reload switches sessions', async () => { @@ -443,7 +463,7 @@ describe('TeamAction', () => { .mockImplementationOnce(() => reload.promise) const rendered = render( Promise.resolve({ ok: false, error: 'stale task', conflict: true }), + updateTask: () => Promise.resolve(taskConflict('stale task')), }))} />) fireEvent.click(screen.getByRole('button', { name: /Agent Team/u })) await screen.findByText('Implement runtime') @@ -458,11 +478,12 @@ describe('TeamAction', () => { }) it('contains edit and dependency failures and supports form cancellation and unassignment', async () => { + const { ownerName: _ownerName, ...unownedTask } = task const updateTask = vi.fn() - .mockResolvedValueOnce({ ok: false, error: 'edit failed', conflict: false }) - .mockResolvedValueOnce({ ok: true, value: { ...task, revision: 2, subject: 'Saved edit' } }) - .mockResolvedValueOnce({ ok: false, error: 'dependency failed', conflict: false }) - .mockResolvedValueOnce({ ok: true, value: { ...task, revision: 2, ownerName: undefined } }) + .mockResolvedValueOnce(remoteFailure('edit failed')) + .mockResolvedValueOnce(taskSuccess({ ...task, revision: 2, subject: 'Saved edit' })) + .mockResolvedValueOnce(taskRejected('dependency failed')) + .mockResolvedValueOnce(taskSuccess({ ...unownedTask, revision: 2 })) render() fireEvent.click(screen.getByRole('button', { name: /Agent Team/u })) await screen.findByText('Implement runtime') @@ -476,12 +497,12 @@ describe('TeamAction', () => { expect(screen.queryByRole('button', { name: '保存' })).toBeNull() fireEvent.click(screen.getByRole('button', { name: /编辑/u })) fireEvent.click(screen.getByRole('button', { name: '保存' })) - expect(await screen.findByText('edit failed')).toBeTruthy() + expect(await screen.findByText('edit failed (internal)')).toBeTruthy() fireEvent.change(screen.getByPlaceholderText('任务标题'), { target: { value: 'Saved edit' } }) fireEvent.change(screen.getByPlaceholderText(zh.blockers), { target: { value: 'task-2' } }) fireEvent.click(screen.getByRole('button', { name: '保存' })) - expect(await screen.findByText('dependency failed')).toBeTruthy() + expect(await screen.findByText('dependency failed (team-rejected)')).toBeTruthy() expect(updateTask.mock.calls[2]?.[1]).toMatchObject({ action: 'set_dependencies', expectedRevision: 2, @@ -498,12 +519,26 @@ describe('TeamAction', () => { }) }) + it('shows a Remote carrier failure from the dependency mutation', async () => { + const updateTask = vi.fn() + .mockResolvedValueOnce(taskSuccess({ ...task, revision: 2, subject: 'Edited' })) + .mockResolvedValueOnce(remoteFailure('dependency transport failed')) + render() + fireEvent.click(screen.getByRole('button', { name: /Agent Team/u })) + await screen.findByText('Implement runtime') + fireEvent.click(screen.getByRole('button', { name: /编辑/u })) + fireEvent.change(screen.getByPlaceholderText('任务标题'), { target: { value: 'Edited' } }) + fireEvent.change(screen.getByPlaceholderText(zh.blockers), { target: { value: 'task-2' } }) + fireEvent.click(screen.getByRole('button', { name: '保存' })) + + expect(await screen.findByText('dependency transport failed (internal)')).toBeTruthy() + }) + it('skips the dependency mutation when an edit keeps the same blockers', async () => { - const blockedTask: TeamTask = { ...task, blockedBy: ['task-0'] } - const updateTask = vi.fn().mockResolvedValue({ - ok: true, - value: { ...blockedTask, revision: 2, subject: 'Same dependencies' }, - }) + const blockedTask: TeamTask = { ...task, blockedBy: ['task-0' as TeamTaskId] } + const updateTask = vi.fn().mockResolvedValue( + taskSuccess({ ...blockedTask, revision: 2, subject: 'Same dependencies' }), + ) render( Promise.resolve({ ok: true, value: { ...view, tasks: [blockedTask] } }), updateTask, @@ -524,8 +559,8 @@ describe('TeamAction', () => { .mockResolvedValueOnce({ ok: true, value: view }) .mockResolvedValueOnce({ ok: true, value: { ...view, tasks: [{ ...task, revision: 3 }] } }) const conflictUpdate = vi.fn() - .mockResolvedValueOnce({ ok: true, value: { ...task, revision: 2, subject: 'Conflict edit' } }) - .mockResolvedValueOnce({ ok: false, error: 'stale dependency', conflict: true }) + .mockResolvedValueOnce(taskSuccess({ ...task, revision: 2, subject: 'Conflict edit' })) + .mockResolvedValueOnce(taskConflict('stale dependency')) const first = render() fireEvent.click(screen.getByRole('button', { name: /Agent Team/u })) await screen.findByText('Implement runtime') @@ -542,8 +577,8 @@ describe('TeamAction', () => { .mockResolvedValueOnce({ ok: true, value: view }) .mockImplementationOnce(() => dependencyReload.promise) const staleUpdate = vi.fn() - .mockResolvedValueOnce({ ok: true, value: { ...task, revision: 2, subject: 'Late edit' } }) - .mockResolvedValueOnce({ ok: false, error: 'stale dependency', conflict: true }) + .mockResolvedValueOnce(taskSuccess({ ...task, revision: 2, subject: 'Late edit' })) + .mockResolvedValueOnce(taskConflict('stale dependency')) const second = render() fireEvent.click(screen.getByRole('button', { name: /Agent Team/u })) await screen.findByText('Implement runtime') @@ -559,9 +594,9 @@ describe('TeamAction', () => { expect(screen.queryByText(zh.conflict)).toBeNull() second.unmount() - const dependency = Promise.withResolvers>() + const dependency = Promise.withResolvers() const lateUpdate = vi.fn() - .mockResolvedValueOnce({ ok: true, value: { ...task, revision: 2, subject: 'Late edit' } }) + .mockResolvedValueOnce(taskSuccess({ ...task, revision: 2, subject: 'Late edit' })) .mockImplementationOnce(() => dependency.promise) const third = render() fireEvent.click(screen.getByRole('button', { name: /Agent Team/u })) @@ -574,7 +609,7 @@ describe('TeamAction', () => { expect(screen.getByRole('button', { name: '保存' }).disabled).toBe(true) expect(screen.getByRole('button', { name: '取消' }).disabled).toBe(true) third.rerender() - dependency.resolve({ ok: true, value: { ...task, revision: 3, subject: 'Late dependency' } }) + dependency.resolve(taskSuccess({ ...task, revision: 3, subject: 'Late dependency' })) await Promise.resolve() expect(screen.queryByText('Late dependency')).toBeNull() }) diff --git a/packages/experimental/client-ui-agent-team/tsconfig.json b/packages/experimental/client-ui-agent-team/tsconfig.json index 32c7d772f8..5bdb88c674 100644 --- a/packages/experimental/client-ui-agent-team/tsconfig.json +++ b/packages/experimental/client-ui-agent-team/tsconfig.json @@ -7,7 +7,7 @@ "include": ["src"], "references": [ { "path": "../../../vendor/cordis" }, - { "path": "../agent-team-remotes" }, + { "path": "../team" }, { "path": "../../api/remotes/tsconfig.client.json" }, { "path": "../../client/locale" }, { "path": "../../client/runtime" }, diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index f7ef10c848..8d1d890101 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -525,16 +525,8 @@ const SERVICE_ROLES: ServiceRole[] = [ pkg: 'experimental-agent-team', title: 'Agent Teams coordination domain', mode: 'core', - consumers: ['experimental-tool-agent-team'], - note: 'Owns the implicit-root roster, durable peer mailbox, shared task DAG, and continuable-child lifecycle; tool-agent-team contributes the scoped model policy and controls.', - }, - { - key: 'teamRemote', - pkg: 'agent-team-remotes', - title: 'Agent Teams browser adapter', - mode: 'core', - consumers: ['client-ui-agent-team'], - note: 'Stateless private adapter from the teams domain to the generated teams Remote namespace; the Web UI mounts its Client contribution.', + consumers: ['tool-agent-team', 'client-ui-agent-team'], + note: 'Owns the implicit-root roster, durable peer mailbox, shared task DAG, continuable-child lifecycle, and generated Team Remote methods; tool-agent-team contributes model controls and client-ui-agent-team mounts the browser contribution.', }, { key: 'jobs', diff --git a/scripts/run-gates.spec.ts b/scripts/run-gates.spec.ts index 33ef9aeb78..9754420fa2 100644 --- a/scripts/run-gates.spec.ts +++ b/scripts/run-gates.spec.ts @@ -445,7 +445,7 @@ describe('Node 24 lane ownership', () => { expect.arrayContaining([ 'packages/subagent/subagent-codex/tests/loader-composition.e2e.ts', 'packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts', - 'packages/experimental/agent-team-remotes/tests/built-lib.e2e.ts', + 'packages/experimental/team/tests/built-lib.e2e.ts', ]), ) expect(subject.find(item => item.id === 'web-snapshot')).toMatchObject({ diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index e9d6fff580..9ab1870f92 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -750,7 +750,7 @@ function builtBinSmokeGate(needs: string[] = ['build']): Gate { 'packages/subagent/subagent-codex/tests/loader-composition.e2e.ts', 'packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts', 'packages/api/remotes/tests/built-lib.e2e.ts', - 'packages/experimental/agent-team-remotes/tests/built-lib.e2e.ts', + 'packages/experimental/team/tests/built-lib.e2e.ts', // Built execution consumers: the only automated proof that package-name // imports reach their lib/ entrypoints under plain Node. The e2e lane runs // unbuilt, so these files self-skip there. diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts index 9cda431092..7a7e0e9b73 100644 --- a/scripts/verify-package-readme-model-experience.ts +++ b/scripts/verify-package-readme-model-experience.ts @@ -74,7 +74,6 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly> = { 'packages/client/ui-session': { kind: 'none', reason: 'Browser-side Session adapter; registers nothing model-facing.' }, 'packages/client/connection': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' }, 'packages/api/remotes': { kind: 'none', reason: 'The Remote BFF selects business methods and forwarded events; selected services own any model-visible effect.' }, - 'packages/experimental/agent-team-remotes': { kind: 'none', reason: 'The private Client assembly mounts typed Team Remote methods and registers no model-facing input.' }, 'packages/experimental/agent-team-web-profile': { kind: 'indirect', reason: 'The private Web layer delegates model composition to the Host-side Agent Teams profile.' }, 'packages/experimental/client-ui-agent-team': { kind: 'none', reason: 'The browser projection and task controls register no model-facing input.' }, 'packages/client/ui-layout': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' }, diff --git a/tsconfig.base.json b/tsconfig.base.json index 3a472cdd5c..847ba00580 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -245,7 +245,6 @@ "@deepseek-ai/dsh-client-ui-permission-presets": ["./packages/client/ui-permission-presets/src"], "@deepseek-ai/dsh-client-ui-skill": ["./packages/client/ui-skill/src"], "@deepseek-ai/dsh-client-ui-subagent": ["./packages/client/ui-subagent/src"], - "@deepseek-ai/dsh-agent-team-remotes": ["./packages/experimental/agent-team-remotes/src"], "@deepseek-ai/dsh-client-ui-agent-team": ["./packages/experimental/client-ui-agent-team/src"], "@deepseek-ai/dsh-client-ui-settings-plugins": ["./packages/client/ui-settings-plugins/src"], "@deepseek-ai/dsh-client-ui-jobs": ["./packages/client/ui-jobs/src"], diff --git a/tsconfig.host.json b/tsconfig.host.json index cf789db1a5..d19ff00848 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -147,7 +147,6 @@ { "path": "./packages/api/remotes/tsconfig.host.json" }, { "path": "./packages/api/session-controller/tsconfig.host.json" }, { "path": "./packages/api/workspace-controller/tsconfig.host.json" }, - { "path": "./packages/experimental/agent-team-remotes/tsconfig.host.json" }, { "path": "./packages/typert/loader" }, { "path": "./packages/session/session-persistence" }, { "path": "./packages/session/session-checkpoint-policy" },