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 8a70534909..cc197d7559 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: a8842bb647ca469fffcffe7f1d91eff42b6a403c -2026-08-06-agent-teams-web.zh.md: 55e8a65711fefe1ae2b9fcca6e0adb4d0358ecf6 +2026-08-06-agent-teams-web.md: 94ac6ea9c03985e21e060bd3af25a0455a9d2ebc +2026-08-06-agent-teams-web.zh.md: a9cc1cc352e37d8ea6585d43908efd7e5cdabe1e 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 a8842bb647..94ac6ea9c0 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 -`TeamService` directly contributes three Typert Remote methods: `teams/view`, `teams/createTask`, and `teams/updateTask`. The generated codecs use a browser-safe `@deepseek-ai/dsh-team/client` vocabulary. 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 [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. -`@deepseek-ai/dsh-agent-team-remotes` is a private Client assembly that mounts the generated Team contribution through the stable `ctx.remote` service. `@deepseek-ai/dsh-client-ui-agent-team` consumes only `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 instead of retrying or overwriting automatically. 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 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. 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 Remote assembly 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 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. ## Boundaries @@ -32,8 +32,8 @@ The Web UI has no mailbox timeline, worktree or Git controls, teammate creation, ## Testing -Team Remote generation and Host build verify the typed methods. Client typechecking and browser component tests cover the mounted namespace, Lead routing, every task action, conflict reload, 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. +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. ## Consequences -The Team service remains the only state machine, while Web is a typed projection and command adapter. 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 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. 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 55e8a65711..a9cc1cc352 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 @@ ## 决策 -`TeamService` 直接提供三个 Typert Remote method:`teams/view`、`teams/createTask` 与 `teams/updateTask`。生成式 codec 使用浏览器安全的 `@deepseek-ai/dsh-team/client` vocabulary。View 包含 roster 与当前 task 状态,但不包含 pending mailbox 内容或已删除 task tombstone。Task conflict 通过封闭 business result 跨越 Remote,使浏览器保留 `team-task-conflict`;transport 与 lookup failure 仍是普通 `RemoteResult` failure。 +[浏览器 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。 -`@deepseek-ai/dsh-agent-team-remotes` 是私有 Client assembly,通过稳定 `ctx.remote` service 挂载生成式 Team contribution。`@deepseek-ai/dsh-client-ui-agent-team` 只消费 `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 挂载 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 失效。 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 之后插入私有 Remote assembly 与 UI。它与 Host 侧 `@deepseek-ai/dsh-agent-team-profile` 一起应用。两个稳定 bundle 都不包含禁用的 Team row 或依赖。 +`@deepseek-ai/dsh-agent-team-web-profile` 在稳定 Web bundle 之后插入私有 Host Remote adapter 与 UI。它与 Host 侧 `@deepseek-ai/dsh-agent-team-profile` 一起应用。两个稳定 bundle 都不包含禁用的 Team row 或依赖。 ## 边界 @@ -32,8 +32,8 @@ Web UI 不提供 mailbox timeline、worktree 或 Git control、teammate creation ## 测试 -Team Remote 生成与 Host build 校验 typed method。Client typecheck 与浏览器 component test 覆盖挂载 namespace、Lead routing、所有 task action、conflict reload、陈旧 async result、navigation、dispose 与状态或错误呈现。Web 端到端测试在真实 Host Remote flow 上组合两个实验性 profile 层。 +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 仍是唯一状态机,Web 是 typed projection 与 command adapter。稳定 API Proxy、Client runtime、Subagent UI 和 Web bundle 保持 Team 无关。源码 checkout 用户必须向 Web profile 添加两个有序 experimental profile 层;promotion 可以移动这些 package,而无需修改 npm name 或生成式 namespace。 +Team service 仍是唯一状态机,且不公开浏览器专用 method 或 result type。稳定 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 new file mode 100644 index 0000000000..05f956f10e --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-08-19-isolate-agent-team-browser-remote.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/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 new file mode 100644 index 0000000000..59cc23c25a --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-08-19-isolate-agent-team-browser-remote.md @@ -0,0 +1,31 @@ +# 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 new file mode 100644 index 0000000000..feb4fcbb5c --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-08-19-isolate-agent-team-browser-remote.zh.md @@ -0,0 +1,31 @@ +# 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/knip.json b/knip.json index a989ed37c2..4dc0d3dcc5 100644 --- a/knip.json +++ b/knip.json @@ -758,7 +758,11 @@ }, "packages/experimental/agent-team-remotes": { "entry": [ + "tests/**/*.spec.ts", "tests/**/*.e2e.ts" + ], + "ignoreDependencies": [ + "zod" ] }, "packages/experimental/agent-team-web-profile": { diff --git a/packages/experimental/agent-team-remotes/README.i18n.yaml b/packages/experimental/agent-team-remotes/README.i18n.yaml index 42db67354d..bd4614e5e6 100644 --- a/packages/experimental/agent-team-remotes/README.i18n.yaml +++ b/packages/experimental/agent-team-remotes/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-remotes/README.md -README.md: be1d28c5ef53b932a9553a716488e465fd2ad8ce -README.zh.md: ee4e0734e1296424b428b8ecb184a5e8b06b194a +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 index be1d28c5ef..7b936c7a0a 100644 --- a/packages/experimental/agent-team-remotes/README.md +++ b/packages/experimental/agent-team-remotes/README.md @@ -2,15 +2,15 @@ English | [中文](README.zh.md) -Private Client assembly for the Agent Teams Typert Remote contribution. Its Client entry imports the generated `@deepseek-ai/dsh-team/remote` runtime value, mounts it through the stable `ctx.remote.$mount()` service, and re-exports the declaration merge that adds `ctx.remote.teams`. +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 contribution exposes `teams/view`, `teams/createTask`, and `teams/updateTask`. The generated codecs validate arguments and results, while the Team service remains the only owner of roster and task state. This package contains no Host resolver or transport logic; `@deepseek-ai/dsh-api-remotes` supplies the stable Remote service and Agent identity policy. +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. -The root export is inert because this package mounts only in a Client environment. [`@deepseek-ai/dsh-agent-team-web-profile`](../agent-team-web-profile/README.md) inserts it before the Team UI so the namespace exists when the UI activates. +[`@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 Client assembly only mounts typed Remote methods and registers no model-facing input. +None, as this browser adapter delegates typed Remote methods and registers no model-facing input. #### KV Cache effect @@ -18,5 +18,5 @@ No direct effect; invoked Team methods and their model-facing consumers own any ## Known Limitations and Deferred Work -- **Fixed contribution set** — adding a Team Remote method requires regenerating the Team artifacts and rebuilding this explicit assembly. +- **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 index ee4e0734e1..baf9547fc0 100644 --- a/packages/experimental/agent-team-remotes/README.zh.md +++ b/packages/experimental/agent-team-remotes/README.zh.md @@ -2,15 +2,15 @@ [English](README.md) | 中文 -Agent Teams Typert Remote contribution 的私有 Client assembly。它的 Client entry 导入生成式 `@deepseek-ai/dsh-team/remote` runtime value,通过稳定 `ctx.remote.$mount()` service 挂载,并重新导出为 `ctx.remote.teams` 增加类型的 declaration merge。 +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`。生成式 codec 校验参数和结果,Team service 仍是 roster 与 task 状态的唯一 owner。本包不包含 Host resolver 或 transport 逻辑;`@deepseek-ai/dsh-api-remotes` 提供稳定 Remote service 与 Agent identity policy。 +生成式 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。 -Root export 不执行行为,因为本包只在 Client 环境挂载。[`@deepseek-ai/dsh-agent-team-web-profile`](../agent-team-web-profile/README.md) 会先于 Team UI 插入本包,确保 UI 激活时 namespace 已存在。 +[`@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。 ## 模型体验 -无直接影响,因为该 Client assembly 只挂载 typed Remote method,不注册面向模型的输入。 +无直接影响,因为该浏览器 adapter 只委托 typed Remote method,不注册面向模型的输入。 #### KV Cache 影响 @@ -18,5 +18,5 @@ Root export 不执行行为,因为本包只在 Client 环境挂载。[`@deepse ## 已知限制与暂缓事项 -- **固定 contribution 集合**:增加 Team Remote method 时,需要重新生成 Team artifact 并重建这个显式 assembly。 +- **固定 contribution 集合**:增加浏览器 operation 时,需要修改该 adapter 并重新生成其 Remote artifact。 - **仅限源码 checkout**:正式发布会排除这个私有包。 diff --git a/packages/experimental/agent-team-remotes/package.json b/packages/experimental/agent-team-remotes/package.json index 13979d5780..0709991289 100644 --- a/packages/experimental/agent-team-remotes/package.json +++ b/packages/experimental/agent-team-remotes/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-agent-team-remotes", - "description": "Private Client assembly for the Agent Teams Typert Remote contribution", + "description": "Private browser Remote adapter for the Agent Teams domain service", "version": "0.1.0-rc.7", "private": true, "repository": { @@ -20,43 +20,51 @@ "types": "./lib/types/invariant.d.ts", "default": "./lib/invariant.js" }, - "./client": { - "types": "./lib/types/client/index.d.ts", - "default": "./lib/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" }, - "dsh": { - "client": { - "inject": [ - "@deepseek-ai/dsh-api-remotes" - ], - "platform": "web", - "immediately": true - } - }, "scripts": { "bundle": "tsdown", "watch": "tsdown --watch" }, "license": "MIT", + "dependencies": { + "zod": "^4.4.3" + }, "peerDependencies": { - "@deepseek-ai/dsh-api-remotes": "workspace:^", + "@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-api-remotes": "workspace:^", + "@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/client.js", - "lib/types/**/*.d.ts" + "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/client/index.ts b/packages/experimental/agent-team-remotes/src/client/index.ts deleted file mode 100644 index 9a8b5edf52..0000000000 --- a/packages/experimental/agent-team-remotes/src/client/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** Client assembly for the generated Agent Teams Remote contribution. */ - -import type { Context } from '@deepseek-ai/cordis' -import type {} from '@deepseek-ai/dsh-api-remotes/client' -import teamsRemote from '@deepseek-ai/dsh-team/remote' - -export type {} from '@deepseek-ai/dsh-team/remote' - -/** Required service: the typed Client Remote contribution mount. */ -export const inject = ['remote'] - -/** - * Mount the Agent Teams Remote namespace selected by the experimental Web profile. - * @param ctx - Client Cordis root carrying the Remote service. - * @returns disposer after the contribution is ready. - */ -export function apply(ctx: Context): Promise<() => Promise> { - return ctx.remote.$mount(teamsRemote) -} diff --git a/packages/experimental/agent-team-remotes/src/index.ts b/packages/experimental/agent-team-remotes/src/index.ts index 5539b6adc3..83005fe5d7 100644 --- a/packages/experimental/agent-team-remotes/src/index.ts +++ b/packages/experimental/agent-team-remotes/src/index.ts @@ -1,4 +1,98 @@ -/** Pure Host half for the private Agent Teams Client Remote assembly. */ +/** Browser-facing Remote adapter over the Agent Teams domain service. */ -/** Host plugin body; the generated contribution mounts only in Client environments. */ -export function apply(): void {} +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 index 8522554074..560e5ff6a6 100644 --- a/packages/experimental/agent-team-remotes/src/invariant.ts +++ b/packages/experimental/agent-team-remotes/src/invariant.ts @@ -1,4 +1,4 @@ -/** Package-owned invariant companion for the Agent Teams Client Remote assembly. */ +/** 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' @@ -10,8 +10,8 @@ export const name = 'agent-team-remotes-invariant' /** Service required before the companion can register. */ export const inject = ['invariants'] -// No runtime invariant: Client Remote validates each request and result with -// the generated Team codecs, while the Team service owns mutable state. +// No runtime invariant: generated codecs validate the adapter's requests and +// results, while the Team service owns every mutable relationship. const install: InvariantInstaller = () => {} /** diff --git a/packages/experimental/agent-team-remotes/src/types.ts b/packages/experimental/agent-team-remotes/src/types.ts new file mode 100644 index 0000000000..edef7e071b --- /dev/null +++ b/packages/experimental/agent-team-remotes/src/types.ts @@ -0,0 +1,79 @@ +/** 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/built-lib.e2e.ts b/packages/experimental/agent-team-remotes/tests/built-lib.e2e.ts index 43b3877eda..f34dad730d 100644 --- a/packages/experimental/agent-team-remotes/tests/built-lib.e2e.ts +++ b/packages/experimental/agent-team-remotes/tests/built-lib.e2e.ts @@ -1,4 +1,4 @@ -/** Plain-Node smoke for the generated Agent Teams Client Remote assembly. */ +/** Plain-Node smoke for the built Agent Teams Remote adapter artifacts. */ import { execFile } from 'node:child_process' import { existsSync } from 'node:fs' @@ -12,63 +12,37 @@ 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/client.js', 'packages/experimental/agent-team-remotes/lib/index.js', - 'packages/experimental/team/lib/typert.remote-client.js', + 'packages/experimental/agent-team-remotes/lib/typert.remote-client.js', ].every(path => existsSync(artifact(path))) -describe.skipIf(!requiredArtifacts)('Agent Teams Remote built LIB assembly', () => { - it('mounts exactly the generated Team contribution and keeps the Host half inert', async () => { +describe.skipIf(!requiredArtifacts)('Agent Teams Remote built LIB adapter', () => { + it('loads the Host adapter and its generated browser contribution under plain Node', async () => { const urls = { - client: artifactUrl('packages/experimental/agent-team-remotes/lib/client.js'), host: artifactUrl('packages/experimental/agent-team-remotes/lib/index.js'), + remote: artifactUrl('packages/experimental/agent-team-remotes/lib/typert.remote-client.js'), } const script = ` - const handoffs = new Map() - globalThis.window = { - __ModuleLoader__: { - load(handoff) { handoffs.set(handoff.id, handoff) }, - }, - } const host = await import(${JSON.stringify(urls.host)}) - host.apply() - await import(${JSON.stringify(urls.client)}) - const handoff = handoffs.get('@deepseek-ai/dsh-agent-team-remotes') - if (handoff === undefined) throw new Error('missing Agent Teams Remote Client handoff') - const plugin = handoff.factory(specifier => { - throw new Error('unexpected Client external ' + specifier) - }) - let mounted - const dispose = () => {} - const result = await plugin.apply({ - remote: { - $mount(contribution) { - mounted = contribution - return Promise.resolve(dispose) - }, - }, - }) + const remote = await import(${JSON.stringify(urls.remote)}) console.log(JSON.stringify({ - inject: plugin.inject, - sameDisposer: result === dispose, - methods: mounted?.descriptors.map(descriptor => descriptor.id), + className: host.default.name, + methods: remote.default.descriptors.map(descriptor => descriptor.id), })) ` const result = await runPlainNode(script) expect(result.exitCode, `stderr:\n${result.stderr}`).toBe(0) const output = JSON.parse(result.stdout.trim().split('\n').at(-1) ?? '{}') as { - inject: string[] - sameDisposer: boolean + className: string methods: string[] } expect(output).toEqual({ - inject: ['remote'], - sameDisposer: true, + className: 'AgentTeamRemoteService', methods: [ - '@deepseek-ai/dsh-team#teams/createTask', - '@deepseek-ai/dsh-team#teams/updateTask', - '@deepseek-ai/dsh-team#teams/view', + '@deepseek-ai/dsh-agent-team-remotes#teams/createTask', + '@deepseek-ai/dsh-agent-team-remotes#teams/updateTask', + '@deepseek-ai/dsh-agent-team-remotes#teams/view', ], }) }) diff --git a/packages/experimental/agent-team-remotes/tests/remote-adapter.spec.ts b/packages/experimental/agent-team-remotes/tests/remote-adapter.spec.ts new file mode 100644 index 0000000000..065dc2d85e --- /dev/null +++ b/packages/experimental/agent-team-remotes/tests/remote-adapter.spec.ts @@ -0,0 +1,103 @@ +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.client.json b/packages/experimental/agent-team-remotes/tsconfig.client.json deleted file mode 100644 index 34f7f86636..0000000000 --- a/packages/experimental/agent-team-remotes/tsconfig.client.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "extends": "../../../tsconfig.base.client.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "lib/types", - "tsBuildInfoFile": "lib/tsconfig.client.tsbuildinfo" - }, - "files": ["src/client/index.ts"], - "references": [ - { - "path": "../../../vendor/cordis" - }, - { - "path": "../../api/remotes/tsconfig.client.json" - }, - { - "path": "../team" - } - ] -} diff --git a/packages/experimental/agent-team-remotes/tsconfig.host.json b/packages/experimental/agent-team-remotes/tsconfig.host.json deleted file mode 100644 index 2f18caea89..0000000000 --- a/packages/experimental/agent-team-remotes/tsconfig.host.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "lib/types", - "tsBuildInfoFile": "lib/tsconfig.host.tsbuildinfo" - }, - "files": ["src/index.ts", "src/invariant.ts"], - "references": [ - { - "path": "../../../vendor/cordis" - }, - { - "path": "../../runtime-diagnostics/invariants" - } - ] -} diff --git a/packages/experimental/agent-team-remotes/tsconfig.json b/packages/experimental/agent-team-remotes/tsconfig.json index 2eca820546..69152cb0d4 100644 --- a/packages/experimental/agent-team-remotes/tsconfig.json +++ b/packages/experimental/agent-team-remotes/tsconfig.json @@ -1,11 +1,15 @@ { - "files": [], + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": ["src"], "references": [ - { - "path": "./tsconfig.host.json" - }, - { - "path": "./tsconfig.client.json" - } + { "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 index 24d7f8d797..e49647c648 100644 --- a/packages/experimental/agent-team-remotes/tsdown.config.ts +++ b/packages/experimental/agent-team-remotes/tsdown.config.ts @@ -1,3 +1,25 @@ -import { clientBundle } from '../../client/tsdown.client.ts' +import { defineConfig } from 'tsdown' -export default clientBundle('@deepseek-ai/dsh-agent-team-remotes', ['lib/types/index.js', 'lib/types/invariant.js']) +/** 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 33f7358d3c..1d4469984b 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: cf893ce84f30ae07834b2ff1d4272b037547cb3e -README.zh.md: e4ecafb020a84b37f2babd731abc4d09f35e66e7 +README.md: 089ab4e29c978bb537f0be5b374a3b5d7c95e484 +README.zh.md: d9c3b2180dd6f2a039211ec8de1fb68cdfcf93b5 diff --git a/packages/experimental/agent-team-web-profile/README.md b/packages/experimental/agent-team-web-profile/README.md index cf893ce84f..089ab4e29c 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 Client Remote assembly 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 experimental Host Remote adapter followed by 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 only the generated Client Remote namespace and browser presentation. Removing either experimental bundle leaves the stable base and Web composition unchanged. +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. ## 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 e4ecafb020..d9c3b2180d 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 先插入实验性 Client Remote assembly,再插入 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 先插入实验性 Host Remote adapter,再插入 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 层只提供生成式 Client Remote namespace 与浏览器呈现。移除任一实验性 bundle 后,稳定 base 与 Web composition 保持不变。 +Host profile 提供 Team domain 与模型工具。本 Web 层提供浏览器 adapter、生成式 Client Remote namespace 与呈现。移除任一实验性 bundle 后,稳定 base 与 Web composition 保持不变。 ## 模型体验 diff --git a/packages/experimental/agent-team/README.md b/packages/experimental/agent-team/README.md index 7f5d99d2ef..e2f317c253 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 -### Client Remote surface +### Browser adapter -`TeamService` contributes the generated `teams/view`, `teams/createTask`, and `teams/updateTask` Typert Remote methods. `./client` exports only browser-safe Team request and view types; `./typert` and `./remote` are generated Host and Client artifacts. The view omits mailbox contents and deleted task tombstones. Task conflicts cross Remote as an explicit business result so a Client can reload instead of losing the Team error code inside a generic carrier failure. +`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. ## Model Experience diff --git a/packages/experimental/agent-team/README.zh.md b/packages/experimental/agent-team/README.zh.md index 0fc1c9f099..7d66ddbe99 100644 --- a/packages/experimental/agent-team/README.zh.md +++ b/packages/experimental/agent-team/README.zh.md @@ -165,9 +165,9 @@ dispose 会关闭准入、中止并等待已获准的创建与 mailbox dispatch -### Client Remote 界面 +### 浏览器适配器 -`TeamService` 提供生成式 `teams/view`、`teams/createTask` 与 `teams/updateTask` Typert Remote method。`./client` 只导出浏览器安全的 Team request 与 view type;`./typert` 和 `./remote` 是生成的 Host 与 Client artifact。View 不包含 mailbox 内容或已删除 task tombstone。Task conflict 通过显式 business result 跨越 Remote,使 Client 能重新读取状态,而不会把 Team error code 丢失在通用 carrier failure 中。 +`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。 ## 模型体验 diff --git a/packages/experimental/agent-team/package.json b/packages/experimental/agent-team/package.json index e0410967ec..962a84c625 100644 --- a/packages/experimental/agent-team/package.json +++ b/packages/experimental/agent-team/package.json @@ -24,14 +24,6 @@ "types": "./lib/types/client.d.ts", "default": "./lib/types/client.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" }, @@ -39,11 +31,7 @@ "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" + "lib/types/**/*.d.ts" ], "license": "MIT", "dependencies": { @@ -58,7 +46,6 @@ "@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": { @@ -76,7 +63,6 @@ "@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 981212eea3..c66e2ce3f6 100644 --- a/packages/experimental/agent-team/src/index.ts +++ b/packages/experimental/agent-team/src/index.ts @@ -1,10 +1,9 @@ /** Agent Teams service façade over roster, mailbox, task, and runtime lifecycle owners. */ -import { Context } from '@deepseek-ai/cordis' +import { Context, Service } 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' @@ -23,8 +22,6 @@ import type { SpawnTeammateResult, TeamMemberView, TeamTaskView, - TeamTaskMutationResult, - TeamView, TeamWaitResult, UpdateTeamTaskRequest, } from './types.ts' @@ -56,7 +53,7 @@ function positiveLimit(name: string, value: number): number { } /** Agent Teams service backed by the exact live Lead Session log. */ -export class TeamService extends TypertRemoteService { +export class TeamService extends Service { static inject = ['agents', 'sessions', 'sessionPersistence', 'subagents'] static Config: z = z.object({ @@ -135,19 +132,6 @@ export class TeamService extends TypertRemoteService { return this.roster.list(this.roster.membership(agent)) } - /** - * Read the current roster and non-deleted task board for a browser client. - * @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.listMembers(agent), - tasks: this.listTasks(agent), - } - } - /** * Create one named, continuable direct child of the Team Lead. * @param caller - exact live Lead Agent. @@ -178,17 +162,6 @@ export class TeamService extends TypertRemoteService { return await this.tasks.create(this.roster.membership(caller), request) } - /** - * Create one shared task for a browser client. - * @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') - createTaskForClient(agent: Agent, request: CreateTeamTaskRequest): Promise { - return this.createTask(agent, request) - } - /** * Return one task, including a deleted tombstone. * @param caller - exact live Team member reading the task. @@ -218,28 +191,6 @@ export class TeamService extends TypertRemoteService { return await this.tasks.update(caller, this.roster.membership(caller), request) } - /** - * Apply one task mutation for a browser client while preserving CAS conflicts. - * @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 updateTaskForClient(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, - }, - } - } - } - /** * Wait for the next Team-domain or member-status change. * @param caller - exact live Team member waiting for activity. diff --git a/packages/experimental/agent-team/src/types.ts b/packages/experimental/agent-team/src/types.ts index bca6c49d38..cee2b1064a 100644 --- a/packages/experimental/agent-team/src/types.ts +++ b/packages/experimental/agent-team/src/types.ts @@ -96,23 +96,6 @@ export interface TeamTaskView { readonly writeScopeWarnings: string[] } -/** Browser-safe point-in-time roster and task-board view. */ -export interface TeamView { - readonly members: TeamMemberView[] - readonly tasks: TeamTaskView[] -} - -/** Browser 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 - } - } - /** One peer message retained until its target Session records it. */ export interface TeamMessageSnapshot { readonly id: TeamMessageId diff --git a/packages/experimental/agent-team/tests/team.spec.ts b/packages/experimental/agent-team/tests/team.spec.ts index ae594227b7..9b606a3de3 100644 --- a/packages/experimental/agent-team/tests/team.spec.ts +++ b/packages/experimental/agent-team/tests/team.spec.ts @@ -512,41 +512,6 @@ describe('Team identity and provisioning', () => { }) describe('Team shared task DAG', () => { - it('projects browser views and preserves task mutation failures across Remote', async () => { - const { ctx, lead } = await setup([]) - expect(ctx.teams.view(lead)).toEqual({ - members: [expect.objectContaining({ id: lead.id, name: 'lead', role: 'lead' })], - tasks: [], - }) - - const task = await ctx.teams.createTaskForClient(lead, { - subject: 'browser task', - description: 'created through the Remote face', - }) - await expect(ctx.teams.updateTaskForClient(lead, { - taskId: task.id, - expectedRevision: task.revision, - action: 'claim', - })).resolves.toMatchObject({ ok: true, value: { status: 'in_progress' } }) - await expect(ctx.teams.updateTaskForClient(lead, { - taskId: task.id, - expectedRevision: task.revision, - action: 'delete', - })).resolves.toMatchObject({ ok: false, error: { code: 'team-task-conflict' } }) - await expect(ctx.teams.updateTaskForClient(lead, { - taskId: TeamTaskId('task-999'), - expectedRevision: 1, - action: 'delete', - })).resolves.toMatchObject({ ok: false, error: { code: 'team-rejected' } }) - - vi.spyOn(ctx.teams, 'updateTask').mockRejectedValueOnce(new Error('unexpected mutation failure')) - await expect(ctx.teams.updateTaskForClient(lead, { - taskId: task.id, - expectedRevision: 2, - action: 'delete', - })).rejects.toThrow('unexpected mutation failure') - }) - it('fails loudly when the durable numeric task id space is exhausted', async () => { const { ctx, lead } = await setup([]) const id = TeamTaskId(`task-${Number.MAX_SAFE_INTEGER}`) diff --git a/packages/experimental/agent-team/tsconfig.json b/packages/experimental/agent-team/tsconfig.json index ecfdf8e6e3..d0defa8242 100644 --- a/packages/experimental/agent-team/tsconfig.json +++ b/packages/experimental/agent-team/tsconfig.json @@ -14,7 +14,6 @@ { "path": "../../core/session" }, { "path": "../../core/agent" }, { "path": "../../subagent/subagent" }, - { "path": "../../typert/protocol" }, { "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 d716c98671..7b2f094b86 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: 18352135f3883e8ecda94bc3c1d7c900cdf5d7ad -README.zh.md: abfbbf6a6238f16274db42b7e7295f21d5406edd +README.md: f64b2878f58455173e9899f6ee007706e9ca1ff2 +README.zh.md: a25ce13978456a349a23af82d53f89a01f4cecdf diff --git a/packages/experimental/client-ui-agent-team/README.md b/packages/experimental/client-ui-agent-team/README.md index 18352135f3..f64b2878f5 100644 --- a/packages/experimental/client-ui-agent-team/README.md +++ b/packages/experimental/client-ui-agent-team/README.md @@ -2,11 +2,11 @@ English | [中文](README.zh.md) -Private Web Agent Teams presentation. It contributes one conversation-header action containing the current roster and shared task board. The browser calls the generated `ctx.remote.teams` namespace 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 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. 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. -The task board shows task identity, owner, blockers, readiness, advisory write scopes, and overlap warnings. Humans can create, edit, assign or unassign, complete, reopen, and delete tasks through `teams/createTask` and `teams/updateTask`. Every mutation sends the displayed revision. A `team-task-conflict` result reloads the Team view and displays a stale-state notice instead of retrying or overwriting. Editing task text or scopes and changing dependencies remain two sequential compare-and-set mutations because the Team service exposes them as separate actions. +The task board shows task identity, owner, blockers, readiness, advisory write scopes, and overlap warnings. Humans can create, edit, assign or unassign, complete, reopen, and delete tasks through `teams/createTask` and `teams/updateTask`. Every mutation sends the displayed revision. A `team-task-conflict` result reloads the Team view and displays a stale-state notice only after that reload succeeds; a reload failure remains visible instead. Editing task text or scopes and changing dependencies remain two sequential compare-and-set mutations because the Team service exposes them as separate actions. The root export is inert on the Host. The Client export owns locale and slot registrations, and Cordis disposes both with the plugin fiber. Install the package through [`@deepseek-ai/dsh-agent-team-web-profile`](../agent-team-web-profile/README.md) after the stable Web bundle and the Host-side Agent Teams profile. diff --git a/packages/experimental/client-ui-agent-team/README.zh.md b/packages/experimental/client-ui-agent-team/README.zh.md index abfbbf6a62..a25ce13978 100644 --- a/packages/experimental/client-ui-agent-team/README.zh.md +++ b/packages/experimental/client-ui-agent-team/README.zh.md @@ -2,11 +2,11 @@ [English](README.md) | 中文 -私有 Web Agent Teams 呈现包。它向会话页头提供一个包含当前 roster 与共享任务板的 action。浏览器调用由 [`@deepseek-ai/dsh-agent-team-remotes`](../agent-team-remotes/README.md) 提供的生成式 `ctx.remote.teams` namespace;它不扩展稳定 API Proxy,也不存储权威 Team 状态。 +私有 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 状态。 打开 panel 会调用 `teams/view`。Roster row 展示持久 name、运行时 status、model 与 diagnostics。选择健康 teammate 时,系统刷新既有直接 child catalog,并打开普通的 `{ parentSessionId, childSessionId, mode: 'continuable' }` address。History 与后续人类 prompt 继续使用稳定 addressed-subagent 会话路径;本包不会向该 address 添加 Team 专用字段。 -任务板展示 task identity、owner、blocker、readiness、提示性 write scope 与重叠 warning。人类可以通过 `teams/createTask` 与 `teams/updateTask` 创建、编辑、分配或取消分配、完成、重开和删除任务。每次 mutation 都发送当前显示的 revision。收到 `team-task-conflict` 结果后,UI 会重新读取 Team view 并显示状态陈旧提示,不会自动重试或覆盖。Team service 将任务文本或 scope 编辑与 dependency 修改公开为两个独立 action,因此两者仍使用两个连续的 compare-and-set mutation。 +任务板展示 task identity、owner、blocker、readiness、提示性 write scope 与重叠 warning。人类可以通过 `teams/createTask` 与 `teams/updateTask` 创建、编辑、分配或取消分配、完成、重开和删除任务。每次 mutation 都发送当前显示的 revision。收到 `team-task-conflict` 结果后,UI 仅在重新读取 Team view 成功后显示状态陈旧提示;如果重新读取失败,则保留该错误。Team service 将任务文本或 scope 编辑与 dependency 修改公开为两个独立 action,因此两者仍使用两个连续的 compare-and-set mutation。 Root export 在 Host 上不执行行为。Client export 负责 locale 与 slot 注册,Cordis 会随 plugin fiber dispose 两者。在稳定 Web bundle 与 Host 侧 Agent Teams profile 之后,通过 [`@deepseek-ai/dsh-agent-team-web-profile`](../agent-team-web-profile/README.md) 安装本包。 diff --git a/packages/experimental/client-ui-agent-team/package.json b/packages/experimental/client-ui-agent-team/package.json index e514a9e6af..fa3e7bc4ef 100644 --- a/packages/experimental/client-ui-agent-team/package.json +++ b/packages/experimental/client-ui-agent-team/package.json @@ -30,7 +30,7 @@ "dsh": { "client": { "inject": [ - "@deepseek-ai/dsh-agent-team-remotes", + "@deepseek-ai/dsh-api-remotes", "@deepseek-ai/dsh-client-locale", "@deepseek-ai/dsh-client-runtime", "@deepseek-ai/dsh-client-ui-conversation", @@ -56,7 +56,6 @@ "@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:^" }, @@ -70,7 +69,6 @@ "@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 38e0ac6684..95c9c6ec9b 100644 --- a/packages/experimental/client-ui-agent-team/src/client/TeamAction.tsx +++ b/packages/experimental/client-ui-agent-team/src/client/TeamAction.tsx @@ -6,7 +6,7 @@ import type { TeamTaskId, TeamTaskView as TeamTask, TeamView, -} from '@deepseek-ai/dsh-team/client' +} from '@deepseek-ai/dsh-agent-team-remotes/types' import { IconCheckOutline14, IconCloseOutline16, IconEditOutline16, IconPlusOutline16, IconRefreshOutline14, IconTrashOutline16, IconUserOutline16, StateDot, @@ -61,7 +61,7 @@ function items(value: string): string[] { } function taskIds(value: string): TeamTaskId[] { - return items(value) as TeamTaskId[] + return items(value) } function statusKey(status: TeamTask['status']): TeamKey { @@ -117,18 +117,20 @@ export function TeamAction({ setPendingTask(null) }, [sessionId]) - const refresh = useCallback(async (): Promise => { + const refresh = useCallback(async (): Promise => { const requestedSession = sessionId const generation = ++refreshGeneration.current setLoading(true) const result = await load(requestedSession) - if (sessionRef.current !== requestedSession || refreshGeneration.current !== generation) return + if (sessionRef.current !== requestedSession || refreshGeneration.current !== generation) return false setLoading(false) if (result.ok) { setView(result.value) setError(null) + return true } else { setError(result.error) + return false } }, [load, sessionId]) @@ -148,9 +150,9 @@ export function TeamAction({ setPendingTask(null) if (!result.ok) { if (result.conflict) { - await refresh() + const reloaded = await refresh() if (sessionRef.current !== requestedSession) return undefined - setError(t('conflict')) + if (reloaded) setError(t('conflict')) } else { setError(result.error) } @@ -234,10 +236,12 @@ export function TeamAction({ setPendingTask(null) if (!dependencyResult.ok) { if (dependencyResult.conflict) { - await refresh() + const reloaded = await refresh() if (sessionRef.current !== requestedSession) return + if (reloaded) setError(t('conflict')) + } else { + setError(dependencyResult.error) } - setError(dependencyResult.conflict ? t('conflict') : dependencyResult.error) return } invalidateRefresh() 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 a147506fd3..4e28bf4645 100644 --- a/packages/experimental/client-ui-agent-team/src/client/index.ts +++ b/packages/experimental/client-ui-agent-team/src/client/index.ts @@ -1,16 +1,17 @@ /** Browser plugin for the Agent Teams roster, task board, and Team-routed teammate navigation. */ -import type {} from '@deepseek-ai/dsh-agent-team-remotes/client' -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 teamsRemote from '@deepseek-ai/dsh-agent-team-remotes/remote' import type { TeamMemberView as TeamRosterMember, TeamTaskMutationResult, TeamTaskView as TeamTask, TeamView, -} from '@deepseek-ai/dsh-team/client' +} from '@deepseek-ai/dsh-agent-team-remotes/types' +import type {} from '@deepseek-ai/dsh-agent-team-remotes/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' @@ -26,7 +27,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { } /** Required browser services for RPC, navigation, slots, and localized copy. */ -export const inject = ['sessions', 'remote', 'remote.teams', 'slots', 'locale'] +export const inject = ['sessions', 'remote', 'slots', 'locale'] function settle(result: RemoteResult): TeamActionResult { if (result.ok) return { ok: true, value: result.value } @@ -47,8 +48,7 @@ function settleMutation(result: RemoteResult): TeamActio } } -/** Register the Team conversation-header action and its RPC-backed business face. */ -export function apply(ctx: ClientContext): void { +function registerUi(ctx: ClientContext): void { ctx.effect(() => ctx.locale.register('team', { zh, en }), 'ui-team: dictionaries') const sessions = ctx.sessions const leadSessionId = (sessionId: SessionId): SessionId => { @@ -77,7 +77,7 @@ export function apply(ctx: ClientContext): void { if (sessions.list.getSnapshot().current !== sessionId) return sessions.openSubagent({ parentSessionId, - childSessionId: member.id, + childSessionId: member.id as SessionId, mode: 'continuable', }) }, @@ -94,3 +94,20 @@ export function apply(ctx: ClientContext): void { }, TeamAction), ) } + +/** Mount the generated Team Remote contribution, then register its browser UI. */ +export async function apply(ctx: ClientContext): Promise<() => Promise> { + const disposeRemote = await ctx.remote.$mount(teamsRemote) + const ui = ctx.inject(['sessions', 'remote.teams', 'slots', 'locale'], registerUi) + try { + await ui + } catch (error) { + await ui.dispose() + await disposeRemote() + throw error + } + return async () => { + await ui.dispose() + await disposeRemote() + } +} 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 a986f73f9d..0e40504b75 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 @@ -1,13 +1,10 @@ import { Context, Service } from '@deepseek-ai/cordis' -import { describe, expect, it } from 'vitest' +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, - TeamTaskId, -} from '@deepseek-ai/dsh-team/client' -import type {} from '@deepseek-ai/dsh-team/remote' +import type { TeamMemberView as TeamRosterMember } from '@deepseek-ai/dsh-agent-team-remotes/types' +import type {} from '@deepseek-ai/dsh-agent-team-remotes/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' @@ -18,6 +15,7 @@ const CHILD = 'team-child' as SessionId async function bench(options: { addressed?: boolean conflict?: boolean + registrationFailure?: boolean remoteFailure?: 'view' | 'update' refreshGate?: Promise } = {}) { @@ -28,16 +26,23 @@ async function bench(options: { return Promise.resolve({ ok: true as const, value }) } const task = { - id: 'task-1' as TeamTaskId, + id: 'task-1', revision: 1, subject: 'Task', description: 'Description', status: 'pending' as const, blockedBy: [], writeScopes: [], ready: true, writeScopeWarnings: [], } class RemoteService extends Service { + readonly disposeMount = vi.fn(() => Promise.resolve()) + readonly mount = vi.fn((_contribution: unknown) => Promise.resolve(this.disposeMount)) + constructor(serviceCtx: Context) { super(serviceCtx, 'remote') } + + $mount(contribution: unknown): Promise<() => Promise> { + return this.mount(contribution) + } } - new RemoteService(ctx) + const remote = new RemoteService(ctx) const failure = { ok: false as const, error: { code: 'internal', message: 'offline', details: {} }, @@ -100,15 +105,29 @@ async function bench(options: { name: 'root', children: { 'conversation.session.header.actions': { kind: 'list', scope: 'session' } }, } as never, () => null) - const fiber = ctx.plugin({ inject: [...inject], apply }) - await fiber.await() + if (options.registrationFailure === true) { + vi.spyOn(ctx.slots, 'inject').mockImplementationOnce(() => { throw new Error('slot registration failed') }) + } + const fiber = options.registrationFailure === true + ? ctx.plugin({ apply() {} }) + : ctx.plugin({ inject: [...inject], apply }) + const activation: Promise = options.registrationFailure === true + ? apply(ctx).catch((error: unknown) => error) + : fiber.await() + if (options.registrationFailure !== true) { + await activation + } else { + await fiber.await() + } const entry = () => ctx.slots.entries('conversation.session.header.actions') .find(candidate => candidate.component === TeamAction) return { ctx, fiber, + activation, calls, navigation, + remote, entry, collapseHeader, select: (sessionId: SessionId) => { current = sessionId }, @@ -118,21 +137,22 @@ async function bench(options: { describe('ui-team browser plugin', () => { it('registers one disposable header action with RPC-backed task operations', async () => { const b = await bench() - expect(inject).toEqual(['sessions', 'remote', 'remote.teams', 'slots', 'locale']) + expect(inject).toEqual(['sessions', 'remote', 'slots', 'locale']) expect(b.entry()).toMatchObject({ options: { id: 'agent-team', order: 20 }, locale: 'team', }) + expect(b.remote.mount).toHaveBeenCalledOnce() const actions = (b.entry()!.inject as unknown as () => TeamActionInjected)() expect((await actions.load(SESSION)).ok).toBe(true) expect((await actions.createTask(SESSION, { subject: 'Task', description: 'Description', blockedBy: [], writeScopes: [], })).ok).toBe(true) expect((await actions.updateTask(SESSION, { - taskId: 'task-1' as TeamTaskId, expectedRevision: 1, action: 'complete', + taskId: 'task-1', expectedRevision: 1, action: 'complete', })).ok).toBe(true) expect((await actions.updateTask(SESSION, { - taskId: 'task-1' as TeamTaskId, expectedRevision: 2, action: 'reassign', owner: 'worker', + taskId: 'task-1', 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', @@ -150,13 +170,21 @@ describe('ui-team browser plugin', () => { await b.fiber.dispose() expect(b.entry()).toBeUndefined() + expect(b.remote.disposeMount).toHaveBeenCalledOnce() + }) + + it('unmounts the Remote contribution when later Client registration fails', async () => { + const b = await bench({ registrationFailure: true }) + await expect(b.activation).resolves.toMatchObject({ message: 'slot registration failed' }) + expect(b.remote.mount).toHaveBeenCalledOnce() + expect(b.remote.disposeMount).toHaveBeenCalledOnce() }) it('marks stale revisions as conflicts and retains non-conflict errors as ordinary failures', async () => { const b = await bench({ conflict: true }) const actions = (b.entry()!.inject as unknown as () => TeamActionInjected)() await expect(actions.updateTask(SESSION, { - taskId: 'task-1' as TeamTaskId, expectedRevision: 1, action: 'delete', + taskId: 'task-1', expectedRevision: 1, action: 'delete', })).resolves.toEqual({ ok: false, error: 'stale (team-task-conflict)', @@ -176,7 +204,7 @@ describe('ui-team browser plugin', () => { const update = await bench({ remoteFailure: 'update' }) const updateActions = (update.entry()!.inject as unknown as () => TeamActionInjected)() await expect(updateActions.updateTask(SESSION, { - taskId: 'task-1' as TeamTaskId, expectedRevision: 1, action: 'delete', + taskId: 'task-1', expectedRevision: 1, action: 'delete', })).resolves.toEqual({ ok: false, error: 'offline (internal)', 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 1fdd77730c..ecee0d8fe3 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,7 +3,8 @@ 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 { TeamTaskId, TeamTaskView as TeamTask, TeamView } from '@deepseek-ai/dsh-team/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 { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts' import { @@ -14,9 +15,8 @@ import { zh } from '../src/client/locales.ts' afterEach(cleanup) const SESSION = 'lead' as SessionId -const tid = (value: string): TeamTaskId => value as TeamTaskId const task: TeamTask = { - id: tid('task-1'), + id: 'task-1', revision: 1, subject: 'Implement runtime', description: 'Build the Team runtime', @@ -31,7 +31,7 @@ const view: TeamView = { members: [ { id: SESSION, name: 'lead', role: 'lead', status: 'idle', model: 'model-a', diagnostics: [] }, { - id: 'worker-id' as SessionId, + id: 'worker-id', name: 'worker', role: 'teammate', status: 'inactive', @@ -53,7 +53,7 @@ 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: tid('task-2'), subject: 'New task' } }), + createTask: () => Promise.resolve({ ok: true, value: { ...task, id: 'task-2', subject: 'New task' } }), updateTask: () => Promise.resolve({ ok: true, value: { ...task, revision: 2 } }), openTeammate: () => Promise.resolve(), ...overrides, @@ -67,7 +67,7 @@ describe('TeamAction', () => { const nextView: TeamView = { ...view, members: [{ id: nextSession, name: 'lead', role: 'lead', status: 'idle', diagnostics: [] }], - tasks: [{ ...task, id: tid('task-next'), subject: 'Next session task' }], + tasks: [{ ...task, id: 'task-next', subject: 'Next session task' }], } const load = vi.fn((sessionId: SessionId) => sessionId === SESSION ? firstLoad.promise @@ -104,7 +104,7 @@ describe('TeamAction', () => { const newer = Promise.withResolvers>() const newestView = { ...view, - tasks: [{ ...task, id: tid('newest-task'), subject: 'Newest task' }], + tasks: [{ ...task, id: 'newest-task', subject: 'Newest task' }], } const load = vi.fn() .mockResolvedValueOnce({ ok: true, value: view }) @@ -297,6 +297,38 @@ describe('TeamAction', () => { expect(updateTask).toHaveBeenCalledTimes(1) }) + 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 }) + const first = render( Promise.resolve({ ok: false, error: 'stale task', conflict: true }), + }))} />) + 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(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 }) + const dependencyUpdate = vi.fn() + .mockResolvedValueOnce({ ok: true, value: { ...task, revision: 2, subject: 'Edited' } }) + .mockResolvedValueOnce({ ok: false, error: 'stale dependency', conflict: true }) + 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 reload failed')).toBeTruthy() + expect(screen.queryByText(zh.conflict)).toBeNull() + }) + it('renders roster/task state variants and contains navigation, refresh, and close actions', async () => { const { ownerName: _ownerName, ...unownedTask } = task const richView: TeamView = { @@ -305,14 +337,14 @@ describe('TeamAction', () => { view.members[0]!, { ...view.members[1]!, status: 'running' }, { - id: 'failed-id' as SessionId, + id: 'failed-id', name: 'failed-worker', role: 'teammate', status: 'failed', diagnostics: ['provider failed'], }, { - id: 'provisioning-id' as SessionId, + id: 'provisioning-id', name: 'provisioning-worker', role: 'teammate', status: 'provisioning', @@ -320,9 +352,9 @@ describe('TeamAction', () => { }, ], tasks: [ - { ...unownedTask, id: tid('ready-task'), status: 'pending', ready: true }, - { ...unownedTask, id: tid('blocked-task'), status: 'pending', ready: false }, - { ...task, id: tid('completed-task'), status: 'completed' }, + { ...unownedTask, id: 'ready-task', status: 'pending', ready: true }, + { ...unownedTask, id: 'blocked-task', status: 'pending', ready: false }, + { ...task, id: 'completed-task', status: 'completed' }, ], } const load = vi.fn(() => Promise.resolve({ ok: true as const, value: richView })) @@ -378,7 +410,7 @@ 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: tid('late-task') } }) + pending.resolve({ ok: true, value: { ...task, id: 'late-task' } }) await Promise.resolve() expect(screen.queryByText('Late task')).toBeNull() }) @@ -467,7 +499,7 @@ describe('TeamAction', () => { }) it('skips the dependency mutation when an edit keeps the same blockers', async () => { - const blockedTask: TeamTask = { ...task, blockedBy: [tid('task-0')] } + const blockedTask: TeamTask = { ...task, blockedBy: ['task-0'] } const updateTask = vi.fn().mockResolvedValue({ ok: true, value: { ...blockedTask, revision: 2, subject: 'Same dependencies' }, diff --git a/packages/experimental/client-ui-agent-team/tsconfig.json b/packages/experimental/client-ui-agent-team/tsconfig.json index 3df58f7e8c..32c7d772f8 100644 --- a/packages/experimental/client-ui-agent-team/tsconfig.json +++ b/packages/experimental/client-ui-agent-team/tsconfig.json @@ -7,14 +7,13 @@ "include": ["src"], "references": [ { "path": "../../../vendor/cordis" }, - { "path": "../agent-team-remotes/tsconfig.client.json" }, + { "path": "../agent-team-remotes" }, { "path": "../../api/remotes/tsconfig.client.json" }, { "path": "../../client/locale" }, { "path": "../../client/runtime" }, { "path": "../../client/ui-conversation" }, { "path": "../../client/ui-primitives" }, { "path": "../../client/ui-slots" }, - { "path": "../team" }, { "path": "../../typert/protocol" }, { "path": "../../runtime-diagnostics/invariants" } ] diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index a9cd414aee..f7ef10c848 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -528,6 +528,14 @@ const SERVICE_ROLES: ServiceRole[] = [ 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.', + }, { key: 'jobs', pkg: 'jobs', diff --git a/tsconfig.client.json b/tsconfig.client.json index f17d67226c..cdba4c13a6 100644 --- a/tsconfig.client.json +++ b/tsconfig.client.json @@ -76,7 +76,6 @@ { "path": "./packages/client/ui-skill" }, { "path": "./packages/client/ui-reference" }, { "path": "./packages/client/ui-subagent" }, - { "path": "./packages/experimental/agent-team-remotes/tsconfig.client.json" }, { "path": "./packages/experimental/client-ui-agent-team" }, { "path": "./packages/client/ui-jobs" }, { "path": "./packages/client/ui-directory-picker-browse" }, diff --git a/vitest.config.ts b/vitest.config.ts index ef3a853735..584562420c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -268,8 +268,6 @@ export default defineConfig({ // only in lib; the post-build built-bin smoke executes both entries. 'packages/api/remotes/src/index.ts', 'packages/api/remotes/src/client/index.ts', - 'packages/experimental/agent-team-remotes/src/index.ts', - 'packages/experimental/agent-team-remotes/src/client/index.ts', // Slash/command/input round: per-file gaps deferred with the same // client-lane debt. TODO(gui): cover and remove with the lane above. 'packages/client/connection/src/client/fixture.ts',