Merge pull request #2555 from deepseek-harness/codex/agentteams-web

feat(team): add experimental Agent Teams Web profile
This commit is contained in:
Dudu-0223
2026-08-26 11:15:48 +08:00
committed by GitHub
76 changed files with 3421 additions and 39 deletions
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md
2026-08-18-experimental-agent-teams-packages.md: 495922d57bd88a78f0ca6b61367ef49c820db632
2026-08-18-experimental-agent-teams-packages.zh.md: 65aed57e223d8fa9b8ff97e2fcc109057b51474d
2026-08-18-experimental-agent-teams-packages.md: b769791d52472290f76eec7e518f24001db919ad
2026-08-18-experimental-agent-teams-packages.zh.md: 25227770b5029b1baedef6bd9b7ad78b79586365
@@ -12,13 +12,13 @@ An experimental directory without a current package previously imposed placement
## Decision
`packages/experimental/agent-team`, `packages/experimental/tool-agent-team`, and `packages/experimental/agent-team-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 Agent Teams profile bundle depends on the Team packages and applies after `dsh-base`. It inserts the Team rows, disables the global continuable-child controls whose model-visible names overlap the Team tools, and leaves 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.
@@ -12,13 +12,13 @@ Agent Teams 的服务与工具约定仍在变化,但它需要使用真实 Sess
## 决策
`packages/experimental/agent-team``packages/experimental/tool-agent-team``packages/experimental/agent-team-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 服务沿允许的方向消费这些能力。
私有 Agent Teams profile bundle 依赖 Team 包,并应用`dsh-base` 之后。它插入 Team 配置行,禁用模型可见名称与 Team 工具重叠的全局 continuable-child control,并保持已发布 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 的解析结果。
@@ -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/feature/2026-08-06-agent-teams-web.md
2026-08-06-agent-teams-web.md: 5002146f7db67f7199102b286a85da0148864fa9
2026-08-06-agent-teams-web.zh.md: d05f22141811f35ba52a92a02c8153b024ef8d8d
@@ -0,0 +1,43 @@
# Agent Note: Experimental Agent Teams Web controls
Status: implemented
English | [中文](2026-08-06-agent-teams-web.zh.md)
## Problem
The durable Agent Teams runtime owns roster, mailbox, and task state but exposes only model tools and Host service methods. Web users need to inspect teammate activity, manage shared tasks with the same compare-and-set rules, and open a teammate conversation. Agent Teams is still experimental, so these capabilities must not add Team-specific contracts or dependencies to the stable API Proxy, Session Controller, Client UI packages, or Web bundle.
## Decision
The private `ctx.agentTeams` service owns generated `agentTeams/view`, `agentTeams/createTask`, and `agentTeams/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. Create and update rejections cross Remote as closed business results; stale update revisions preserve `team-task-conflict`, while other Team rejections preserve `team-rejected`. Unexpected failures remain ordinary `RemoteResult` failures.
`@deepseek-ai/dsh-experimental-client-ui-agent-team` mounts the `@deepseek-ai/dsh-experimental-agent-team/remote` contribution through the stable `ctx.remote` service, then consumes the generated `ctx.remote.agentTeams` 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 update sends the displayed revision. Each create or update owns an independent pending token, invalidates older refreshes before starting, and reloads the complete Team view after success. A conflict asks the user to review only after its reload succeeds; a reload failure remains visible. Overlapping refreshes publish only the latest request for the selected Session.
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-experimental-agent-team-web-profile` inserts only the UI after the stable Web bundle. It is applied alongside the Host-side `@deepseek-ai/dsh-experimental-agent-team-profile`, which already inserts `ctx.agentTeams` and the model tools. Neither stable bundle contains disabled Team rows or dependencies.
Stable Web presets still register continuable Subagent controls inside their preset scope. Top-level Agent Teams profile overrides cannot replace those registrations, so this experimental composition may expose both the Team roster and legacy child controls. A Team-aware Web preset is deferred; the [Web profile README](../../../../packages/experimental/agent-team-web-profile/README.md#known-limitations-and-deferred-work) owns the current limitation.
## Boundaries
The Web UI has no mailbox timeline, worktree or Git controls, teammate creation, rename, deletion, interruption, or automatic merge behavior. It does not infer filesystem authority from task ownership or write scopes. A human continuation after teammate navigation is an ordinary addressed-child prompt, not a Team mailbox message.
## Alternatives considered
**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.agentTeams`; 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
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, independent pending operations, complete-board reloads, successful and failed conflict reloads, stale async results, navigation, disposal, and status or error presentation. A Web end-to-end test asserts that its overlay equals both shipped experimental profile layers, then exercises the real Host Remote flow.
## Consequences
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, Session Controller, Client UI packages, and Web bundle remain Team-agnostic. Source-checkout users must add two ordered experimental profile layers to a Web profile. Promotion renames the experimental npm packages but does not require a new generated namespace.
@@ -0,0 +1,43 @@
# Agent Note:实验性 Agent Teams Web 控件
状态:已实现
[English](2026-08-06-agent-teams-web.md) | 中文
## 问题
持久 Agent Teams runtime 负责 roster、mailbox 与 task 状态,但只提供模型工具和 Host service method。Web 用户需要查看 teammate 活动、按同样的 compare-and-set 规则管理共享任务,并打开 teammate 会话。Agent Teams 仍处于实验阶段,因此这些能力不能向稳定 API Proxy、Session Controller、Client UI package 或 Web bundle 增加 Team 专用 contract 或依赖。
## 决策
私有 `ctx.agentTeams` service 除 domain operation 外,还直接负责生成式 `agentTeams/view``agentTeams/createTask``agentTeams/updateTask` Remote method。Team package 负责浏览器安全的 view 与 mutation-result type。View 包含 roster 与当前 task 状态,但不包含 pending mailbox 内容或已删除 task tombstone。Create 与 update rejection 通过封闭 business result 跨越 Remote;过期的 update revision 保留为 `team-task-conflict`,其他 Team rejection 保留为 `team-rejected`。意外 failure 仍是普通 `RemoteResult` failure。
`@deepseek-ai/dsh-experimental-client-ui-agent-team` 通过稳定 `ctx.remote` service 挂载 `@deepseek-ai/dsh-experimental-agent-team/remote` contribution,随后直接消费生成式 `ctx.remote.agentTeams` method,不增加 Client result 包装层。它展示 roster status、model 与 diagnostics,并支持 task create、edit、dependency update、assignment、completion、reopen 与 deletion。每次 update 都发送当前显示的 revision。每个 create 或 update 都独立持有 pending token,在开始前使更早的 refresh 失效,并在成功后重新读取完整 Team view。Conflict 仅在其 reload 成功后要求用户检查;如果重新读取失败,则保留该错误。重叠 refresh 只发布所选 Session 的最新请求。
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-experimental-agent-team-web-profile` 在稳定 Web bundle 之后只插入 UI。它与 Host 侧 `@deepseek-ai/dsh-experimental-agent-team-profile` 一起应用,后者已经插入 `ctx.agentTeams` 与模型工具。两个稳定 bundle 都不包含禁用的 Team row 或依赖。
稳定 Web preset 仍会在自身 preset scope 内注册 continuable Subagent control。顶层 Agent Teams profile override 无法替换这些 registration,因此该实验性 composition 可能同时暴露 Team roster 与 legacy child control。Team-aware Web preset 暂缓实现;[Web profile README](../../../../packages/experimental/agent-team-web-profile/README.zh.md#known-limitations-and-deferred-work)负责记录当前限制。
## 边界
Web UI 不提供 mailbox timeline、worktree 或 Git control、teammate creation、rename、deletion、interrupt 或自动 merge。它不会从 task ownership 或 write scope 推断文件系统权限。导航到 teammate 后的人类 continuation 是普通 addressed-child prompt,不是 Team mailbox message。
## 考虑过的替代方案
**扩展 legacy API Proxy Team RPC map。** 拒绝,因为这会把实验性 domain 放入稳定 wire package,并重复生成式 Remote vocabulary 与 validation。
**引入独立的浏览器 Remote service。** 拒绝,因为这些 method 没有区别于 `ctx.agentTeams` 的状态、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 的一部分。
## 测试
Team service 单元测试、生成流程与 plain-Node built-artifact smoke 校验直接 Remote method、error mapping 与导出 descriptor。Client typecheck 与浏览器 component test 覆盖挂载 namespace、Lead routing、原始生成式 result、所有 task action、独立 pending operation、完整 task board reload、成功及失败的 conflict reload、陈旧 async result、navigation、dispose 与状态或错误呈现。Web 端到端测试先断言 overlay 等于两个随附实验性 profile 层,再运行真实 Host Remote flow。
## 后果
Team service 是 domain state 与公开选定 Team value 的 Remote operation 的唯一 Cordis owner。稳定 API Proxy、Session Controller、Client UI package 和 Web bundle 保持 Team 无关。源码 checkout 用户必须向 Web profile 添加两个有序 experimental profile 层。Promotion 会重命名实验性 npm package,但不要求新的生成式 namespace。
+109
View File
@@ -0,0 +1,109 @@
// Keyless assembled-browser coverage for the private Agent Teams Web profiles
// over the real Host Typert Remote flow.
import { fileURLToPath } from 'node:url'
import { join } from 'node:path'
import { readFileSync } from 'node:fs'
import type { Browser, Page } from 'playwright'
import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import * as yaml from 'js-yaml'
import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
import { createMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
import {
assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/agent-team-panel', import.meta.url))
const PANEL_EXPECTED = join(SNAPSHOT_DIR, 'task.expected.md')
const OVERLAY = fileURLToPath(new URL('./agent-team-panel.overlay.yml', import.meta.url))
const HOST_PATCH = fileURLToPath(new URL('../../../packages/experimental/agent-team-profile/cordis.patch.yml', import.meta.url))
const WEB_PATCH = fileURLToPath(new URL('../../../packages/experimental/agent-team-web-profile/cordis.patch.yml', import.meta.url))
const INSTALL_ANCHORS = [
fileURLToPath(new URL('../../../packages/experimental/agent-team-profile/package.json', import.meta.url)),
fileURLToPath(new URL('../../../packages/experimental/agent-team-web-profile/package.json', import.meta.url)),
]
const MODE = webSnapshotMode()
function profileEntries(path: string): unknown[] {
const parsed = yaml.load(readFileSync(path, 'utf8'), { schema: entryListSchema })
if (!Array.isArray(parsed)) throw new Error(`profile layer at ${path} must be a list`)
return parsed
}
describe('Agent Teams panel overlay', () => {
it('matches the shipped Host and Web profile layers', () => {
expect(profileEntries(OVERLAY)).toEqual([
...profileEntries(HOST_PATCH),
...profileEntries(WEB_PATCH),
])
})
})
describe('web e2e: Agent Teams panel', () => {
let scaffold: WebScaffold
let browser: Browser
let page: Page
let tripwire: ReturnType<typeof watchConsole>
beforeAll(async () => {
scaffold = await launchWebScaffold({ extraOverlayPath: OVERLAY, extraInstallAnchors: INSTALL_ANCHORS })
browser = await chromium.launch()
page = await newEnglishPage(browser)
tripwire = watchConsole(page)
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
await connectFreshWorkspace(page, scaffold.workspaceCwd)
const agent = scaffold.ctx.agents.list()[0]
if (agent === undefined) throw new Error('connected Team workspace did not create an Agent')
agent.session.append('turn/start', { turn: 1 })
agent.session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'Open the Agent Team controls.' }],
source: { kind: 'user' },
}), { surfaceOp: 'append' })
agent.session.append('step/start', { turn: 1, step: 1 })
agent.session.append('assistant/message', {
turn: 1,
step: 1,
message: createMessage({
role: 'assistant',
content: [{ type: 'text', text: 'Ready.' }],
source: { kind: 'model', provider: 'fixture', model: 'fixture' },
}),
}, { surfaceOp: 'append' })
agent.session.append('step/end', { turn: 1, step: 1 })
agent.session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
await scaffold.ctx.sessions.flush(agent.session)
await page.getByText('Ready.').waitFor({ timeout: 10_000 })
}, 120_000)
afterAll(async () => {
await browser?.close()
await scaffold?.close()
})
it('loads the roster and creates one shared task through generated Remote', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-team-panel'))
const action = page.locator('[data-team-action]')
await action.getByRole('button', { name: /Agent Team/iu }).click()
await action.getByText('No shared tasks yet').waitFor()
await action.getByText('lead').waitFor()
await action.getByRole('button', { name: 'New task' }).click()
await action.getByPlaceholder('Task subject').fill('Browser task')
await action.getByPlaceholder('Task description').fill('Created through the assembled browser')
await action.getByPlaceholder(/Write scopes/iu).fill('src/web')
await action.getByRole('button', { name: 'Save' }).click()
await action.getByText('Browser task').waitFor()
const snapshot = await captureStableAria(page, '[data-team-action]', scaffold.workspaceCwd)
await compareOrRefreshGolden(PANEL_EXPECTED, snapshot, MODE)
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
}, 60_000)
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
await assertFixtureInventory(SNAPSHOT_DIR, ['task.expected.md'])
})
})
@@ -0,0 +1,42 @@
# Equivalent to the two explicit Agent Teams profile layers over the scaffold's
# stable base and Web bundles.
- id: tool-subagent-control
disabled: true
- id: tool-subagent-list-agents
disabled: true
- id: tool-subagent-report
disabled: true
- id: tool-subagent
config:
provider: spawn
toolName: subagent
backgroundMode: one-shot
- id: tool-subagent-fork
config:
provider: fork
toolName: subagent_fork
backgroundMode: one-shot
- insert:
- id: agent-team
name: '@deepseek-ai/dsh-experimental-agent-team'
config:
maxMembers: 8
maxTasks: 256
maxPendingMessagesPerMember: 64
maxMessageBytes: 65536
disposalTimeoutMs: 5000
- id: tool-agent-team
name: '@deepseek-ai/dsh-experimental-tool-agent-team'
config:
freshProvider: spawn
forkProvider: fork
- insert:
- id: ui-agent-team
name: '@deepseek-ai/dsh-experimental-client-ui-agent-team'
+34 -4
View File
@@ -54,6 +54,7 @@ import {
composeEntries,
healProfilesModuleFallback,
loadOverlayPatches,
type Profile,
} from '@deepseek-ai/dsh-app-boot'
import { dshHomePath } from '@deepseek-ai/dsh-home-paths'
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
@@ -237,6 +238,11 @@ export interface LaunchOptions {
* ordering.
*/
extraOverlayPath?: string
/**
* Additional source-checkout package manifests whose dependency closures
* supply private profile layers named by {@link extraOverlayPath}.
*/
extraInstallAnchors?: string[]
/**
* Replay fixture (session.jsonl) served by the inserted dsh-llm-replay row
* in replay/refresh modes; ignored in record mode (the real adapter
@@ -569,11 +575,35 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
let replayHandle: ReplayHandle | undefined
try {
process.chdir(workspaceCwd)
// The production module-resolution setup: an empty profile root inside the temp
// harness home, with bare plugin names resolving through the flat module
// fallback the launcher heals under <home>/profiles.
await healProfilesModuleFallback({ installAnchor: INSTALL_ANCHOR, home: harnessHome })
const profileDir = join(harnessHome, 'profiles', 'scaffold')
const extraLayers: Profile['layers'] = await Promise.all((options.extraInstallAnchors ?? []).map(async (anchor) => {
const manifest = JSON.parse(await readFile(anchor, 'utf8')) as { name?: unknown }
if (typeof manifest.name !== 'string' || manifest.name === '') {
throw new Error(`web scaffold extra install anchor has no package name: ${anchor}`)
}
const packageDir = dirname(anchor)
return {
packageName: manifest.name,
packageDir,
patchPath: join(packageDir, 'cordis.patch.yml'),
patches: [],
}
}))
// Mirror the production launcher: the shared installation closure keeps
// its carrier-specific fallback, while private bundle dependencies stay
// isolated to this synthetic scaffold profile.
await healProfilesModuleFallback({
installAnchor: INSTALL_ANCHOR,
home: harnessHome,
profile: {
name: 'scaffold',
dir: profileDir,
layers: extraLayers,
patchPath: join(profileDir, 'cordis.patch.yml'),
patches: [],
patchReload: 'startup',
},
})
await mkdir(profileDir, { recursive: true })
const rootConfig = join(profileDir, 'cordis.yml')
await writeFile(rootConfig, '[]\n')
@@ -0,0 +1,29 @@
- button "Agent Team" [expanded]:
- img
- text: Agent Team
- dialog "Agent Team":
- strong: Agent Team
- button "Refresh Team":
- img
- button "Close":
- img
- heading "Members" [level=3]
- 'button "lead Idle · Model: deepseek-v4-flash" [disabled]'
- heading "Shared tasks" [level=3]
- button "New task":
- img
- text: New task
- article:
- strong: Browser task
- text: Pending
- paragraph: Created through the assembled browser
- text: "task-1 Ready Write scopes: src/web Owner"
- combobox "Owner":
- option "Unowned" [selected]
- option "lead"
- button "Edit":
- img
- text: Edit
- button "Delete":
- img
- text: Delete
+1
View File
@@ -82,6 +82,7 @@
"tests/shipped-composition.e2e.ts",
"tests/schedule-after.e2e.ts",
"tests/feedback-command.e2e.ts",
"tests/agent-team-panel.e2e.ts",
"tests/startup-auto-selection.e2e.ts",
"tests/produced-files.e2e.ts",
"tests/produced-file-mentions.e2e.ts",
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/capability-seams.md
capability-seams.md: d86a05e240cd64a0c8a172b00fe9e1df809fe408
capability-seams.zh.md: 1050191d696fd5a46aa28101beef118b07c29ed2
capability-seams.md: 2ee2d1b989c4eb25b245cfd528b2fa1ba3bb0272
capability-seams.zh.md: 7ecda568ef4df4d360414ee872d2b0241ebe1dce
+3 -1
View File
@@ -172,6 +172,7 @@ flowchart LR
pkg_experimental_agent_team["experimental-agent-team"]
svc_agentTeams["ctx.agentTeams<br/>Agent Teams coordination domain"]
pkg_experimental_tool_agent_team["experimental-tool-agent-team"]
pkg_experimental_client_ui_agent_team["experimental-client-ui-agent-team"]
pkg_jobs["jobs"]
svc_jobs["ctx.jobs<br/>Background job registry"]
pkg_jobs_local["jobs-local"]
@@ -324,6 +325,7 @@ flowchart LR
svc_agentDefaultModel --> pkg_headless
svc_agentDefaultModel --> pkg_host_apiproxy
svc_agentLoop --> pkg_agent_spine_demo
svc_agentTeams --> pkg_experimental_client_ui_agent_team
svc_agentTeams --> pkg_experimental_tool_agent_team
svc_agents --> pkg_acp
svc_agents --> pkg_agent_loop
@@ -500,7 +502,7 @@ flowchart LR
| `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox), [`fs-e2b`](../packages/e2b/fs-e2b) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | tool-fs executes read/write/edit through ctx.fs; fs-sandbox fences mutations by the shared sandbox mode; fs-observation-policy contributes observed-state checks through the fs/* event gate. |
| `ctx.compaction` | `seam` | [`compaction`](../packages/compaction/compaction) | [`compaction-basic`](../packages/compaction/compaction-basic) | [`compaction-basic`](../packages/compaction/compaction-basic) | - | The basic backend consumes post-step pressure and request-error recovery events; there is no model-facing compact tool. |
| `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process), [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code), [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`tool-ralph`](../packages/workflow/tool-ralph) | - | Providers implement transports; the service also owns optional Activation-based continuation orchestration, tool-subagent selects one-shot or continuable delegation, tool-subagent-control delivers follow-ups, and tool-ralph requires one fresh structured-output route. |
| `ctx.agentTeams` | `core` | [`experimental-agent-team`](../packages/experimental/agent-team) | - | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | - | 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. |
| `ctx.agentTeams` | `core` | [`experimental-agent-team`](../packages/experimental/agent-team) | - | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team), [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | - | 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. |
| `ctx.jobs` | `seam` | [`jobs`](../packages/jobs/jobs) | [`jobs-local`](../packages/jobs/jobs-local) | [`tool-bash`](../packages/shell/tool-bash), [`tool-terminal`](../packages/terminal/tool-terminal), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-jobs`](../packages/jobs/tool-jobs) | - | Producers (background bash, PTY sends, and subagent delegations) register running work; tool-jobs is the model-facing controller that reads, lists, and kills it; jobs-local is the process-local registry. |
| `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-fetch-http`](../packages/web/web-fetch-http) | [`tool-web`](../packages/web/tool-web) | - | Search and fetch providers register into one ctx.web seam; tool-web owns the stable model-facing names. |
| `ctx.spillStore` | `seam` | [`spill`](../packages/spill/spill) | [`spill-local`](../packages/spill/spill-local) | [`spill-policy`](../packages/spill/spill-policy) | - | The backend saves oversized tool text and returns a model-facing locator plus retrieval hint; spill-policy is the tools/post-execute consumer that decides when to spill. |
+3 -1
View File
@@ -174,6 +174,7 @@ flowchart LR
pkg_experimental_agent_team["experimental-agent-team"]
svc_agentTeams["ctx.agentTeams<br/>Agent Teams coordination domain"]
pkg_experimental_tool_agent_team["experimental-tool-agent-team"]
pkg_experimental_client_ui_agent_team["experimental-client-ui-agent-team"]
pkg_jobs["jobs"]
svc_jobs["ctx.jobs<br/>Background job registry"]
pkg_jobs_local["jobs-local"]
@@ -326,6 +327,7 @@ flowchart LR
svc_agentDefaultModel --> pkg_headless
svc_agentDefaultModel --> pkg_host_apiproxy
svc_agentLoop --> pkg_agent_spine_demo
svc_agentTeams --> pkg_experimental_client_ui_agent_team
svc_agentTeams --> pkg_experimental_tool_agent_team
svc_agents --> pkg_acp
svc_agents --> pkg_agent_loop
@@ -502,7 +504,7 @@ flowchart LR
| `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox), [`fs-e2b`](../packages/e2b/fs-e2b) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | tool-fs 通过 ctx.fs 执行读取/写入/编辑;fs-sandbox 按共享沙箱模式限制变更;fs-observation-policy 通过 fs/* 事件门禁贡献基于观测状态的检查。 |
| `ctx.compaction` | `seam` | [`compaction`](../packages/compaction/compaction) | [`compaction-basic`](../packages/compaction/compaction-basic) | [`compaction-basic`](../packages/compaction/compaction-basic) | - | 基础后端消费步骤后的压力事件和请求错误恢复事件;不存在面向模型的压缩工具。 |
| `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process), [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code), [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`tool-ralph`](../packages/workflow/tool-ralph) | - | 提供方实现传输;该服务还负责可选的、基于 Activation 的延续编排,tool-subagent 选择一次性或可延续委派,tool-subagent-control 传递后续消息,而 tool-ralph 要求一条全新的结构化输出路由。 |
| `ctx.agentTeams` | `core` | [`experimental-agent-team`](../packages/experimental/agent-team) | - | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | - | 负责隐式 Root roster、持久 peer mailbox、共享任务 DAGcontinuable child 生命周期tool-agent-team 提供作用域化模型策略和控制工具。 |
| `ctx.agentTeams` | `core` | [`experimental-agent-team`](../packages/experimental/agent-team) | - | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team), [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | - | 负责隐式 Root roster、持久 peer mailbox、共享任务 DAGcontinuable child 生命周期与生成式 Team Remote methodtool-agent-team 提供模型控制工具,client-ui-agent-team 挂载浏览器 contribution。 |
| `ctx.jobs` | `seam` | [`jobs`](../packages/jobs/jobs) | [`jobs-local`](../packages/jobs/jobs-local) | [`tool-bash`](../packages/shell/tool-bash), [`tool-terminal`](../packages/terminal/tool-terminal), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-jobs`](../packages/jobs/tool-jobs) | - | 生产方(后台 bash、PTY 发送和 subagent 委派)登记正在运行的工作;tool-jobs 是面向模型的控制器,用于读取、列出和终止这些工作;jobs-local 是进程本地注册表。 |
| `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-fetch-http`](../packages/web/web-fetch-http) | [`tool-web`](../packages/web/tool-web) | - | 搜索和抓取提供方注册到同一个 ctx.web seamtool-web 负责稳定的面向模型名称。 |
| `ctx.spillStore` | `seam` | [`spill`](../packages/spill/spill) | [`spill-local`](../packages/spill/spill-local) | [`spill-policy`](../packages/spill/spill-policy) | - | 后端保存过大的工具文本,并返回面向模型的定位信息和取回提示;spill-policy 是 tools/post-execute 消费方,负责决定何时 spill。 |
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/config-catalog.md
config-catalog.md: e4d60a02367bb35965564ee88dcb4065c97a5cf4
config-catalog.zh.md: 9dc302705780297a4dd177174613ad3346516e2a
config-catalog.md: aaadee78a5de4a42fc28566215b9fff70a5caca4
config-catalog.zh.md: 393857b50cb7e750eb90fc6e3a605559a5ce2cad
+3 -1
View File
@@ -594,7 +594,7 @@ export interface Config {
}
```
Source: [`packages/experimental/agent-team/src/types.ts:125`](../packages/experimental/agent-team/src/types.ts)
Source: [`packages/experimental/agent-team/src/types.ts:131`](../packages/experimental/agent-team/src/types.ts)
<a id="deepseek-aidsh-experimental-tool-agent-team"></a>
@@ -3365,6 +3365,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
- `@deepseek-ai/dsh-commands` ([`packages/interaction/commands/src/index.ts`](../packages/interaction/commands/src/index.ts))
- `@deepseek-ai/dsh-cordis-client-runner` ([`packages/extensions/cordis-client-runner/src/index.ts`](../packages/extensions/cordis-client-runner/src/index.ts))
- `@deepseek-ai/dsh-deepseek-llm-api-extensions` ([`packages/llm/deepseek-llm-api-extensions/src/index.ts`](../packages/llm/deepseek-llm-api-extensions/src/index.ts))
- `@deepseek-ai/dsh-experimental-client-ui-agent-team` ([`packages/experimental/client-ui-agent-team/src/index.ts`](../packages/experimental/client-ui-agent-team/src/index.ts))
- `@deepseek-ai/dsh-fs-e2b` — requires `e2b` ([`packages/e2b/fs-e2b/src/index.ts`](../packages/e2b/fs-e2b/src/index.ts))
- `@deepseek-ai/dsh-fs-observation-policy` ([`packages/fs/fs-observation-policy/src/index.ts`](../packages/fs/fs-observation-policy/src/index.ts))
- `@deepseek-ai/dsh-goal-round-driver` — requires `agents` · `goals` · `sessions` ([`packages/goal/goal-round-driver/src/index.ts`](../packages/goal/goal-round-driver/src/index.ts))
@@ -3431,6 +3432,7 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them.
- `@deepseek-ai/dsh-cmdline` ([`packages/boot/cmdline/src/index.ts`](../packages/boot/cmdline/src/index.ts))
- `@deepseek-ai/dsh-code-runtime-python` ([`packages/code-runtime/code-runtime-python/src/index.ts`](../packages/code-runtime/code-runtime-python/src/index.ts))
- `@deepseek-ai/dsh-experimental-agent-team-profile` ([`packages/experimental/agent-team-profile/src/index.ts`](../packages/experimental/agent-team-profile/src/index.ts))
- `@deepseek-ai/dsh-experimental-agent-team-web-profile` ([`packages/experimental/agent-team-web-profile/src/index.ts`](../packages/experimental/agent-team-web-profile/src/index.ts))
- `@deepseek-ai/dsh-experimental-webworker-packer` ([`packages/experimental/webworker-packer/src/index.ts`](../packages/experimental/webworker-packer/src/index.ts))
- `@deepseek-ai/dsh-experimental-webworker-runtime` ([`packages/experimental/webworker-runtime/src/index.ts`](../packages/experimental/webworker-runtime/src/index.ts))
- `@deepseek-ai/dsh-home-paths` ([`packages/util/home-paths/src/index.ts`](../packages/util/home-paths/src/index.ts))
+2
View File
@@ -3367,6 +3367,7 @@ export interface Config {
- `@deepseek-ai/dsh-commands`[`packages/interaction/commands/src/index.ts`](../packages/interaction/commands/src/index.ts)
- `@deepseek-ai/dsh-cordis-client-runner`[`packages/extensions/cordis-client-runner/src/index.ts`](../packages/extensions/cordis-client-runner/src/index.ts)
- `@deepseek-ai/dsh-deepseek-llm-api-extensions`[`packages/llm/deepseek-llm-api-extensions/src/index.ts`](../packages/llm/deepseek-llm-api-extensions/src/index.ts)
- `@deepseek-ai/dsh-experimental-client-ui-agent-team`[`packages/experimental/client-ui-agent-team/src/index.ts`](../packages/experimental/client-ui-agent-team/src/index.ts)
- `@deepseek-ai/dsh-fs-e2b` — 需要 `e2b`[`packages/e2b/fs-e2b/src/index.ts`](../packages/e2b/fs-e2b/src/index.ts)
- `@deepseek-ai/dsh-fs-observation-policy`[`packages/fs/fs-observation-policy/src/index.ts`](../packages/fs/fs-observation-policy/src/index.ts)
- `@deepseek-ai/dsh-goal-round-driver` — 需要 `agents` · `goals` · `sessions`[`packages/goal/goal-round-driver/src/index.ts`](../packages/goal/goal-round-driver/src/index.ts)
@@ -3432,6 +3433,7 @@ export interface Config {
- `@deepseek-ai/dsh-cmdline`[`packages/boot/cmdline/src/index.ts`](../packages/boot/cmdline/src/index.ts)
- `@deepseek-ai/dsh-code-runtime-python`[`packages/code-runtime/code-runtime-python/src/index.ts`](../packages/code-runtime/code-runtime-python/src/index.ts)
- `@deepseek-ai/dsh-experimental-agent-team-profile`[`packages/experimental/agent-team-profile/src/index.ts`](../packages/experimental/agent-team-profile/src/index.ts)
- `@deepseek-ai/dsh-experimental-agent-team-web-profile`[`packages/experimental/agent-team-web-profile/src/index.ts`](../packages/experimental/agent-team-web-profile/src/index.ts)
- `@deepseek-ai/dsh-experimental-webworker-packer`[`packages/experimental/webworker-packer/src/index.ts`](../packages/experimental/webworker-packer/src/index.ts)
- `@deepseek-ai/dsh-experimental-webworker-runtime`[`packages/experimental/webworker-runtime/src/index.ts`](../packages/experimental/webworker-runtime/src/index.ts)
- `@deepseek-ai/dsh-home-paths`[`packages/util/home-paths/src/index.ts`](../packages/util/home-paths/src/index.ts)
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/module-graph.md
module-graph.md: 3cafd70ae2cc982470d8594228764bf701ab687c
module-graph.zh.md: 0037321174ff91b59a3957d4bfa6f620af6dcc93
module-graph.md: e044939e4b48dee60faa03ff955c713e88ddaefa
module-graph.zh.md: d707bd63394b94f511ca40484e23e9e629521515
+19 -1
View File
@@ -206,6 +206,8 @@ flowchart TD
subgraph group_experimental["packages/experimental"]
pkg_experimental_agent_team["experimental-agent-team"]
pkg_experimental_agent_team_profile["experimental-agent-team-profile"]
pkg_experimental_agent_team_web_profile["experimental-agent-team-web-profile"]
pkg_experimental_client_ui_agent_team["experimental-client-ui-agent-team"]
pkg_experimental_tool_agent_team["experimental-tool-agent-team"]
pkg_experimental_webworker_packer["experimental-webworker-packer"]
pkg_experimental_webworker_runtime["experimental-webworker-runtime"]
@@ -378,6 +380,7 @@ flowchart TD
pkg_code_runtime_python --> pkg_invariants
pkg_e2b --> pkg_invariants
pkg_experimental_agent_team_profile --> pkg_invariants
pkg_experimental_agent_team_web_profile --> pkg_invariants
pkg_experimental_webworker_packer --> pkg_invariants
pkg_host_directory_picker --> pkg_invariants
pkg_host_directory_picker_browse --> pkg_invariants
@@ -1196,6 +1199,7 @@ flowchart TD
pkg_experimental_agent_team --> pkg_session
pkg_experimental_agent_team --> pkg_session_persistence
pkg_experimental_agent_team --> pkg_subagent
pkg_experimental_agent_team --> pkg_typert_protocol
pkg_host_frontend_static --> pkg_client_connection
pkg_host_frontend_static --> pkg_host_webserver
pkg_host_frontend_static --> pkg_invariants
@@ -1479,6 +1483,18 @@ flowchart TD
pkg_client_ui_user_questions --> pkg_session
pkg_client_ui_user_questions --> pkg_typert_protocol
pkg_client_ui_user_questions --> pkg_user_questions
pkg_experimental_client_ui_agent_team --> pkg_api_remotes
pkg_experimental_client_ui_agent_team --> pkg_api_session_controller
pkg_experimental_client_ui_agent_team --> pkg_client_locale
pkg_experimental_client_ui_agent_team --> pkg_client_ui_conversation
pkg_experimental_client_ui_agent_team --> pkg_client_ui_primitives
pkg_experimental_client_ui_agent_team --> pkg_client_ui_renderer
pkg_experimental_client_ui_agent_team --> pkg_client_ui_session
pkg_experimental_client_ui_agent_team --> pkg_client_ui_slots
pkg_experimental_client_ui_agent_team --> pkg_experimental_agent_team
pkg_experimental_client_ui_agent_team --> pkg_invariants
pkg_experimental_client_ui_agent_team --> pkg_session
pkg_experimental_client_ui_agent_team --> pkg_typert_protocol
pkg_client_ui_chat --> pkg_agent
pkg_client_ui_chat --> pkg_api_remotes
pkg_client_ui_chat --> pkg_api_session_controller
@@ -1684,6 +1700,7 @@ flowchart TD
| [`code-runtime-python`](../packages/code-runtime/code-runtime-python) | `code-runtime` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`e2b`](../packages/e2b/e2b) | `e2b` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`experimental-agent-team-profile`](../packages/experimental/agent-team-profile) | `experimental` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`experimental-agent-team-web-profile`](../packages/experimental/agent-team-web-profile) | `experimental` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`experimental-webworker-packer`](../packages/experimental/webworker-packer) | `experimental` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`host-directory-picker`](../packages/host/directory-picker) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) |
@@ -1848,7 +1865,7 @@ flowchart TD
| [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`hooks-claude-code`](../packages/hooks/hooks-claude-code) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`api-gateway`](../packages/api/gateway) | `api` | [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-registry`](../packages/typert/registry) |
| [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent) |
| [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) |
| [`host-frontend-static`](../packages/host/frontend-static) | `host` | [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
| [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
@@ -1885,6 +1902,7 @@ flowchart TD
| [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`attachment`](../packages/attachment/attachment), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
| [`client-ui-user-questions`](../packages/client/ui-user-questions) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol), [`user-questions`](../packages/interaction/user-questions) |
| [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`experimental-agent-team`](../packages/experimental/agent-team), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
| [`client-ui-chat`](../packages/client/ui-chat) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-locale`](../packages/client/locale), [`client-ui-approval`](../packages/client/ui-approval), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-workspace`](../packages/client/ui-workspace), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-stats`](../packages/session/session-stats), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`util-workspace-path`](../packages/util/workspace-path) |
| [`client-ui-commands`](../packages/client/ui-commands) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
| [`client-ui-reference`](../packages/client/ui-reference) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`session-reference`](../packages/context/session-reference), [`typert-protocol`](../packages/typert/protocol) |
+19 -1
View File
@@ -208,6 +208,8 @@ flowchart TD
subgraph group_experimental["packages/experimental"]
pkg_experimental_agent_team["experimental-agent-team"]
pkg_experimental_agent_team_profile["experimental-agent-team-profile"]
pkg_experimental_agent_team_web_profile["experimental-agent-team-web-profile"]
pkg_experimental_client_ui_agent_team["experimental-client-ui-agent-team"]
pkg_experimental_tool_agent_team["experimental-tool-agent-team"]
pkg_experimental_webworker_packer["experimental-webworker-packer"]
pkg_experimental_webworker_runtime["experimental-webworker-runtime"]
@@ -380,6 +382,7 @@ flowchart TD
pkg_code_runtime_python --> pkg_invariants
pkg_e2b --> pkg_invariants
pkg_experimental_agent_team_profile --> pkg_invariants
pkg_experimental_agent_team_web_profile --> pkg_invariants
pkg_experimental_webworker_packer --> pkg_invariants
pkg_host_directory_picker --> pkg_invariants
pkg_host_directory_picker_browse --> pkg_invariants
@@ -1198,6 +1201,7 @@ flowchart TD
pkg_experimental_agent_team --> pkg_session
pkg_experimental_agent_team --> pkg_session_persistence
pkg_experimental_agent_team --> pkg_subagent
pkg_experimental_agent_team --> pkg_typert_protocol
pkg_host_frontend_static --> pkg_client_connection
pkg_host_frontend_static --> pkg_host_webserver
pkg_host_frontend_static --> pkg_invariants
@@ -1481,6 +1485,18 @@ flowchart TD
pkg_client_ui_user_questions --> pkg_session
pkg_client_ui_user_questions --> pkg_typert_protocol
pkg_client_ui_user_questions --> pkg_user_questions
pkg_experimental_client_ui_agent_team --> pkg_api_remotes
pkg_experimental_client_ui_agent_team --> pkg_api_session_controller
pkg_experimental_client_ui_agent_team --> pkg_client_locale
pkg_experimental_client_ui_agent_team --> pkg_client_ui_conversation
pkg_experimental_client_ui_agent_team --> pkg_client_ui_primitives
pkg_experimental_client_ui_agent_team --> pkg_client_ui_renderer
pkg_experimental_client_ui_agent_team --> pkg_client_ui_session
pkg_experimental_client_ui_agent_team --> pkg_client_ui_slots
pkg_experimental_client_ui_agent_team --> pkg_experimental_agent_team
pkg_experimental_client_ui_agent_team --> pkg_invariants
pkg_experimental_client_ui_agent_team --> pkg_session
pkg_experimental_client_ui_agent_team --> pkg_typert_protocol
pkg_client_ui_chat --> pkg_agent
pkg_client_ui_chat --> pkg_api_remotes
pkg_client_ui_chat --> pkg_api_session_controller
@@ -1686,6 +1702,7 @@ flowchart TD
| [`code-runtime-python`](../packages/code-runtime/code-runtime-python) | `code-runtime` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`e2b`](../packages/e2b/e2b) | `e2b` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`experimental-agent-team-profile`](../packages/experimental/agent-team-profile) | `experimental` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`experimental-agent-team-web-profile`](../packages/experimental/agent-team-web-profile) | `experimental` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`experimental-webworker-packer`](../packages/experimental/webworker-packer) | `experimental` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`host-directory-picker`](../packages/host/directory-picker) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) |
@@ -1850,7 +1867,7 @@ flowchart TD
| [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`hooks-claude-code`](../packages/hooks/hooks-claude-code) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`api-gateway`](../packages/api/gateway) | `api` | [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-registry`](../packages/typert/registry) |
| [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent) |
| [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) |
| [`host-frontend-static`](../packages/host/frontend-static) | `host` | [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
| [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
@@ -1887,6 +1904,7 @@ flowchart TD
| [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`attachment`](../packages/attachment/attachment), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
| [`client-ui-user-questions`](../packages/client/ui-user-questions) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol), [`user-questions`](../packages/interaction/user-questions) |
| [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`experimental-agent-team`](../packages/experimental/agent-team), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
| [`client-ui-chat`](../packages/client/ui-chat) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-locale`](../packages/client/locale), [`client-ui-approval`](../packages/client/ui-approval), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-workspace`](../packages/client/ui-workspace), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-stats`](../packages/session/session-stats), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`util-workspace-path`](../packages/util/workspace-path) |
| [`client-ui-commands`](../packages/client/ui-commands) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
| [`client-ui-reference`](../packages/client/ui-reference) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`session-reference`](../packages/context/session-reference), [`typert-protocol`](../packages/typert/protocol) |
+1 -1
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/persistence-catalog.md
persistence-catalog.md: 1ff60ebdacf198ce7bc7d7c8c13eb9278320281a
persistence-catalog.md: 593fb073022e139a13f1d642c6f12a94400b0446
persistence-catalog.zh.md: 59acfa23c81687f23498d598cb0aead3e669ceb2
+4 -4
View File
@@ -767,7 +767,7 @@ Source: [`packages/subagent/tool-subagent/src/model-selection-state.ts:13`](../p
Types: [TeamId](subsystems/agent-team.md) · [TeamMemberSnapshot](subsystems/agent-team.md)
Source: [`packages/experimental/agent-team/src/types.ts:206`](../packages/experimental/agent-team/src/types.ts)
Source: [`packages/experimental/agent-team/src/types.ts:223`](../packages/experimental/agent-team/src/types.ts)
<a id="teammessagedelivered--log-only"></a>
@@ -785,7 +785,7 @@ Source: [`packages/experimental/agent-team/src/types.ts:206`](../packages/experi
Types: [TeamId](subsystems/agent-team.md) · [TeamMessageId](subsystems/agent-team.md)
Source: [`packages/experimental/agent-team/src/types.ts:212`](../packages/experimental/agent-team/src/types.ts)
Source: [`packages/experimental/agent-team/src/types.ts:229`](../packages/experimental/agent-team/src/types.ts)
<a id="teammessagequeued--log-only"></a>
@@ -798,7 +798,7 @@ Source: [`packages/experimental/agent-team/src/types.ts:212`](../packages/experi
Types: [TeamId](subsystems/agent-team.md) · [TeamMessageSnapshot](subsystems/agent-team.md)
Source: [`packages/experimental/agent-team/src/types.ts:210`](../packages/experimental/agent-team/src/types.ts)
Source: [`packages/experimental/agent-team/src/types.ts:227`](../packages/experimental/agent-team/src/types.ts)
<a id="teamtask--log-only"></a>
@@ -811,7 +811,7 @@ Source: [`packages/experimental/agent-team/src/types.ts:210`](../packages/experi
Types: [TeamId](subsystems/agent-team.md) · [TeamTaskSnapshot](subsystems/agent-team.md)
Source: [`packages/experimental/agent-team/src/types.ts:208`](../packages/experimental/agent-team/src/types.ts)
Source: [`packages/experimental/agent-team/src/types.ts:225`](../packages/experimental/agent-team/src/types.ts)
### `todo/*`
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/agent-team.md
agent-team.md: 8704bff3071d4e084e95dafcd61b7eea65278f61
agent-team.zh.md: dfa55743e5ddbde054f87180edd37a788af84663
agent-team.md: e3a746734b067bb7ef8f083c5c9af9cb3707dcf7
agent-team.zh.md: 1f885a73861c091b50d27d15080ebc09f6d382ec
+23
View File
@@ -175,6 +175,29 @@ interrupt(caller: Agent, targetName: string): { previousStatus: 'running' | 'idl
* @returns Team membership, or undefined for non-Team subagents and stale identities.
*/
tryMembership(agent: Agent): TeamMembership | undefined
/**
* 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
/**
* 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 or a typed Team rejection.
*/
@Remote('createTask') remoteCreateTask(agent: Agent, request: CreateTeamTaskRequest): Promise<TeamTaskMutationResult>
/**
* 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') remoteUpdateTask(agent: Agent, request: UpdateTeamTaskRequest): Promise<TeamTaskMutationResult>
```
Types: [Agent](core.md)
+23
View File
@@ -175,6 +175,29 @@ interrupt(caller: Agent, targetName: string): { previousStatus: 'running' | 'idl
* @returns Team membership, or undefined for non-Team subagents and stale identities.
*/
tryMembership(agent: Agent): TeamMembership | undefined
/**
* 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
/**
* 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 or a typed Team rejection.
*/
@Remote('createTask') remoteCreateTask(agent: Agent, request: CreateTeamTaskRequest): Promise<TeamTaskMutationResult>
/**
* 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') remoteUpdateTask(agent: Agent, request: UpdateTeamTaskRequest): Promise<TeamTaskMutationResult>
```
Types: [Agent](core.zh.md)
+11
View File
@@ -756,6 +756,17 @@
"@deepseek-ai/.+"
]
},
"packages/experimental/agent-team": {
"entry": [
"tests/**/*.spec.ts",
"tests/**/*.e2e.ts"
]
},
"packages/experimental/agent-team-web-profile": {
"ignoreDependencies": [
"@deepseek-ai/dsh-experimental-client-ui-agent-team"
]
},
"packages/bundle/web-app": {
"ignoreDependencies": [
"@deepseek-ai/.+"
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/experimental/README.md
README.md: ad658da84303d93ddede7d383d7950d8bca3f38f
README.zh.md: a8b777b40e44c1d3b95c744a5c972a2dca7192fb
README.md: 2812388377611ebbed0f415d637bd14bffcee34c
README.zh.md: 3dcf13d9ef73b540fe4b8f3d7f6df2e623bca6f4
+2
View File
@@ -26,6 +26,8 @@ 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-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` |
| [`webworker-packer`](webworker-packer/README.md) | Builds the gzip-compressed VFS image consumed by the browser worker preview | library and CLI — no ctx key |
| [`webworker-runtime`](webworker-runtime/README.md) | Runs the harness plugin tree inside a dedicated browser worker | library and worker entry — no ctx key |
+2
View File
@@ -26,6 +26,8 @@ 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-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` |
| [`webworker-packer`](webworker-packer/README.zh.md) | 构建浏览器 worker 预览所消费的 gzip 压缩 VFS 镜像 | 库与 CLI,不使用 ctx key |
| [`webworker-runtime`](webworker-runtime/README.zh.md) | 在专用浏览器 worker 中运行 harness 插件树 | 库与 worker 入口,不使用 ctx key |
@@ -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 packages/experimental/agent-team-web-profile/README.md
README.md: 8b7c4985a9322c256e156e51f9692e360ccdbe27
README.zh.md: bbf92766bfbb391ffd1cf98a859c7329f48fb49a
@@ -0,0 +1,98 @@
---
description: "Add the experimental Agent Teams panel to a source-checkout Web profile after the Host Team layer."
kind: "package-bundle"
---
# @deepseek-ai/dsh-experimental-agent-team-web-profile
English | [中文](README.zh.md)
## Summary
`dsh-experimental-agent-team-web-profile` is the private Web layer for [Agent Teams](../agent-team/README.md). Add it after `@deepseek-ai/dsh-web-app` and [`@deepseek-ai/dsh-experimental-agent-team-profile`](../agent-team-profile/README.md) to show the Team roster, task board, and teammate navigation in the browser. Removing either experimental layer leaves the stable base and Web composition unchanged. Official releases exclude this package, so it is available only from a source checkout.
## Table of Contents
- [Use this package](#use-this-package)
- [Understand the implementation](#understand-the-implementation)
- [Further Exploration](#further-exploration)
- [Model Experience](#model-experience)
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
- [Dev Note](#dev-note)
-----
<a id="use-this-package"></a>
## Use this package
### Install into a profile
From this repository checkout, add the Host and Web Agent Teams layers to an initialized `web` profile in this order:
```sh
pnpm dsh plugin --profile web add ./packages/experimental/agent-team-profile
pnpm dsh plugin --profile web add ./packages/experimental/agent-team-web-profile
```
The first command supplies the Team domain, generated Remote methods, and model tools. The second command activates this package's declared patch and its browser presentation. Removing the package with `dsh plugin --profile web remove @deepseek-ai/dsh-experimental-agent-team-web-profile` removes the Web layer from the profile's ordered bundle list.
### What you get
The conversation header gains the Team roster, shared task board, and teammate navigation. [`@deepseek-ai/dsh-experimental-client-ui-agent-team`](../client-ui-agent-team/README.md) owns those browser interactions and mounts the generated Client Remote namespace used to reach the Host Team service.
-----
<a id="understand-the-implementation"></a>
## Understand the implementation
<details>
<summary>Implementation internals — click to expand</summary>
The package's runtime content is [`cordis.patch.yml`](cordis.patch.yml). Applied after `dsh-web-app` and the Host Agent Teams layer, its single `insert` entry adds the `ui-agent-team` row for `@deepseek-ai/dsh-experimental-client-ui-agent-team`. The inserted Client plugin owns the generated Remote assembly and Team UI; this static bundle holds no mutable state and installs no runtime invariant.
| File | Role |
|---|---|
| [`cordis.patch.yml`](cordis.patch.yml) | Ordered Web patch containing the `ui-agent-team` row |
| [`src/index.ts`](src/index.ts) | Empty module entry; the patch is the runtime content |
| [`src/invariant.ts`](src/invariant.ts) | Empty invariant companion for the static bundle |
</details>
-----
<a id="further-exploration"></a>
## Further Exploration
- [Experimental packages](../README.md) — incubation status and release exclusion.
- [Agent Teams Host profile](../agent-team-profile/README.md) — the required domain, Remote, and model-tool layer.
- [Agent Teams browser UI](../client-ui-agent-team/README.md) — roster, task-board, and teammate-navigation behavior.
- [Web bundle](../../bundle/web-app/README.md) — the stable browser layer this patch extends.
-----
<a id="model-experience"></a>
## Model Experience
Indirectly, through the Host-side Agent Teams profile selected alongside this Web layer.
#### KV Cache effect
This Web bundle adds no model request content; the Host-side Team tools own prompt, schema, and cache effects.
## Known Limitations and Deferred Work
<a id="known-limitations-and-deferred-work"></a>
- **Ordered composition**`dsh-base`, `dsh-web-app`, `dsh-experimental-agent-team-profile`, and this package must remain in that order.
- **Preset-scoped legacy controls** — stable Web presets still mount continuable Subagent controls inside the preset scope. Top-level Host profile overrides do not replace those scoped registrations, so the Team roster and legacy child controls can both appear until Web has a Team-aware preset. The [Web Agent Teams decision](../../../.agents/notes/implemented/feature/2026-08-06-agent-teams-web.md) records this deferred composition work.
- **Source-checkout only** — official CLI, Web, npm, and Python release payloads exclude this private package.
<a id="dev-note"></a>
### Dev Note
<details>
<summary>Working context for maintainers — click to expand</summary>
None.
</details>
@@ -0,0 +1,98 @@
---
description: "在 Host Team 层之后,为源码 checkout 的 Web profile 添加实验性 Agent Teams 面板。"
kind: "package-bundle"
---
# @deepseek-ai/dsh-experimental-agent-team-web-profile
[English](README.md) | 中文
## 概述
`dsh-experimental-agent-team-web-profile` 是 [Agent Teams](../agent-team/README.zh.md) 的私有 Web 层。把它放在 `@deepseek-ai/dsh-web-app` 与 [`@deepseek-ai/dsh-experimental-agent-team-profile`](../agent-team-profile/README.zh.md) 之后,即可在浏览器中显示 Team roster、任务板与 teammate 导航。移除任一实验层都会让稳定的 base 与 Web composition 保持不变。正式发布会排除本包,因此只能从源码 checkout 使用。
## 目录
- [使用本包](#use-this-package)
- [理解实现](#understand-the-implementation)
- [进一步探索](#further-exploration)
- [模型体验](#model-experience)
- [已知限制与延期工作](#known-limitations-and-deferred-work)
- [开发备注](#dev-note)
-----
<a id="use-this-package"></a>
## 使用本包
### 安装到 profile
在本仓库 checkout 中,按以下顺序把 Host 与 Web Agent Teams 层添加到已初始化的 `web` profile
```sh
pnpm dsh plugin --profile web add ./packages/experimental/agent-team-profile
pnpm dsh plugin --profile web add ./packages/experimental/agent-team-web-profile
```
第一条命令提供 Team domain、生成的 Remote 方法与模型工具。第二条命令激活本包声明的 patch 及其浏览器 presentation。执行 `dsh plugin --profile web remove @deepseek-ai/dsh-experimental-agent-team-web-profile` 移除本包时,Web 层也会从 profile 的有序 bundle 列表中移除。
### 获得的功能
对话标题栏会获得 Team roster、共享任务板与 teammate 导航。[`@deepseek-ai/dsh-experimental-client-ui-agent-team`](../client-ui-agent-team/README.zh.md) 负责这些浏览器交互,并挂载用于访问 Host Team service 的生成 Client Remote namespace。
-----
<a id="understand-the-implementation"></a>
## 理解实现
<details>
<summary>实现细节——点击展开</summary>
本包的运行时内容是 [`cordis.patch.yml`](cordis.patch.yml)。在 `dsh-web-app` 与 Host Agent Teams 层之后应用时,它唯一的 `insert` 条目会为 `@deepseek-ai/dsh-experimental-client-ui-agent-team` 添加 `ui-agent-team` 行。插入的 Client 插件负责生成的 Remote assembly 与 Team UI;这个静态 bundle 不持有可变状态,也不安装运行时不变式。
| 文件 | 职责 |
|---|---|
| [`cordis.patch.yml`](cordis.patch.yml) | 包含 `ui-agent-team` 行的有序 Web patch |
| [`src/index.ts`](src/index.ts) | 空模块入口;patch 是运行时内容 |
| [`src/invariant.ts`](src/invariant.ts) | 静态 bundle 的空不变式伴生插件 |
</details>
-----
<a id="further-exploration"></a>
## 进一步探索
- [实验性包](../README.zh.md)——孵化状态与发布排除规则。
- [Agent Teams Host profile](../agent-team-profile/README.zh.md)——所需的 domain、Remote 与模型工具层。
- [Agent Teams 浏览器 UI](../client-ui-agent-team/README.zh.md)——roster、任务板与 teammate 导航行为。
- [Web bundle](../../bundle/web-app/README.zh.md)——本 patch 扩展的稳定浏览器层。
-----
<a id="model-experience"></a>
## 模型体验
通过与本 Web 层同时选择的 Host-side Agent Teams profile 间接产生影响。
#### KV Cache 影响
本 Web bundle 不添加任何模型请求内容;Host-side Team 工具负责提示词、schema 与缓存影响。
## 已知限制与延期工作
<a id="known-limitations-and-deferred-work"></a>
- **有序组合**——`dsh-base``dsh-web-app``dsh-experimental-agent-team-profile` 与本包必须保持这个顺序。
- **Preset-scoped 旧控制项**——稳定 Web preset 仍会在 preset scope 内挂载 continuable Subagent 控制项。顶层 Host profile override 不会替换这些 scoped registration,因此在 Web 获得 Team-aware preset 前,Team roster 与旧 child 控制项可能同时出现。[Web Agent Teams 决策](../../../.agents/notes/implemented/feature/2026-08-06-agent-teams-web.zh.md)记录了这项暂缓的 composition 工作。
- **仅限源码 checkout**——正式 CLI、Web、npm 与 Python 发布产物都不包含这个私有包。
<a id="dev-note"></a>
### 开发备注
<details>
<summary>维护者的工作上下文——点击展开</summary>
无。
</details>
@@ -0,0 +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 the Team service is present.
- insert:
- id: ui-agent-team
name: '@deepseek-ai/dsh-experimental-client-ui-agent-team'
@@ -0,0 +1,52 @@
{
"name": "@deepseek-ai/dsh-experimental-agent-team-web-profile",
"description": "Private Web profile layer for Agent Teams Remote and UI plugins",
"version": "0.1.1-rc.2",
"private": true,
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/experimental/agent-team-web-profile"
},
"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"
},
"./cordis.patch.yml": "./cordis.patch.yml",
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/invariant.js",
"cordis.patch.yml",
"lib/types/**/*.d.ts"
],
"license": "MIT",
"dsh": {
"bundle": {
"patch": "./cordis.patch.yml"
}
},
"dependencies": {
"@deepseek-ai/dsh-experimental-client-ui-agent-team": "workspace:^"
},
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/cordis-plugin-include": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/cordis": "workspace:^",
"js-yaml": "^4.2.0"
}
}
@@ -0,0 +1,3 @@
/** Private Web profile layer for the Agent Teams Client plugins. */
export {}
@@ -0,0 +1,23 @@
/** Package-owned invariant companion for the Agent Teams Web profile. */
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-experimental-agent-team-web-profile'
/** Cordis companion plugin name. */
export const name = 'agent-team-web-profile-invariant'
/** Service required before the companion can register. */
export const inject = ['invariants']
// No runtime invariant: the package carries only a static profile patch. The
// Remote assembly and Team UI own their activation requirements.
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))
@@ -0,0 +1,50 @@
/** The experimental Web bundle must carry one parseable Team Client layer. */
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
import * as yaml from 'js-yaml'
import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
import * as WebProfileInvariant from '../src/invariant.ts'
describe('Agent Teams Web profile bundle', () => {
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
publishConfig?: unknown
dependencies?: Record<string, string>
dsh?: { bundle?: { patch?: string } }
}
expect(manifest.private).toBe(true)
expect(manifest.publishConfig).toBeUndefined()
expect(manifest.dsh?.bundle?.patch).toBe('./cordis.patch.yml')
expect(manifest.dependencies).toEqual({
'@deepseek-ai/dsh-experimental-client-ui-agent-team': 'workspace:^',
})
const parsed = yaml.load(
readFileSync(resolve(root, manifest.dsh!.bundle!.patch!), 'utf8'),
{ schema: entryListSchema },
) as { insert?: { id?: string; name?: string }[] }[]
expect(parsed.flatMap(patch => patch.insert ?? [])).toEqual([
{ id: 'ui-agent-team', name: '@deepseek-ai/dsh-experimental-client-ui-agent-team' },
])
})
it('reserves package ownership without installing a runtime audit', async () => {
const ctx = new Context()
await ctx.plugin(InvariantRegistry, { enabled: true })
const fiber = ctx.plugin(WebProfileInvariant)
await fiber.await()
expect(WebProfileInvariant.name).toBe('agent-team-web-profile-invariant')
expect(WebProfileInvariant.inject).toEqual(['invariants'])
expect(() => {
Reflect.apply(ctx.emit.bind(ctx), undefined, ['unrelated/event'])
}).not.toThrow()
await fiber.dispose()
})
})
@@ -0,0 +1,12 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": ["src"],
"references": [
{ "path": "../../../vendor/cordis" },
{ "path": "../../runtime-diagnostics/invariants" }
]
}
@@ -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/README.md
README.md: ca088a4cf30a20e8db092be76f5ada1c76ba26d0
README.zh.md: ec5f7e1f46238b4b0b3398d4ab35585fdae15bf0
README.md: bac73d873ec0856fa2b0550a7f25331c5d7035fb
README.zh.md: 2981e611a48736124dda79a8721e01c11fbcf7e6
@@ -164,6 +164,11 @@ Read these pages when the package-level contract is not enough. They move from t
-----
<a id="model-experience"></a>
### Browser Remote
`TeamService` owns the generated `agentTeams/view`, `agentTeams/createTask`, and `agentTeams/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`; create and update rejections remain explicit domain results inside a successful transport response, with stale update revisions distinguished as task conflicts.
## Model Experience
### Peer messages
@@ -164,6 +164,11 @@ dispose 会关闭准入、中止并等待已获准的创建与 mailbox dispatch
-----
<a id="model-experience"></a>
### 浏览器 Remote
`TeamService` 除了 roster、mailbox、task 与 lifecycle operation,还直接负责生成式 `agentTeams/view``agentTeams/createTask``agentTeams/updateTask` Remote method。`./remote` 导出由 Web UI 挂载的 Client contribution`./client` 则重新导出可在浏览器 compilation face 中安全使用的 request、view 与 task mutation result type。Typert 在外层 `RemoteResult` 中保留 transport failurecreate 与 update rejection 则作为 transport 成功响应中的显式 domain result,其中过期的 update revision 会区分为 task conflict。
## 模型体验
### Peer 消息
+24 -1
View File
@@ -20,13 +20,34 @@
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
"./client": {
"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"
},
"files": [
"lib/index.js",
"lib/invariant.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"
],
"license": "MIT",
"dependencies": {
@@ -41,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": {
@@ -58,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:^"
}
}
@@ -0,0 +1,13 @@
/** Client-safe Agent Teams request, result, and view vocabulary. */
export type {
CreateTeamTaskRequest,
TeamMemberView,
TeamTaskAction,
TeamTaskId,
TeamTaskMutationResult,
TeamTaskStatus,
TeamTaskView,
TeamView,
UpdateTeamTaskRequest,
} from './types.ts'
+56 -2
View File
@@ -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<Config> = z.object({
@@ -222,6 +225,57 @@ 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 or a typed Team rejection.
*/
@Remote('createTask')
remoteCreateTask(agent: Agent, request: CreateTeamTaskRequest): Promise<TeamTaskMutationResult> {
return this.taskMutationResult(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')
remoteUpdateTask(agent: Agent, request: UpdateTeamTaskRequest): Promise<TeamTaskMutationResult> {
return this.taskMutationResult(this.updateTask(agent, request))
}
/** Preserve Team task rejections while allowing unexpected failures to reject the Remote call. */
private async taskMutationResult(operation: Promise<TeamTaskView>): Promise<TeamTaskMutationResult> {
try {
return { ok: true, value: await operation }
} 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(() => {
+19 -2
View File
@@ -1,8 +1,8 @@
/** Public Agent Teams identities, durable records, and service request values. */
import type { Branded } from '@deepseek-ai/dsh-brand'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { SessionId } from '@deepseek-ai/dsh-session'
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
/** Identifies the implicit team rooted at one top-level Session. */
export type TeamId = Branded<'TeamId'>
@@ -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
@@ -0,0 +1,69 @@
/** Plain-Node smoke for the built Agent Teams service and Remote contribution. */
import { execFile } from 'node:child_process'
import { existsSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { describe, expect, it } from 'vitest'
const packageDir = fileURLToPath(new URL('..', import.meta.url))
const root = resolve(packageDir, '../../..')
const artifact = (path: string): string => join(root, path)
const artifactUrl = (path: string): string => pathToFileURL(artifact(path)).href
const requiredArtifacts = [
'packages/experimental/agent-team/lib/index.js',
'packages/experimental/agent-team/lib/typert.remote-client.js',
].every(path => existsSync(artifact(path)))
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/lib/index.js'),
remote: artifactUrl('packages/experimental/agent-team/lib/typert.remote-client.js'),
}
const script = `
const host = await import(${JSON.stringify(urls.host)})
const remote = await import(${JSON.stringify(urls.remote)})
console.log(JSON.stringify({
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 {
className: string
methods: string[]
}
expect(output).toEqual({
className: 'TeamService',
methods: [
'@deepseek-ai/dsh-experimental-agent-team#agentTeams/createTask',
'@deepseek-ai/dsh-experimental-agent-team#agentTeams/updateTask',
'@deepseek-ai/dsh-experimental-agent-team#agentTeams/view',
],
})
})
})
function runPlainNode(script: string): Promise<{
readonly exitCode: number | null
readonly stdout: string
readonly stderr: string
}> {
return new Promise((resolveRun) => {
execFile(process.execPath, ['--input-type=module', '-e', script], {
cwd: packageDir,
encoding: 'utf8',
timeout: 30_000,
}, (error, stdout, stderr) => {
resolveRun({
exitCode: error === null ? 0 : typeof error.code === 'number' ? error.code : null,
stdout,
stderr,
})
})
})
}
@@ -837,6 +837,67 @@ 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.agentTeams.typertRemote).toMatchObject({ serviceKey: 'agentTeams', namespace: 'agentTeams' })
expect(ctx.agentTeams.remoteView(lead)).toEqual({
members: [expect.objectContaining({ name: 'lead', role: 'lead', status: 'idle' })],
tasks: [],
})
const createdResult = await ctx.agentTeams.remoteCreateTask(lead, {
subject: 'Remote task',
description: 'Created through the generated API',
blockedBy: [],
writeScopes: ['packages/experimental/agent-team'],
})
expect(createdResult).toMatchObject({ ok: true, value: { revision: 1 } })
if (!createdResult.ok) throw new Error('Remote task creation did not succeed')
const created = createdResult.value
await expect(ctx.agentTeams.remoteUpdateTask(lead, {
taskId: created.id,
expectedRevision: created.revision,
action: 'claim',
})).resolves.toMatchObject({
ok: true,
value: { id: created.id, revision: 2, ownerName: 'lead' },
})
expect(ctx.agentTeams.remoteView(lead).tasks).toHaveLength(1)
})
it('preserves Team task rejections and propagates unexpected failures', async () => {
const { ctx, lead } = await setup([])
const createRequest = {
subject: 'Remote task', description: 'Rejected task', blockedBy: [], writeScopes: [],
}
const request = { taskId: TeamTaskId('task-1'), expectedRevision: 1, action: 'delete' as const }
vi.spyOn(ctx.agentTeams, 'createTask')
.mockRejectedValueOnce(new TeamError('invalid task', 'TEAM_TASK_INVALID'))
.mockRejectedValueOnce(new Error('unexpected creation failure'))
vi.spyOn(ctx.agentTeams, '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.agentTeams.remoteCreateTask(lead, createRequest)).resolves.toEqual({
ok: false,
error: { code: 'team-rejected', message: 'invalid task' },
})
await expect(ctx.agentTeams.remoteCreateTask(lead, createRequest))
.rejects.toThrow('unexpected creation failure')
await expect(ctx.agentTeams.remoteUpdateTask(lead, request)).resolves.toEqual({
ok: false,
error: { code: 'team-task-conflict', message: 'stale' },
})
await expect(ctx.agentTeams.remoteUpdateTask(lead, request)).resolves.toEqual({
ok: false,
error: { code: 'team-rejected', message: 'denied' },
})
await expect(ctx.agentTeams.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([])
@@ -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" }
@@ -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 packages/experimental/client-ui-agent-team/README.md
README.md: 3980608d4fbe7539c454c874fd9334d323048be9
README.zh.md: 082e32d83ed0a936b06ff03f43c81aec918405a5
@@ -0,0 +1,96 @@
---
description: "Use and debug the experimental Web Agent Teams roster, shared task board, and teammate navigation panel."
kind: "package-reference"
---
# @deepseek-ai/dsh-experimental-client-ui-agent-team
English | [中文](README.zh.md)
## Summary
This package adds an Agent Teams action to the Web conversation header, where a user can inspect the current roster, manage the shared task board, and navigate into a teammate's conversation. It reads authoritative Team state through the generated `ctx.remote.agentTeams` contribution and keeps ordinary child-history navigation on the stable addressed-subagent path. Choose it for the experimental source-checkout Web profile; official releases exclude it. The browser projection does not extend the stable API Proxy, store Team state, or register model-facing input.
## Table of Contents
- [Use this package](#use-this-package)
- [Understand the implementation](#understand-the-implementation)
- [Further Exploration](#further-exploration)
- [Model Experience](#model-experience)
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
- [Dev Note](#dev-note)
-----
<a id="use-this-package"></a>
## Use this package
Install the package through [`@deepseek-ai/dsh-experimental-agent-team-web-profile`](../agent-team-web-profile/README.md) after the stable Web bundle and the Host-side Agent Teams profile. The Web Client loader mounts the `/client` export; the root Host export is inert, and the package has no user configuration fields.
### Inspect and navigate the roster
Opening the panel calls `agentTeams/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 address field.
### Manage the task board
The task board shows task identity, owner, blockers, readiness, advisory write scopes, and overlap warnings. A user can create, edit, assign or unassign, complete, reopen, and delete tasks through `agentTeams/createTask` and `agentTeams/updateTask`. Every update sends the displayed revision, and create or update rejections remain explicit business results.
-----
<a id="understand-the-implementation"></a>
## Understand the implementation
<details>
<summary>Implementation internals — click to expand</summary>
The Client export mounts the generated `ctx.remote.agentTeams` contribution from [`@deepseek-ai/dsh-experimental-agent-team/remote`](../agent-team/README.md), then registers its locale dictionaries and one conversation-header slot through Cordis effects. Disposing the plugin fiber removes both registrations.
Starting a create or update invalidates older refreshes. Success reloads the complete Team view so every task's derived fields stay current. A `team-task-conflict` result displays a stale-state notice only after that reload succeeds; a reload failure remains visible instead. Editing task text or scopes and changing dependencies use two sequential compare-and-set mutations because the Team service exposes them as separate actions.
| File | Role |
|---|---|
| [`src/client/mount.ts`](src/client/mount.ts) | Generated Remote, locale, navigation, and slot registrations |
| [`src/client/TeamAction.tsx`](src/client/TeamAction.tsx) | Roster and task-board interaction state |
| [`src/client/locales.ts`](src/client/locales.ts) | English and Chinese panel copy |
| [`src/index.ts`](src/index.ts) | Inert Host entry |
</details>
-----
<a id="further-exploration"></a>
## Further Exploration
- [Agent Teams Web profile](../agent-team-web-profile/README.md) — the source-checkout bundle that mounts this Client plugin.
- [Agent Teams service](../agent-team/README.md) — authoritative roster, task, and Remote behavior.
- [Conversation UI](../../client/ui-conversation/README.md) — the stable header slot and addressed-subagent navigation surface.
- [Experimental packages](../README.md) — incubation status and release exclusion.
-----
<a id="model-experience"></a>
## Model Experience
None, as this browser projection and task control surface registers no model-facing input.
#### KV Cache effect
No direct effect; the Team tools and ordinary conversation submission own any later model-visible use.
## Known Limitations and Deferred Work
<a id="known-limitations-and-deferred-work"></a>
- **Snapshot refresh** — the panel refreshes on open, explicit refresh, and mutations; it has no live event subscription or mailbox timeline.
- **Ordinary child continuation** — a human message sent after navigation uses the stable addressed-subagent prompt path, not the Team peer mailbox.
- **No lifecycle or workspace controls** — the panel cannot spawn, rename, delete, or interrupt teammates, and write scopes remain advisory metadata.
<a id="dev-note"></a>
### Dev Note
<details>
<summary>Working context for maintainers — click to expand</summary>
None.
</details>
@@ -0,0 +1,96 @@
---
description: "使用并排查实验性 Web Agent Teams roster、共享任务板与 teammate 导航面板。"
kind: "package-reference"
---
# @deepseek-ai/dsh-experimental-client-ui-agent-team
[English](README.md) | 中文
## 概述
本包向 Web 会话页头添加 Agent Teams action,让用户检查当前 roster、管理共享任务板并导航到 teammate 会话。它通过生成的 `ctx.remote.agentTeams` contribution 读取权威 Team 状态,并让普通 child history 导航继续使用稳定的 addressed-subagent 路径。需要实验性源码 checkout Web profile 时选择本包;正式发布会排除它。这个浏览器 projection 不扩展稳定 API Proxy、不存储 Team 状态,也不注册面向模型的输入。
## 目录
- [使用本包](#use-this-package)
- [理解实现](#understand-the-implementation)
- [进一步探索](#further-exploration)
- [模型体验](#model-experience)
- [已知限制与延期工作](#known-limitations-and-deferred-work)
- [开发备注](#dev-note)
-----
<a id="use-this-package"></a>
## 使用本包
在稳定 Web bundle 与 Host-side Agent Teams profile 之后,通过 [`@deepseek-ai/dsh-experimental-agent-team-web-profile`](../agent-team-web-profile/README.zh.md) 安装本包。Web Client loader 挂载 `/client` exportroot Host export 不执行行为,本包也没有用户配置字段。
### 检查并导航 roster
打开 panel 会调用 `agentTeams/view`。Roster row 展示持久 name、运行时 status、model 与 diagnostics。选择健康 teammate 时,系统刷新既有直接 child catalog,并打开普通的 `{ parentSessionId, childSessionId, mode: 'continuable' }` address。History 与后续人类 prompt 继续使用稳定 addressed-subagent 会话路径;本包不会添加 Team 专用 address 字段。
### 管理任务板
任务板展示 task identity、owner、blocker、readiness、提示性 write scope 与重叠 warning。用户可以通过 `agentTeams/createTask``agentTeams/updateTask` 创建、编辑、分配或取消分配、完成、重开和删除任务。每次 update 都发送当前显示的 revisioncreate 或 update rejection 都保留为显式 business result。
-----
<a id="understand-the-implementation"></a>
## 理解实现
<details>
<summary>实现细节——点击展开</summary>
Client export 挂载来自 [`@deepseek-ai/dsh-experimental-agent-team/remote`](../agent-team/README.zh.md) 的生成式 `ctx.remote.agentTeams` contribution,然后通过 Cordis effect 注册 locale dictionary 与一个 conversation-header slot。Dispose plugin fiber 会移除这两项 registration。
开始 create 或 update 会让更早的 refresh 失效。成功后会重新读取完整 Team view,使每个 task 的派生字段保持最新。`team-task-conflict` 结果仅在重新读取成功后显示状态陈旧提示;如果重新读取失败,则保留该错误。由于 Team service 把任务文本或 scope 编辑与 dependency 修改公开为独立 action,两者使用两个连续的 compare-and-set mutation。
| 文件 | 职责 |
|---|---|
| [`src/client/mount.ts`](src/client/mount.ts) | 生成式 Remote、locale、导航与 slot registration |
| [`src/client/TeamAction.tsx`](src/client/TeamAction.tsx) | Roster 与任务板交互状态 |
| [`src/client/locales.ts`](src/client/locales.ts) | 中英文 panel 文案 |
| [`src/index.ts`](src/index.ts) | 不执行行为的 Host entry |
</details>
-----
<a id="further-exploration"></a>
## 进一步探索
- [Agent Teams Web profile](../agent-team-web-profile/README.zh.md)——挂载本 Client plugin 的源码 checkout bundle。
- [Agent Teams service](../agent-team/README.zh.md)——权威 roster、task 与 Remote 行为。
- [会话 UI](../../client/ui-conversation/README.zh.md)——稳定 header slot 与 addressed-subagent 导航表层。
- [实验性包](../README.zh.md)——孵化状态与发布排除规则。
-----
<a id="model-experience"></a>
## 模型体验
无直接影响,因为该浏览器 projection 与任务控制界面不注册面向模型的输入。
#### KV Cache 影响
无直接影响;Team 工具与普通会话提交负责后续任何模型可见用途。
## 已知限制与延期工作
<a id="known-limitations-and-deferred-work"></a>
- **Snapshot refresh**——panel 会在打开、显式 refresh 与 mutation 后刷新;它没有实时 event subscription 或 mailbox timeline。
- **普通 child continuation**——导航后发送的人类消息使用稳定 addressed-subagent prompt 路径,而不是 Team peer mailbox。
- **没有 lifecycle 或 workspace control**——panel 不能 spawn、rename、delete 或 interrupt teammatewrite scope 仍只是提示性 metadata。
<a id="dev-note"></a>
### 开发备注
<details>
<summary>维护者的工作上下文——点击展开</summary>
无。
</details>
@@ -0,0 +1,90 @@
{
"name": "@deepseek-ai/dsh-experimental-client-ui-agent-team",
"description": "Web Agent Teams roster, task board, and teammate navigation",
"version": "0.1.1-rc.2",
"private": true,
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/experimental/client-ui-agent-team"
},
"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"
},
"./client": {
"types": "./lib/types/client/index.d.ts",
"default": "./lib/client.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"dsh": {
"client": {
"inject": [
"@deepseek-ai/dsh-api-remotes",
"@deepseek-ai/dsh-api-session-controller",
"@deepseek-ai/dsh-client-locale",
"@deepseek-ai/dsh-client-ui-conversation",
"@deepseek-ai/dsh-client-ui-primitives"
],
"platform": "web"
}
},
"scripts": {
"bundle": "tsdown",
"watch": "tsdown --watch"
},
"license": "MIT",
"dependencies": {
"react": "^18.2.0"
},
"peerDependencies": {
"@deepseek-ai/dsh-api-remotes": "workspace:^",
"@deepseek-ai/dsh-api-session-controller": "workspace:^",
"@deepseek-ai/dsh-client-locale": "workspace:^",
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
"@deepseek-ai/dsh-client-ui-renderer": "workspace:^",
"@deepseek-ai/dsh-client-ui-session": "workspace:^",
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
"@deepseek-ai/dsh-experimental-agent-team": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-typert-protocol": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-api-remotes": "workspace:^",
"@deepseek-ai/dsh-api-session-controller": "workspace:^",
"@deepseek-ai/dsh-client-locale": "workspace:^",
"@deepseek-ai/dsh-client-test-runtime": "workspace:^",
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
"@deepseek-ai/dsh-client-ui-renderer": "workspace:^",
"@deepseek-ai/dsh-client-ui-session": "workspace:^",
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
"@deepseek-ai/dsh-experimental-agent-team": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-typert-protocol": "workspace:^",
"@testing-library/react": "^16.1.0",
"@types/react": "~18.3.1",
"@deepseek-ai/cordis": "workspace:^",
"react-dom": "^18.2.0"
},
"files": [
"lib/index.js",
"lib/invariant.js",
"lib/client.js",
"lib/types/**/*.d.ts"
]
}
@@ -0,0 +1,239 @@
.root {
position: relative;
}
.trigger,
.iconButton,
.smallButton,
.taskActions button,
.formActions button {
border: 0;
border-radius: 6px;
background: transparent;
color: var(--dsw-alias-label-secondary);
cursor: pointer;
}
.trigger {
display: inline-flex;
align-items: center;
gap: 5px;
min-height: 28px;
padding: 3px 7px;
font-size: 12px;
}
.trigger:hover,
.iconButton:hover,
.smallButton:hover,
.taskActions button:hover,
.formActions button:hover {
background: var(--dsw-alias-interactive-bg-hover);
}
.count {
min-width: 16px;
border-radius: 8px;
background: var(--dsw-alias-fill-l2);
font-variant-numeric: tabular-nums;
text-align: center;
}
.panel {
--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
position: absolute;
top: calc(100% + 5px);
left: 0;
z-index: 110;
box-sizing: border-box;
width: min(560px, calc(100vw - 32px));
max-height: min(680px, calc(100vh - 120px));
padding: 10px;
overflow: auto;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 12px;
background: var(--dsw-specific-menu);
box-shadow: var(--dsw-shadow-lv3);
}
.toolbar,
.sectionTitle,
.taskTitle,
.taskActions,
.formActions {
display: flex;
align-items: center;
gap: 8px;
}
.toolbar {
min-height: 28px;
}
.spacer {
flex: 1;
}
.iconButton {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
}
.panel h3 {
margin: 12px 0 6px;
color: var(--dsw-alias-label-secondary);
font-size: 12px;
font-weight: 500;
}
.roster {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 6px;
}
.member {
display: flex;
align-items: flex-start;
gap: 8px;
min-width: 0;
padding: 8px;
border: 1px solid var(--dsw-alias-border-l1);
border-radius: 8px;
background: transparent;
color: var(--dsw-alias-label-primary);
text-align: left;
cursor: pointer;
}
.member:disabled {
cursor: default;
}
.memberText {
display: flex;
flex-direction: column;
min-width: 0;
}
.memberText small,
.meta {
color: var(--dsw-alias-label-tertiary);
font-size: 11px;
}
.diagnostic,
.error,
.warning {
color: var(--dsw-alias-state-error-primary);
}
.smallButton {
display: inline-flex;
align-items: center;
gap: 4px;
margin-left: auto;
padding: 4px 7px;
font-size: 11px;
}
.tasks {
display: flex;
flex-direction: column;
gap: 7px;
}
.task,
.form {
padding: 9px;
border: 1px solid var(--dsw-alias-border-l1);
border-radius: 9px;
background: var(--dsw-alias-bg-base);
}
.taskTitle span {
margin-left: auto;
color: var(--dsw-alias-label-tertiary);
font-size: 11px;
}
.task p {
margin: 5px 0;
color: var(--dsw-alias-label-secondary);
font-size: 12px;
white-space: pre-wrap;
}
.meta {
display: flex;
flex-wrap: wrap;
gap: 4px 10px;
}
.taskActions {
flex-wrap: wrap;
margin-top: 8px;
}
.taskActions label {
display: inline-flex;
align-items: center;
gap: 4px;
color: var(--dsw-alias-label-tertiary);
font-size: 11px;
}
.taskActions select,
.form input,
.form textarea {
box-sizing: border-box;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 6px;
background: var(--dsw-alias-bg-base);
color: var(--dsw-alias-label-primary);
font: inherit;
}
.taskActions button,
.formActions button {
display: inline-flex;
align-items: center;
gap: 3px;
padding: 4px 6px;
font-size: 11px;
}
.taskActions button:disabled,
.formActions button:disabled {
opacity: 0.45;
cursor: default;
}
.form {
display: grid;
gap: 6px;
margin-bottom: 7px;
}
.form input,
.form textarea {
width: 100%;
padding: 6px 8px;
font-size: 12px;
}
.form textarea {
min-height: 58px;
resize: vertical;
}
.notice,
.error {
padding: 9px;
font-size: 12px;
}
@@ -0,0 +1,421 @@
import { useCallback, useEffect, useRef, useState, type ChangeEvent } from 'react'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type {
TeamMemberView as TeamRosterMember,
TeamTaskAction,
TeamTaskId,
TeamTaskMutationResult,
TeamTaskView as TeamTask,
TeamView,
} from '@deepseek-ai/dsh-experimental-agent-team/client'
import type { RemoteFailure, RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
import {
IconCheckOutline14, IconCloseOutline16, IconEditOutline16, IconPlusOutline16,
IconRefreshOutline14, IconTrashOutline16, IconUserOutline16, StateDot,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
import { NS, type TeamKey } from './locales.ts'
import css from './TeamAction.module.css'
/** Generated Remote result consumed directly by the Team UI. */
export type TeamActionResult<T> = RemoteResult<T>
/** Generated Remote result whose business value preserves Team task rejections. */
export type TeamTaskActionResult = RemoteResult<TeamTaskMutationResult>
/** Business actions injected by the browser plugin. */
export interface TeamActionInjected {
load: (sessionId: SessionId) => Promise<TeamActionResult<TeamView>>
createTask: (sessionId: SessionId, input: {
subject: string
description: string
blockedBy: TeamTaskId[]
writeScopes: string[]
}) => Promise<TeamTaskActionResult>
updateTask: (sessionId: SessionId, input: {
taskId: TeamTaskId
expectedRevision: number
action: TeamTaskAction
subject?: string
description?: string
blockedBy?: TeamTaskId[]
writeScopes?: string[]
owner?: string
}) => Promise<TeamTaskActionResult>
openTeammate: (sessionId: SessionId, member: TeamRosterMember) => Promise<void>
}
/** Full props of the Team conversation-header action. */
export type TeamActionProps =
PropsRuntime<'conversation.session.header.actions'> & TeamActionInjected & PropsLocale<typeof NS>
interface Draft {
subject: string
description: string
blockers: string
scopes: string
}
const EMPTY_DRAFT: Draft = { subject: '', description: '', blockers: '', scopes: '' }
function items(value: string): string[] {
return [...new Set(value.split(',').map(item => item.trim()).filter(Boolean))]
}
function taskIds(value: string): TeamTaskId[] {
return items(value) as TeamTaskId[]
}
function failureText(error: Pick<RemoteFailure, 'code' | 'message'>): string {
return `${error.message} (${error.code})`
}
function statusKey(status: TeamTask['status']): TeamKey {
switch (status) {
case 'pending': return 'status.pending'
case 'in_progress': return 'status.in_progress'
case 'completed': return 'status.completed'
/* v8 ignore next -- Team views omit deleted task tombstones. */
case 'deleted': return 'status.completed'
}
}
function memberStatusKey(status: TeamRosterMember['status']): TeamKey {
switch (status) {
case 'running': return 'memberStatus.running'
case 'idle': return 'memberStatus.idle'
case 'inactive': return 'memberStatus.inactive'
case 'provisioning': return 'memberStatus.provisioning'
case 'failed': return 'memberStatus.failed'
}
}
/** Render the live Team roster and compare-and-set task board. */
export function TeamAction({
sessionId, load, createTask, updateTask, openTeammate, t,
}: TeamActionProps) {
const [open, setOpen] = useState(false)
const [loading, setLoading] = useState(false)
const [view, setView] = useState<TeamView | null>(null)
const [error, setError] = useState<string | null>(null)
const [creating, setCreating] = useState(false)
const [createDraft, setCreateDraft] = useState<Draft>(EMPTY_DRAFT)
const [editing, setEditing] = useState<string | null>(null)
const [editDraft, setEditDraft] = useState<Draft>(EMPTY_DRAFT)
const [pendingTasks, setPendingTasks] = useState<ReadonlySet<string>>(() => new Set())
const sessionRef = useRef(sessionId)
const refreshGeneration = useRef(0)
sessionRef.current = sessionId
useEffect(() => {
refreshGeneration.current += 1
setOpen(false)
setLoading(false)
setView(null)
setError(null)
setCreating(false)
setCreateDraft(EMPTY_DRAFT)
setEditing(null)
setEditDraft(EMPTY_DRAFT)
setPendingTasks(new Set())
}, [sessionId])
const refresh = useCallback(async (): Promise<boolean> => {
const requestedSession = sessionId
const generation = ++refreshGeneration.current
setLoading(true)
const result = await load(requestedSession)
if (sessionRef.current !== requestedSession || refreshGeneration.current !== generation) return false
setLoading(false)
if (result.ok) {
setView(result.value)
setError(null)
return true
} else {
setError(failureText(result.error))
return false
}
}, [load, sessionId])
const invalidateRefresh = useCallback((): void => {
refreshGeneration.current += 1
setLoading(false)
}, [])
const settleTask = useCallback(async (
taskId: string,
operation: () => Promise<TeamTaskActionResult>,
): Promise<TeamTask | undefined> => {
const requestedSession = sessionId
invalidateRefresh()
setPendingTasks(current => new Set(current).add(taskId))
try {
const result = await operation()
if (sessionRef.current !== requestedSession) return undefined
if (!result.ok) {
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(failureText(result.value.error))
}
return undefined
}
const task = result.value.value
setError(null)
await refresh()
if (sessionRef.current !== requestedSession) return undefined
return task
} finally {
if (sessionRef.current === requestedSession) {
setPendingTasks((current) => {
const next = new Set(current)
next.delete(taskId)
return next
})
}
}
}, [invalidateRefresh, refresh, sessionId, t])
const submitCreate = async (): Promise<void> => {
const subject = createDraft.subject.trim()
const description = createDraft.description.trim()
/* v8 ignore next -- TaskForm disables Save while either normalized field is empty. */
if (subject === '' || description === '') return
const created = await settleTask('create', () => createTask(sessionId, {
subject,
description,
blockedBy: taskIds(createDraft.blockers),
writeScopes: items(createDraft.scopes),
}))
if (created === undefined) return
setCreateDraft(EMPTY_DRAFT)
setCreating(false)
}
const startEdit = (task: TeamTask): void => {
setEditing(task.id)
setEditDraft({
subject: task.subject,
description: task.description,
blockers: task.blockedBy.join(', '),
scopes: task.writeScopes.join(', '),
})
}
const submitEdit = async (task: TeamTask): Promise<void> => {
const requestedSession = sessionId
const edited = await settleTask(task.id, () => updateTask(requestedSession, {
taskId: task.id,
expectedRevision: task.revision,
action: 'edit',
subject: editDraft.subject.trim(),
description: editDraft.description.trim(),
writeScopes: items(editDraft.scopes),
}))
if (edited === undefined) return
const blockedBy = taskIds(editDraft.blockers)
if (blockedBy.length === edited.blockedBy.length
&& blockedBy.every((blocker, index) => blocker === edited.blockedBy[index])) {
setEditing(null)
return
}
const dependencyTask = await settleTask(task.id, () => updateTask(requestedSession, {
taskId: task.id,
expectedRevision: edited.revision,
action: 'set_dependencies',
blockedBy,
}))
if (dependencyTask === undefined) return
setEditing(null)
}
const teammates = view?.members.filter(member => member.role === 'teammate') ?? []
const assignable = view?.members.filter(member => member.status !== 'failed' && member.status !== 'provisioning') ?? []
return (
<div className={css.root} data-team-action>
<button
type="button"
className={css.trigger}
aria-expanded={open}
onClick={() => {
const next = !open
setOpen(next)
if (next) void refresh()
}}
>
<IconUserOutline16 size={14} />
<span>{t('trigger')}</span>
{teammates.length > 0 && <span className={css.count}>{teammates.length}</span>}
</button>
{open && (
<div className={css.panel} role="dialog" aria-label={t('trigger')}>
<div className={css.toolbar}>
<strong>{t('trigger')}</strong>
<span className={css.spacer} />
<button type="button" className={css.iconButton} aria-label={t('refresh')} onClick={() => { void refresh() }}>
<IconRefreshOutline14 />
</button>
<button type="button" className={css.iconButton} aria-label={t('close')} onClick={() => { setOpen(false) }}>
<IconCloseOutline16 size={14} />
</button>
</div>
{error !== null && <div className={css.error} role="alert">{error}</div>}
{loading && view === null && <div className={css.notice}>{t('loading')}</div>}
{view !== null && (
<>
<section>
<h3>{t('roster')}</h3>
<div className={css.roster}>
{view.members.map(member => (
<button
key={member.id}
type="button"
className={css.member}
disabled={member.role === 'lead' || member.status === 'failed' || member.status === 'provisioning'}
title={member.role === 'teammate' ? t('open') : undefined}
onClick={() => {
void openTeammate(sessionId, member).catch((reason: unknown) => { setError(String(reason)) })
}}
>
<StateDot state={member.status === 'running' ? 'ongoing' : member.status === 'failed' ? 'error' : 'done'} />
<span className={css.memberText}>
<span>{member.name}</span>
<small>{t(memberStatusKey(member.status))}{member.model === undefined ? '' : ` · ${t('model')}: ${member.model}`}</small>
{member.diagnostics.map(diagnostic => <small key={diagnostic} className={css.diagnostic}>{diagnostic}</small>)}
</span>
</button>
))}
</div>
</section>
<section>
<div className={css.sectionTitle}>
<h3>{t('tasks')}</h3>
<button type="button" className={css.smallButton} onClick={() => { setCreating(true) }}>
<IconPlusOutline16 size={13} /> {t('create')}
</button>
</div>
{creating && (
<TaskForm
draft={createDraft}
setDraft={setCreateDraft}
pending={pendingTasks.has('create')}
onSave={() => { void submitCreate() }}
onCancel={() => { setCreating(false) }}
t={t}
/>
)}
{view.tasks.length === 0 && !creating && <div className={css.notice}>{t('empty')}</div>}
<div className={css.tasks}>
{view.tasks.map(task => editing === task.id
? (
<TaskForm
key={task.id}
draft={editDraft}
setDraft={setEditDraft}
pending={pendingTasks.has(task.id)}
onSave={() => { void submitEdit(task) }}
onCancel={() => { setEditing(null) }}
t={t}
/>
)
: (
<article key={task.id} className={css.task}>
<div className={css.taskTitle}>
<strong>{task.subject}</strong>
<span>{t(statusKey(task.status))}</span>
</div>
<p>{task.description}</p>
<div className={css.meta}>
<span>{task.id}</span>
{task.status === 'pending' && <span>{task.ready ? t('ready') : t('blocked')}</span>}
{task.blockedBy.length > 0 && <span>{t('blockedBy')}: {task.blockedBy.join(', ')}</span>}
{task.writeScopes.length > 0 && <span>{t('writeScopes')}: {task.writeScopes.join(', ')}</span>}
{task.writeScopeWarnings.map(warning => <span key={warning} className={css.warning}>{warning}</span>)}
</div>
<div className={css.taskActions}>
<label>
{t('owner')}
<select
value={task.ownerName ?? ''}
disabled={pendingTasks.has(task.id) || task.status === 'completed'}
onChange={(event: ChangeEvent<HTMLSelectElement>) => {
const owner = event.target.value
void settleTask(task.id, () => updateTask(sessionId, {
taskId: task.id,
expectedRevision: task.revision,
action: 'reassign',
...owner === '' ? {} : { owner },
}))
}}
>
<option value="">{t('unowned')}</option>
{assignable.map(member => <option key={member.id} value={member.name}>{member.name}</option>)}
</select>
</label>
<button type="button" onClick={() => { startEdit(task) }} disabled={pendingTasks.has(task.id)}>
<IconEditOutline16 size={13} /> {t('edit')}
</button>
{task.status === 'in_progress' && (
<button type="button" disabled={pendingTasks.has(task.id)} onClick={() => {
void settleTask(task.id, () => updateTask(sessionId, {
taskId: task.id, expectedRevision: task.revision, action: 'complete',
}))
}}><IconCheckOutline14 /> {t('complete')}</button>
)}
{task.status === 'completed' && (
<button type="button" disabled={pendingTasks.has(task.id)} onClick={() => {
void settleTask(task.id, () => updateTask(sessionId, {
taskId: task.id, expectedRevision: task.revision, action: 'reopen',
}))
}}>{t('reopen')}</button>
)}
<button type="button" disabled={pendingTasks.has(task.id)} onClick={() => {
void settleTask(task.id, () => updateTask(sessionId, {
taskId: task.id, expectedRevision: task.revision, action: 'delete',
}))
}}><IconTrashOutline16 size={13} /> {t('delete')}</button>
</div>
</article>
))}
</div>
</section>
</>
)}
</div>
)}
</div>
)
}
interface TaskFormProps {
draft: Draft
setDraft: (draft: Draft) => void
pending: boolean
onSave: () => void
onCancel: () => void
t: TeamActionProps['t']
}
function TaskForm({ draft, setDraft, pending, onSave, onCancel, t }: TaskFormProps) {
const field = (key: keyof Draft, value: string): void => { setDraft({ ...draft, [key]: value }) }
return (
<div className={css.form}>
<input value={draft.subject} placeholder={t('subject')} onChange={(event: ChangeEvent<HTMLInputElement>) => { field('subject', event.target.value) }} />
<textarea value={draft.description} placeholder={t('description')} onChange={(event: ChangeEvent<HTMLTextAreaElement>) => { field('description', event.target.value) }} />
<input value={draft.blockers} placeholder={t('blockers')} onChange={(event: ChangeEvent<HTMLInputElement>) => { field('blockers', event.target.value) }} />
<input value={draft.scopes} placeholder={t('scopes')} onChange={(event: ChangeEvent<HTMLInputElement>) => { field('scopes', event.target.value) }} />
<div className={css.formActions}>
<button type="button" disabled={pending || draft.subject.trim() === '' || draft.description.trim() === ''} onClick={onSave}>{t('save')}</button>
<button type="button" disabled={pending} onClick={onCancel}>{t('cancel')}</button>
</div>
</div>
)
}
@@ -0,0 +1,14 @@
/** Browser entry binding the generated Team Remote artifact to its Client UI. */
import agentTeamsRemote from '@deepseek-ai/dsh-experimental-agent-team/remote'
import type { Context as ClientContext } from '@deepseek-ai/cordis'
import { mountAgentTeamUi } from './mount.ts'
export { inject } from './mount.ts'
export type { TeamActionInjected, TeamActionProps, TeamActionResult } from './TeamAction.tsx'
export type { TeamKey } from './locales.ts'
/** Mount the generated Team Remote contribution and its browser UI. */
export async function apply(ctx: ClientContext): Promise<() => Promise<void>> {
return await mountAgentTeamUi(ctx, agentTeamsRemote)
}
@@ -0,0 +1,85 @@
/** Agent Teams Web dictionaries. */
/** Locale namespace owned by the Agent Teams Web UI. */
export const NS = 'agent-team'
/** Simplified Chinese dictionary and key source. */
export const zh = {
trigger: 'Agent Team',
refresh: '刷新 Team',
close: '关闭',
loading: '正在加载 Team…',
empty: '还没有共享任务',
roster: '成员',
tasks: '共享任务',
model: '模型',
open: '打开 teammate 会话',
create: '新建任务',
subject: '任务标题',
description: '任务描述',
blockers: '依赖任务 id(逗号分隔)',
scopes: '写入范围(逗号分隔)',
save: '保存',
cancel: '取消',
edit: '编辑',
complete: '完成',
reopen: '重开',
delete: '删除',
owner: 'Owner',
unowned: '未分配',
blockedBy: '依赖',
writeScopes: '写入范围',
ready: '可开始',
blocked: '被依赖阻塞',
conflict: '任务状态已变化,已重新加载;请检查后重试。',
'memberStatus.running': '运行中',
'memberStatus.idle': '空闲',
'memberStatus.inactive': '未运行',
'memberStatus.provisioning': '准备中',
'memberStatus.failed': '失败',
'status.pending': '待处理',
'status.in_progress': '进行中',
'status.completed': '已完成',
} satisfies Record<string, string>
/** Agent Teams locale key union. */
export type TeamKey = keyof typeof zh
/** English dictionary checked against the Chinese key set. */
export const en = {
trigger: 'Agent Team',
refresh: 'Refresh Team',
close: 'Close',
loading: 'Loading Team…',
empty: 'No shared tasks yet',
roster: 'Members',
tasks: 'Shared tasks',
model: 'Model',
open: 'Open teammate conversation',
create: 'New task',
subject: 'Task subject',
description: 'Task description',
blockers: 'Blocking task ids (comma separated)',
scopes: 'Write scopes (comma separated)',
save: 'Save',
cancel: 'Cancel',
edit: 'Edit',
complete: 'Complete',
reopen: 'Reopen',
delete: 'Delete',
owner: 'Owner',
unowned: 'Unowned',
blockedBy: 'Blocked by',
writeScopes: 'Write scopes',
ready: 'Ready',
blocked: 'Blocked by dependencies',
conflict: 'Task state changed and was reloaded. Review it before retrying.',
'memberStatus.running': 'Running',
'memberStatus.idle': 'Idle',
'memberStatus.inactive': 'Inactive',
'memberStatus.provisioning': 'Provisioning',
'memberStatus.failed': 'Failed',
'status.pending': 'Pending',
'status.in_progress': 'In progress',
'status.completed': 'Completed',
} satisfies Record<TeamKey, string>
@@ -0,0 +1,101 @@
/** Source-safe Agent Teams browser registration and Remote mount lifecycle. */
import type {
TeamMemberView as TeamRosterMember,
TeamView,
} from '@deepseek-ai/dsh-experimental-agent-team/client'
import type {} from '@deepseek-ai/dsh-experimental-agent-team/remote'
import type { Context as ClientContext } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-api-remotes/client'
import type {} from '@deepseek-ai/dsh-api-session-controller/client'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
import type {} from '@deepseek-ai/dsh-client-locale/client'
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
import type { TypertRemoteContribution } from '@deepseek-ai/dsh-typert-protocol'
import {
TeamAction, type TeamActionInjected, type TeamActionResult, type TeamTaskActionResult,
} from './TeamAction.tsx'
import { en, NS, zh, type TeamKey } from './locales.ts'
declare module '@deepseek-ai/dsh-client-ui-slots' {
interface LocaleNamespaceMap {
/** Agent Teams roster and task-board copy. */
'agent-team': TeamKey
}
}
/** Required browser services for RPC, navigation, slots, and localized copy. */
export const inject = ['sessions', 'remote', 'slots', 'locale']
function registerUi(ctx: ClientContext): void {
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'client-ui-agent-team: dictionaries')
const sessions = ctx.sessions
const leadSessionId = (sessionId: SessionId): SessionId => {
const address = sessions.binding(sessionId)?.session.getSnapshot().subagent?.address
return address?.parentSessionId ?? sessionId
}
const actions: TeamActionInjected = {
async load(sessionId): Promise<TeamActionResult<TeamView>> {
return await ctx.remote.agentTeams.view(leadSessionId(sessionId))
},
async createTask(sessionId, input): Promise<TeamTaskActionResult> {
return await ctx.remote.agentTeams.createTask(leadSessionId(sessionId), input)
},
async updateTask(sessionId, input) {
const { owner, ...rest } = input
return await ctx.remote.agentTeams.updateTask(leadSessionId(sessionId), {
...rest,
...owner === undefined ? {} : { owner },
})
},
async openTeammate(sessionId: SessionId, member: TeamRosterMember): Promise<void> {
if (member.role !== 'teammate') return
const parentSessionId = leadSessionId(sessionId)
await sessions.refreshSubagents(parentSessionId)
if (sessions.list.getSnapshot().current !== sessionId) return
sessions.openSubagent({
parentSessionId,
childSessionId: member.id,
mode: 'continuable',
})
},
}
ctx.slots.inject(
'conversation.session.header.actions',
() => ctx.slots.register({
name: 'conversation.session.header.actions',
id: 'agent-team',
order: 20,
locale: NS,
inject: () => actions,
}, TeamAction),
)
}
/**
* Mount one generated Team Remote contribution, then register its browser UI.
* @param ctx - Client Context carrying navigation, locale, slot, and Remote services.
* @param contribution - generated Team descriptors selected by the browser entry.
* @returns disposer for both the UI registrations and Remote namespace.
*/
export async function mountAgentTeamUi(
ctx: ClientContext,
contribution: TypertRemoteContribution,
): Promise<() => Promise<void>> {
const disposeRemote = await ctx.remote.$mount(contribution)
const ui = ctx.inject(['sessions', 'remote.agentTeams', 'slots', 'locale'], registerUi)
try {
await ui
} catch (error) {
await ui.dispose()
await disposeRemote()
throw error
}
return async () => {
await ui.dispose()
await disposeRemote()
}
}
@@ -0,0 +1,6 @@
declare module '*.module.css' {
const classes: Record<string, string>
export default classes
}
declare module '*.css'
@@ -0,0 +1,4 @@
/** Pure host half for the browser-discovered Agent Teams UI plugin. */
/** Host plugin body; Team behavior lives in the browser export and Host RPC domain. */
export function apply(): void {}
@@ -0,0 +1,22 @@
/** Package-owned invariant companion for the Team Web presentation. */
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-experimental-client-ui-agent-team'
/** Cordis companion plugin name. */
export const name = 'client-ui-agent-team-invariant'
/** Invariant registry dependency. */
export const inject = ['invariants']
/** No runtime invariant: RPC is authoritative and the package owns only one disposable slot registration. */
const install: InvariantInstaller = () => {}
/**
* Register this package's invariant ownership.
* @param ctx - Cordis Context carrying the invariant registry.
* @returns disposer for the package registration.
*/
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
@@ -0,0 +1,299 @@
import { Context, Service } from '@deepseek-ai/cordis'
import { describe, expect, it, vi } from 'vitest'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
import { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client'
import type { TeamMemberView as TeamRosterMember, TeamTaskId } from '@deepseek-ai/dsh-experimental-agent-team/client'
import type {} from '@deepseek-ai/dsh-experimental-agent-team/remote'
import type { TypertRemoteContribution } from '@deepseek-ai/dsh-typert-protocol'
import { TeamAction, type TeamActionInjected } from '../src/client/TeamAction.tsx'
import { inject, mountAgentTeamUi } from '../src/client/mount.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
const REMOTE: TypertRemoteContribution = {
package: '@deepseek-ai/dsh-experimental-agent-team',
descriptors: [],
}
async function bench(options: {
addressed?: boolean
conflict?: boolean
registrationFailure?: boolean
remoteFailure?: 'view' | 'update'
refreshGate?: Promise<void>
} = {}) {
const ctx = new Context()
const calls: { method: string; args: unknown[] }[] = []
const answer = <T>(method: string, value: T) => (...args: unknown[]) => {
calls.push({ method, args })
return Promise.resolve({ ok: true as const, value })
}
const task = {
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<void>> {
return this.mount(contribution)
}
}
const remote = new RemoteService(ctx)
const failure = {
ok: false as const,
error: { code: 'internal', message: 'offline', details: {} },
}
const view = {
members: [{
id: SESSION, name: 'lead', role: 'lead' as const, status: 'idle' as const, diagnostics: [],
}], tasks: [task],
}
ctx.provide('remote.agentTeams', {
view: (...args: unknown[]) => {
calls.push({ method: 'agentTeams/view', args })
return Promise.resolve(options.remoteFailure === 'view'
? failure
: { ok: true as const, value: view })
},
createTask: answer('agentTeams/createTask', task),
updateTask: (...args: unknown[]) => {
calls.push({ method: 'agentTeams/updateTask', args })
if (options.remoteFailure === 'update') return Promise.resolve(failure)
return Promise.resolve(options.conflict
? {
ok: true as const,
value: {
ok: false as const,
error: {
code: 'team-task-conflict' as const,
message: 'stale',
},
},
}
: { ok: true as const, value: { ok: true as const, value: { ...task, revision: 2 } } })
},
})
const navigation: unknown[] = []
let current = options.addressed === true ? CHILD : SESSION
ctx.provide('sessions', {
list: { getSnapshot: () => ({ current }) },
binding: (id: SessionId) => options.addressed === true && id === CHILD
? { session: { getSnapshot: () => ({
subagent: {
address: {
parentSessionId: SESSION,
childSessionId: CHILD,
mode: 'continuable' as const,
},
},
}) } }
: undefined,
refreshSubagents: (id: SessionId) => {
navigation.push(['refresh', id])
return options.refreshGate ?? Promise.resolve()
},
openSubagent: (address: unknown) => { navigation.push(['open', address]) },
})
ctx.provide('conversation', {})
ctx.provide('locale', new LocaleRuntime(ctx))
await ctx.plugin(SlotRegistry).await()
const collapseHeader = ctx.slots.register({
name: 'root',
children: { 'conversation.session.header.actions': { kind: 'list', scope: 'session' } },
} as never, () => null)
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: clientCtx => mountAgentTeamUi(clientCtx, REMOTE) })
const activation: Promise<unknown> = options.registrationFailure === true
? mountAgentTeamUi(ctx, REMOTE).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 },
}
}
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', 'slots', 'locale'])
expect(b.entry()).toMatchObject({
options: { id: 'agent-team', order: 20 },
locale: 'agent-team',
})
expect(b.remote.mount).toHaveBeenCalledOnce()
expect(b.remote.mount).toHaveBeenCalledWith(REMOTE)
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_ID, expectedRevision: 1, action: 'complete',
})).ok).toBe(true)
expect((await actions.updateTask(SESSION, {
taskId: TASK_ID, expectedRevision: 2, action: 'reassign', owner: 'worker',
})).ok).toBe(true)
expect(b.calls.map(call => call.method)).toEqual([
'agentTeams/view', 'agentTeams/createTask', 'agentTeams/updateTask', 'agentTeams/updateTask',
])
expect(b.calls.at(-1)?.args[1]).toMatchObject({ owner: 'worker' })
await actions.openTeammate(SESSION, {
id: SESSION,
name: 'lead',
role: 'lead',
status: 'idle',
diagnostics: [],
})
expect(b.navigation).toEqual([])
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('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_ID, expectedRevision: 1, action: 'delete',
})).resolves.toEqual({
ok: true,
value: {
ok: false,
error: { code: 'team-task-conflict', message: 'stale' },
},
})
})
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: { 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_ID, expectedRevision: 1, action: 'delete',
})).resolves.toEqual({
ok: false,
error: { code: 'internal', message: 'offline', details: {} },
})
})
it('refreshes the descriptor catalog before opening a continuable teammate address', async () => {
const b = await bench()
const actions = (b.entry()!.inject as unknown as () => TeamActionInjected)()
const member: TeamRosterMember = {
id: CHILD,
name: 'worker',
role: 'teammate',
status: 'inactive',
diagnostics: [],
}
await actions.openTeammate(SESSION, member)
expect(b.navigation).toEqual([
['refresh', SESSION],
['open', {
parentSessionId: SESSION,
childSessionId: CHILD,
mode: 'continuable',
}],
])
})
it('routes Team actions from an addressed teammate conversation back through its Lead', async () => {
const b = await bench({ addressed: true })
const actions = (b.entry()!.inject as unknown as () => TeamActionInjected)()
await actions.load(CHILD)
await actions.openTeammate(CHILD, {
id: CHILD,
name: 'worker',
role: 'teammate',
status: 'inactive',
diagnostics: [],
})
expect(b.calls[0]).toEqual({ method: 'agentTeams/view', args: [SESSION] })
expect(b.navigation).toEqual([
['refresh', SESSION],
['open', {
parentSessionId: SESSION,
childSessionId: CHILD,
mode: 'continuable',
}],
])
})
it('does not open a teammate after navigation switches during catalog refresh', async () => {
const refresh = Promise.withResolvers<undefined>()
const b = await bench({ refreshGate: refresh.promise })
const actions = (b.entry()!.inject as unknown as () => TeamActionInjected)()
const opening = actions.openTeammate(SESSION, {
id: CHILD,
name: 'worker',
role: 'teammate',
status: 'inactive',
diagnostics: [],
})
expect(b.navigation).toEqual([['refresh', SESSION]])
b.select('other-session' as SessionId)
refresh.resolve(undefined)
await opening
expect(b.navigation).toEqual([['refresh', SESSION]])
})
it('re-registers after the conversation header slot is collapsed and declared again', async () => {
const b = await bench()
expect(b.entry()).toBeDefined()
b.collapseHeader()
expect(b.entry()).toBeUndefined()
b.ctx.slots.register({
name: 'root',
children: { 'conversation.session.header.actions': { kind: 'list', scope: 'session' } },
} as never, () => null)
await Promise.resolve()
expect(b.entry()).toBeDefined()
})
it('keeps the node half inert', () => {
expect(() => { nodeApply() }).not.toThrow()
})
})
@@ -0,0 +1,753 @@
// @vitest-environment jsdom
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-session/types'
import type {
TeamTaskId, TeamTaskView as TeamTask, TeamView,
} from '@deepseek-ai/dsh-experimental-agent-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,
revision: 1,
subject: 'Implement runtime',
description: 'Build the Team runtime',
status: 'in_progress',
ownerName: 'lead',
blockedBy: [],
writeScopes: ['src'],
ready: false,
writeScopeWarnings: ['write scopes overlap with task-2'],
}
const view: TeamView = {
members: [
{ id: SESSION, name: 'lead', role: 'lead', status: 'idle', model: 'model-a', diagnostics: [] },
{
id: 'worker-id' as SessionId,
name: 'worker',
role: 'teammate',
status: 'inactive',
model: 'model-a',
diagnostics: [],
},
],
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,
...actions,
t: makeTranslate(zh, commonZh),
} as unknown as TeamActionProps
}
function actions(overrides: Partial<TeamActionInjected> = {}): TeamActionInjected {
return {
load: () => Promise.resolve({ ok: true, value: view }),
createTask: () => Promise.resolve(taskSuccess({ ...task, id: TASK_2, subject: 'New task' })),
updateTask: () => Promise.resolve({
ok: true,
value: { ok: true, value: { ...task, revision: 2 } },
}),
openTeammate: () => Promise.resolve(),
...overrides,
}
}
describe('TeamAction', () => {
it('ignores a stale Team load after the conversation switches sessions', async () => {
const nextSession = 'next-lead' as SessionId
const firstLoad = Promise.withResolvers<{ ok: true; value: TeamView }>()
const nextView: TeamView = {
...view,
members: [{ id: nextSession, name: 'lead', role: 'lead', status: 'idle', diagnostics: [] }],
tasks: [{ ...task, id: 'task-next' as TeamTaskId, subject: 'Next session task' }],
}
const load = vi.fn((sessionId: SessionId) => sessionId === SESSION
? firstLoad.promise
: Promise.resolve({ ok: true as const, value: nextView }))
const injected = actions({ load })
const rendered = render(<TeamAction {...props(injected)} />)
fireEvent.click(screen.getByRole('button', { name: /Agent Team/u }))
await waitFor(() => { expect(load).toHaveBeenCalledWith(SESSION) })
rendered.rerender(<TeamAction {...props(injected, nextSession)} />)
fireEvent.click(screen.getByRole('button', { name: /Agent Team/u }))
expect(await screen.findByText('Next session task')).toBeTruthy()
firstLoad.resolve({ ok: true, value: view })
await Promise.resolve()
await waitFor(() => {
expect(screen.getByText('Next session task')).toBeTruthy()
expect(screen.queryByText('Implement runtime')).toBeNull()
})
})
it('loads roster/task diagnostics on open and navigates a healthy teammate', async () => {
const openTeammate = vi.fn(() => Promise.resolve())
render(<TeamAction {...props(actions({ openTeammate }))} />)
fireEvent.click(screen.getByRole('button', { name: /Agent Team/u }))
const worker = await screen.findByRole('button', { name: /worker/u })
expect(screen.getByText('write scopes overlap with task-2')).toBeTruthy()
fireEvent.click(worker)
await waitFor(() => { expect(openTeammate).toHaveBeenCalledWith(SESSION, view.members[1]) })
})
it('keeps only the newest overlapping refresh for one session', async () => {
const older = Promise.withResolvers<TeamActionResult<TeamView>>()
const newer = Promise.withResolvers<TeamActionResult<TeamView>>()
const newestView = {
...view,
tasks: [{ ...task, id: 'newest-task' as TeamTaskId, subject: 'Newest task' }],
}
const load = vi.fn()
.mockResolvedValueOnce({ ok: true, value: view })
.mockImplementationOnce(() => older.promise)
.mockImplementationOnce(() => newer.promise)
render(<TeamAction {...props(actions({ load }))} />)
fireEvent.click(screen.getByRole('button', { name: /Agent Team/u }))
await screen.findByText('Implement runtime')
const refresh = screen.getByRole('button', { name: zh.refresh })
fireEvent.click(refresh)
fireEvent.click(refresh)
newer.resolve({ ok: true, value: newestView })
expect(await screen.findByText('Newest task')).toBeTruthy()
older.resolve({ ok: true, value: view })
await Promise.resolve()
expect(screen.getByText('Newest task')).toBeTruthy()
expect(screen.queryByText('Implement runtime')).toBeNull()
})
it('keeps a successful task mutation newer than an in-flight refresh', async () => {
const stale = Promise.withResolvers<TeamActionResult<TeamView>>()
const completedView = { ...view, tasks: [{ ...task, revision: 2, status: 'completed' as const }] }
const load = vi.fn()
.mockResolvedValueOnce({ ok: true, value: view })
.mockImplementationOnce(() => stale.promise)
.mockResolvedValueOnce({ ok: true, value: completedView })
const updateTask = vi.fn(() => Promise.resolve(
taskSuccess({ ...task, revision: 2, status: 'completed' }),
))
render(<TeamAction {...props(actions({ load, updateTask }))} />)
fireEvent.click(screen.getByRole('button', { name: /Agent Team/u }))
await screen.findByText('Implement runtime')
fireEvent.click(screen.getByRole('button', { name: zh.refresh }))
fireEvent.click(screen.getByRole('button', { name: //u }))
expect(await screen.findByRole('button', { name: //u })).toBeTruthy()
stale.resolve({ ok: true, value: view })
await Promise.resolve()
expect(screen.getByRole('button', { name: //u })).toBeTruthy()
expect(screen.queryByRole('button', { name: //u })).toBeNull()
})
it('keeps a created task newer than an in-flight refresh', async () => {
const stale = Promise.withResolvers<TeamActionResult<TeamView>>()
const createdTask = { ...task, id: TASK_2, subject: 'New task' }
const load = vi.fn()
.mockResolvedValueOnce({ ok: true, value: view })
.mockImplementationOnce(() => stale.promise)
.mockResolvedValueOnce({ ok: true, value: { ...view, tasks: [...view.tasks, createdTask] } })
render(<TeamAction {...props(actions({
load,
createTask: () => Promise.resolve(taskSuccess(createdTask)),
}))} />)
fireEvent.click(screen.getByRole('button', { name: /Agent Team/u }))
await screen.findByText('Implement runtime')
fireEvent.click(screen.getByRole('button', { name: zh.refresh }))
fireEvent.click(screen.getByRole('button', { name: //u }))
fireEvent.change(screen.getByPlaceholderText('任务标题'), { target: { value: 'New task' } })
fireEvent.change(screen.getByPlaceholderText('任务描述'), { target: { value: 'Details' } })
fireEvent.click(screen.getByRole('button', { name: '保存' }))
expect(await screen.findByText('New task')).toBeTruthy()
stale.resolve({ ok: true, value: view })
await Promise.resolve()
expect(screen.getByText('New task')).toBeTruthy()
})
it('keeps task and create failures newer than an in-flight refresh', async () => {
const staleTask = Promise.withResolvers<TeamActionResult<TeamView>>()
const taskLoad = vi.fn()
.mockResolvedValueOnce({ ok: true, value: view })
.mockImplementationOnce(() => staleTask.promise)
const first = render(<TeamAction {...props(actions({
load: taskLoad,
updateTask: () => Promise.resolve(taskRejected('task rejected')),
}))} />)
fireEvent.click(screen.getByRole('button', { name: /Agent Team/u }))
await screen.findByText('Implement runtime')
fireEvent.click(screen.getByRole('button', { name: zh.refresh }))
fireEvent.click(screen.getByRole('button', { name: //u }))
expect(await screen.findByText('task rejected (team-rejected)')).toBeTruthy()
staleTask.resolve({ ok: true, value: view })
await Promise.resolve()
expect(screen.getByText('task rejected (team-rejected)')).toBeTruthy()
first.unmount()
const staleCreate = Promise.withResolvers<TeamActionResult<TeamView>>()
const createLoad = vi.fn()
.mockResolvedValueOnce({ ok: true, value: view })
.mockImplementationOnce(() => staleCreate.promise)
render(<TeamAction {...props(actions({
load: createLoad,
createTask: () => Promise.resolve(taskRejected('create rejected')),
}))} />)
fireEvent.click(screen.getByRole('button', { name: /Agent Team/u }))
await screen.findByText('Implement runtime')
fireEvent.click(screen.getByRole('button', { name: zh.refresh }))
fireEvent.click(screen.getByRole('button', { name: //u }))
fireEvent.change(screen.getByPlaceholderText('任务标题'), { target: { value: 'Rejected task' } })
fireEvent.change(screen.getByPlaceholderText('任务描述'), { target: { value: 'Rejected details' } })
fireEvent.click(screen.getByRole('button', { name: '保存' }))
expect(await screen.findByText('create rejected (team-rejected)')).toBeTruthy()
staleCreate.resolve({ ok: true, value: view })
await Promise.resolve()
expect(screen.getByText('create rejected (team-rejected)')).toBeTruthy()
})
it('tracks simultaneous create and task mutations independently', async () => {
const create = Promise.withResolvers<TeamTaskActionResult>()
const createdTask = { ...task, id: TASK_2, subject: 'Concurrent task' }
const completedTask = { ...task, revision: 2, status: 'completed' as const }
const load = vi.fn()
.mockResolvedValueOnce({ ok: true, value: view })
.mockResolvedValueOnce({ ok: true, value: { ...view, tasks: [completedTask] } })
.mockResolvedValueOnce({ ok: true, value: { ...view, tasks: [completedTask, createdTask] } })
const createTask = vi.fn(() => create.promise)
render(<TeamAction {...props(actions({ load, createTask }))} />)
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: 'Concurrent task' } })
fireEvent.change(screen.getByPlaceholderText('任务描述'), { target: { value: 'Concurrent details' } })
const save = screen.getByRole<HTMLButtonElement>('button', { name: '保存' })
fireEvent.click(save)
await waitFor(() => { expect(save.disabled).toBe(true) })
const complete = screen.getByRole<HTMLButtonElement>('button', { name: //u })
expect(complete.disabled).toBe(false)
fireEvent.click(complete)
expect(await screen.findByRole('button', { name: //u })).toBeTruthy()
expect(save.disabled).toBe(true)
fireEvent.click(save)
expect(createTask).toHaveBeenCalledTimes(1)
create.resolve(taskSuccess(createdTask))
expect(await screen.findByText('Concurrent task')).toBeTruthy()
expect(screen.queryByRole('button', { name: '保存' })).toBeNull()
})
it('reloads derived fields for every task after a mutation', async () => {
const related = {
...task,
id: TASK_2,
subject: 'Related task',
writeScopeWarnings: ['old warning'],
}
const completed = { ...task, revision: 2, status: 'completed' as const }
const refreshed = {
...view,
tasks: [completed, { ...related, writeScopeWarnings: ['derived warning refreshed'] }],
}
const load = vi.fn()
.mockResolvedValueOnce({ ok: true, value: { ...view, tasks: [task, related] } })
.mockResolvedValueOnce({ ok: true, value: refreshed })
render(<TeamAction {...props(actions({
load,
updateTask: () => Promise.resolve(taskSuccess(completed)),
}))} />)
fireEvent.click(screen.getByRole('button', { name: /Agent Team/u }))
await screen.findByText('old warning')
fireEvent.click(screen.getAllByRole('button', { name: //u })[0]!)
expect(await screen.findByText('derived warning refreshed')).toBeTruthy()
expect(screen.queryByText('old warning')).toBeNull()
expect(load).toHaveBeenCalledTimes(2)
})
it('creates a task from normalized blocker and write-scope lists', async () => {
const createTask = vi.fn(actions().createTask)
render(<TeamAction {...props(actions({ createTask }))} />)
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: ' New task ' } })
fireEvent.change(screen.getByPlaceholderText('任务描述'), { target: { value: ' Details ' } })
fireEvent.change(screen.getByPlaceholderText(//u), { target: { value: 'task-1, task-1' } })
fireEvent.change(screen.getByPlaceholderText(//u), { target: { value: 'src/a, src/b' } })
fireEvent.click(screen.getByRole('button', { name: '保存' }))
await waitFor(() => {
expect(createTask).toHaveBeenCalledWith(SESSION, {
subject: 'New task',
description: 'Details',
blockedBy: ['task-1'],
writeScopes: ['src/a', 'src/b'],
})
})
})
it('assigns, edits, completes, reopens, and deletes with contiguous CAS revisions', async () => {
let current = { ...task }
const updateTask: TeamActionInjected['updateTask'] = vi.fn((
_sessionId: SessionId,
input: Parameters<TeamActionInjected['updateTask']>[1],
) => {
const revision = current.revision + 1
switch (input.action) {
case 'reassign':
current = {
...current,
revision,
status: 'in_progress',
ownerName: input.owner ?? 'lead',
}
break
case 'edit':
current = {
...current,
revision,
subject: input.subject ?? current.subject,
description: input.description ?? current.description,
writeScopes: input.writeScopes ?? current.writeScopes,
}
break
case 'set_dependencies':
current = { ...current, revision, blockedBy: input.blockedBy ?? [] }
break
case 'complete':
current = { ...current, revision, status: 'completed' }
break
case 'reopen': {
const { ownerName: _ownerName, ...unowned } = current
current = { ...unowned, revision, status: 'pending', ready: true }
break
}
case 'delete':
current = { ...current, revision, status: 'deleted' }
break
default:
throw new Error(`unexpected action ${input.action}`)
}
return Promise.resolve(taskSuccess(current))
})
const load = vi.fn(() => Promise.resolve({
ok: true as const,
value: { ...view, tasks: current.status === 'deleted' ? [] : [current] },
}))
render(<TeamAction {...props(actions({ load, updateTask }))} />)
fireEvent.click(screen.getByRole('button', { name: /Agent Team/u }))
await screen.findByText('Implement runtime')
fireEvent.change(screen.getByRole('combobox'), { target: { value: 'worker' } })
await waitFor(() => {
expect(screen.getByRole<HTMLSelectElement>('combobox').value).toBe('worker')
expect(current).toMatchObject({ revision: 2, ownerName: 'worker' })
})
fireEvent.click(screen.getByRole('button', { name: //u }))
fireEvent.change(screen.getByPlaceholderText('任务标题'), { target: { value: 'Updated runtime' } })
fireEvent.change(screen.getByPlaceholderText('任务描述'), { target: { value: 'Updated details' } })
fireEvent.change(screen.getByPlaceholderText(//u), { target: { value: 'task-0' } })
fireEvent.change(screen.getByPlaceholderText(//u), { target: { value: 'src/runtime' } })
fireEvent.click(screen.getByRole('button', { name: '保存' }))
expect(await screen.findByText('Updated runtime')).toBeTruthy()
expect(current).toMatchObject({
revision: 4,
description: 'Updated details',
blockedBy: ['task-0'],
writeScopes: ['src/runtime'],
})
fireEvent.click(screen.getByRole('button', { name: //u }))
fireEvent.click(await screen.findByRole('button', { name: //u }))
await waitFor(() => {
expect(screen.queryByRole('button', { name: //u })).toBeNull()
expect(current).toMatchObject({ revision: 6, status: 'pending' })
})
fireEvent.click(screen.getByRole('button', { name: //u }))
await waitFor(() => { expect(screen.queryByText('Updated runtime')).toBeNull() })
expect(vi.mocked(updateTask).mock.calls.map(([, input]) => [input.action, input.expectedRevision]))
.toEqual([
['reassign', 1],
['edit', 2],
['set_dependencies', 3],
['complete', 4],
['reopen', 5],
['delete', 6],
])
})
it('reloads and warns instead of retrying a stale task mutation', async () => {
const load = vi.fn()
.mockResolvedValueOnce({ ok: true, value: view })
.mockResolvedValueOnce({ ok: true, value: { ...view, tasks: [{ ...task, revision: 2 }] } })
const updateTask = vi.fn(() => Promise.resolve(taskConflict('stale')))
render(<TeamAction {...props(actions({ load, updateTask }))} />)
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(zh.conflict)).toBeTruthy()
expect(load).toHaveBeenCalledTimes(2)
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(remoteFailure('task reload failed'))
const first = render(<TeamAction {...props(actions({
load: taskLoad,
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 (internal)')).toBeTruthy()
expect(screen.queryByText(zh.conflict)).toBeNull()
first.unmount()
const dependencyLoad = vi.fn()
.mockResolvedValueOnce({ ok: true, value: view })
.mockResolvedValueOnce({ ok: true, value: { ...view, tasks: [{ ...task, revision: 2, subject: 'Edited' }] } })
.mockResolvedValueOnce(remoteFailure('dependency reload failed'))
const dependencyUpdate = vi.fn()
.mockResolvedValueOnce(taskSuccess({ ...task, revision: 2, subject: 'Edited' }))
.mockResolvedValueOnce(taskConflict('stale dependency'))
render(<TeamAction {...props(actions({ load: dependencyLoad, updateTask: dependencyUpdate }))} />)
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 (internal)')).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 = {
...view,
members: [
view.members[0]!,
{ ...view.members[1]!, status: 'running' },
{
id: 'failed-id' as SessionId,
name: 'failed-worker',
role: 'teammate',
status: 'failed',
diagnostics: ['provider failed'],
},
{
id: 'provisioning-id' as SessionId,
name: 'provisioning-worker',
role: 'teammate',
status: 'provisioning',
diagnostics: [],
},
],
tasks: [
{ ...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 }))
const openTeammate = vi.fn(() => Promise.reject(new Error('navigation failed')))
render(<TeamAction {...props(actions({ load, openTeammate }))} />)
fireEvent.click(screen.getByRole('button', { name: /Agent Team/u }))
expect(await screen.findByText('provider failed')).toBeTruthy()
expect(screen.getByText(zh.ready)).toBeTruthy()
expect(screen.getByText(zh.blocked)).toBeTruthy()
expect(screen.getByRole<HTMLButtonElement>('button', { name: /failed-worker/u }).disabled).toBe(true)
expect(screen.getByRole<HTMLButtonElement>('button', { name: /provisioning-worker/u }).disabled).toBe(true)
fireEvent.click(screen.getByRole('button', { name: /^worker/u }))
expect(await screen.findByText('Error: navigation failed')).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: zh.refresh }))
await waitFor(() => { expect(load).toHaveBeenCalledTimes(2) })
fireEvent.click(screen.getByRole('button', { name: /Agent Team/u }))
expect(screen.queryByRole('dialog')).toBeNull()
fireEvent.click(screen.getByRole('button', { name: /Agent Team/u }))
await screen.findByRole('dialog')
fireEvent.click(screen.getByRole('button', { name: zh.close }))
expect(screen.queryByRole('dialog')).toBeNull()
})
it('shows load and create failures and ignores a create result after a session switch', async () => {
const failedLoad = actions({
load: () => Promise.resolve(remoteFailure('load failed')),
})
const first = render(<TeamAction {...props(failedLoad)} />)
fireEvent.click(screen.getByRole('button', { name: /Agent Team/u }))
expect(await screen.findByText('load failed (internal)')).toBeTruthy()
first.unmount()
const createTask = vi.fn(() => Promise.resolve(remoteFailure('create failed')))
const second = render(<TeamAction {...props(actions({ createTask }))} />)
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: 'Task' } })
fireEvent.change(screen.getByPlaceholderText('任务描述'), { target: { value: 'Description' } })
fireEvent.click(screen.getByRole('button', { name: '保存' }))
expect(await screen.findByText('create failed (internal)')).toBeTruthy()
second.unmount()
const pending = Promise.withResolvers<TeamTaskActionResult>()
const third = render(<TeamAction {...props(actions({ createTask: () => pending.promise }))} />)
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: 'Late task' } })
fireEvent.change(screen.getByPlaceholderText('任务描述'), { target: { value: 'Late description' } })
fireEvent.click(screen.getByRole('button', { name: '保存' }))
third.rerender(<TeamAction {...props(actions(), 'next-session' as SessionId)} />)
pending.resolve(taskSuccess({ ...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<TeamTaskActionResult>()
const rendered = render(<TeamAction {...props(actions({ updateTask: () => 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(<TeamAction {...props(actions(), 'next-session' as SessionId)} />)
pending.resolve(taskSuccess({ ...task, revision: 2, status: 'completed' }))
await Promise.resolve()
expect(screen.queryByText('Implement runtime')).toBeNull()
rendered.unmount()
render(<TeamAction {...props(actions({
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 (team-rejected)')).toBeTruthy()
})
it('does not publish a task conflict after its reload switches sessions', async () => {
const reload = Promise.withResolvers<TeamActionResult<TeamView>>()
const load = vi.fn()
.mockResolvedValueOnce({ ok: true, value: view })
.mockImplementationOnce(() => reload.promise)
const rendered = render(<TeamAction {...props(actions({
load,
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 }))
await waitFor(() => { expect(load).toHaveBeenCalledTimes(2) })
rendered.rerender(<TeamAction {...props(actions(), 'next-session' as SessionId)} />)
reload.resolve({ ok: true, value: view })
await Promise.resolve()
await Promise.resolve()
expect(screen.queryByText(zh.conflict)).toBeNull()
})
it('does not settle a successful task after its reload switches sessions', async () => {
const reload = Promise.withResolvers<TeamActionResult<TeamView>>()
const load = vi.fn()
.mockResolvedValueOnce({ ok: true, value: view })
.mockImplementationOnce(() => reload.promise)
const rendered = render(<TeamAction {...props(actions({
load,
updateTask: () => Promise.resolve(taskSuccess({ ...task, revision: 2, status: 'completed' })),
}))} />)
fireEvent.click(screen.getByRole('button', { name: /Agent Team/u }))
await screen.findByText('Implement runtime')
fireEvent.click(screen.getByRole('button', { name: //u }))
await waitFor(() => { expect(load).toHaveBeenCalledTimes(2) })
rendered.rerender(<TeamAction {...props(actions(), 'next-session' as SessionId)} />)
reload.resolve({ ok: true, value: { ...view, tasks: [{ ...task, revision: 2, status: 'completed' }] } })
await Promise.resolve()
await Promise.resolve()
expect(screen.queryByText('Implement runtime')).toBeNull()
})
it('contains edit and dependency failures and supports form cancellation and unassignment', async () => {
const { ownerName: _ownerName, ...unownedTask } = task
const updateTask = vi.fn()
.mockResolvedValueOnce(remoteFailure('edit failed'))
.mockResolvedValueOnce(taskSuccess({ ...task, revision: 2, subject: 'Saved edit' }))
.mockResolvedValueOnce(taskRejected('dependency failed'))
.mockResolvedValueOnce(taskSuccess({ ...unownedTask, revision: 2 }))
render(<TeamAction {...props(actions({ updateTask }))} />)
fireEvent.click(screen.getByRole('button', { name: /Agent Team/u }))
await screen.findByText('Implement runtime')
fireEvent.click(screen.getByRole('button', { name: //u }))
fireEvent.click(screen.getByRole('button', { name: '取消' }))
expect(screen.queryByPlaceholderText('任务标题')).toBeNull()
fireEvent.click(screen.getByRole('button', { name: //u }))
fireEvent.click(screen.getByRole('button', { name: '取消' }))
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 (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 (team-rejected)')).toBeTruthy()
expect(updateTask.mock.calls[2]?.[1]).toMatchObject({
action: 'set_dependencies',
expectedRevision: 2,
blockedBy: ['task-2'],
})
fireEvent.click(screen.getByRole('button', { name: '取消' }))
fireEvent.change(screen.getByRole('combobox'), { target: { value: '' } })
await waitFor(() => {
expect(updateTask).toHaveBeenLastCalledWith(SESSION, expect.objectContaining({
action: 'reassign',
}))
expect(updateTask.mock.calls.at(-1)?.[1]).not.toHaveProperty('owner')
})
})
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(<TeamAction {...props(actions({ updateTask }))} />)
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' as TeamTaskId] }
const updateTask = vi.fn().mockResolvedValue(
taskSuccess({ ...blockedTask, revision: 2, subject: 'Same dependencies' }),
)
render(<TeamAction {...props(actions({
load: () => Promise.resolve({ ok: true, value: { ...view, tasks: [blockedTask] } }),
updateTask,
}))} />)
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: 'Same dependencies' } })
fireEvent.click(screen.getByRole('button', { name: '保存' }))
await waitFor(() => { expect(screen.queryByRole('button', { name: '保存' })).toBeNull() })
expect(updateTask).toHaveBeenCalledTimes(1)
expect(updateTask).toHaveBeenCalledWith(SESSION, expect.objectContaining({ action: 'edit' }))
})
it('reloads a dependency conflict and ignores dependency settlement after a session switch', async () => {
const load = vi.fn()
.mockResolvedValueOnce({ ok: true, value: view })
.mockResolvedValueOnce({ ok: true, value: { ...view, tasks: [{ ...task, revision: 2, subject: 'Conflict edit' }] } })
.mockResolvedValueOnce({ ok: true, value: { ...view, tasks: [{ ...task, revision: 3 }] } })
const conflictUpdate = vi.fn()
.mockResolvedValueOnce(taskSuccess({ ...task, revision: 2, subject: 'Conflict edit' }))
.mockResolvedValueOnce(taskConflict('stale dependency'))
const first = render(<TeamAction {...props(actions({ load, updateTask: conflictUpdate }))} />)
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: 'Conflict edit' } })
fireEvent.change(screen.getByPlaceholderText(zh.blockers), { target: { value: 'task-2' } })
fireEvent.click(screen.getByRole('button', { name: '保存' }))
expect(await screen.findByText(zh.conflict)).toBeTruthy()
expect(load).toHaveBeenCalledTimes(3)
first.unmount()
const dependencyReload = Promise.withResolvers<TeamActionResult<TeamView>>()
const dependencyLoad = vi.fn()
.mockResolvedValueOnce({ ok: true, value: view })
.mockResolvedValueOnce({ ok: true, value: { ...view, tasks: [{ ...task, revision: 2, subject: 'Late edit' }] } })
.mockImplementationOnce(() => dependencyReload.promise)
const staleUpdate = vi.fn()
.mockResolvedValueOnce(taskSuccess({ ...task, revision: 2, subject: 'Late edit' }))
.mockResolvedValueOnce(taskConflict('stale dependency'))
const second = render(<TeamAction {...props(actions({ load: dependencyLoad, updateTask: staleUpdate }))} />)
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: 'Late edit' } })
fireEvent.change(screen.getByPlaceholderText(zh.blockers), { target: { value: 'task-2' } })
fireEvent.click(screen.getByRole('button', { name: '保存' }))
await waitFor(() => { expect(dependencyLoad).toHaveBeenCalledTimes(3) })
second.rerender(<TeamAction {...props(actions(), 'next-session' as SessionId)} />)
dependencyReload.resolve({ ok: true, value: { ...view, tasks: [{ ...task, revision: 3 }] } })
await Promise.resolve()
await Promise.resolve()
expect(screen.queryByText(zh.conflict)).toBeNull()
second.unmount()
const dependency = Promise.withResolvers<TeamTaskActionResult>()
const lateUpdate = vi.fn()
.mockResolvedValueOnce(taskSuccess({ ...task, revision: 2, subject: 'Late edit' }))
.mockImplementationOnce(() => dependency.promise)
const third = render(<TeamAction {...props(actions({ updateTask: lateUpdate }))} />)
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: 'Late edit' } })
fireEvent.change(screen.getByPlaceholderText(zh.blockers), { target: { value: 'task-2' } })
fireEvent.click(screen.getByRole('button', { name: '保存' }))
await waitFor(() => { expect(lateUpdate).toHaveBeenCalledTimes(2) })
expect(screen.getByRole<HTMLButtonElement>('button', { name: '保存' }).disabled).toBe(true)
expect(screen.getByRole<HTMLButtonElement>('button', { name: '取消' }).disabled).toBe(true)
third.rerender(<TeamAction {...props(actions(), 'next-session' as SessionId)} />)
dependency.resolve(taskSuccess({ ...task, revision: 3, subject: 'Late dependency' }))
await Promise.resolve()
expect(screen.queryByText('Late dependency')).toBeNull()
})
})
@@ -0,0 +1,23 @@
{
"extends": "../../../tsconfig.base.client.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": ["src"],
"references": [
{ "path": "../../../vendor/cordis" },
{ "path": "../agent-team" },
{ "path": "../../api/remotes/tsconfig.client.json" },
{ "path": "../../api/session-controller/tsconfig.client.json" },
{ "path": "../../client/locale" },
{ "path": "../../client/ui-conversation" },
{ "path": "../../client/ui-primitives" },
{ "path": "../../client/ui-renderer" },
{ "path": "../../client/ui-session" },
{ "path": "../../client/ui-slots" },
{ "path": "../../core/session" },
{ "path": "../../typert/protocol" },
{ "path": "../../runtime-diagnostics/invariants" }
]
}
@@ -0,0 +1,3 @@
import { clientBundle } from '../../client/tsdown.client.ts'
export default clientBundle('@deepseek-ai/dsh-experimental-client-ui-agent-team', ['lib/types/index.js', 'lib/types/invariant.js'])
@@ -1144,6 +1144,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
occupants: [
'client-ui-agent-preset AgentPresetLabel id \'agent-preset\'',
'client-ui-jobs JobListAction id \'job-list\'',
'experimental-client-ui-agent-team TeamAction id \'agent-team\'',
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.session.header.actions\', () => ctx.slots.register(\n { name: \'conversation.session.header.actions\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
@@ -375,6 +375,24 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
parameters: [{ name: 'agent', description: 'candidate exact live Agent.' }],
returns: 'Team membership, or undefined for non-Team subagents and stale identities.',
},
{
signature: '@Remote(\'view\') remoteView(agent: Agent): TeamView',
description: 'Read the current roster and non-deleted task board through the generated Remote API.',
parameters: [{ name: 'agent', description: 'exact live Team member used as the authority credential.' }],
returns: 'detached current roster and task views.',
},
{
signature: '@Remote(\'createTask\') remoteCreateTask(agent: Agent, request: CreateTeamTaskRequest): Promise<TeamTaskMutationResult>',
description: 'Create one shared task through the generated Remote API.',
parameters: [{ name: 'agent', description: 'exact live Team member creating the task.' }, { name: 'request', description: 'task text, blockers, and advisory write scopes.' }],
returns: 'the revision-one task or a typed Team rejection.',
},
{
signature: '@Remote(\'updateTask\') remoteUpdateTask(agent: Agent, request: UpdateTeamTaskRequest): Promise<TeamTaskMutationResult>',
description: 'Apply one task mutation and preserve Team rejections as business results.',
parameters: [{ name: 'agent', description: 'exact live Team member authorizing the mutation.' }, { name: 'request', description: 'task identity, expected revision, action, and action fields.' }],
returns: 'the committed task or a typed Team rejection.',
},
],
},
{
@@ -5171,6 +5189,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [
name: 'TeamTaskId',
declaration: 'export type TeamTaskId = Branded<\'TeamTaskId\'>;',
},
{
name: 'TeamTaskMutationResult',
declaration: 'export type TeamTaskMutationResult = {\n readonly ok: true;\n readonly value: TeamTaskView;\n} | {\n readonly ok: false;\n readonly error: {\n readonly code: \'team-task-conflict\' | \'team-rejected\';\n readonly message: string;\n };\n};',
},
{
name: 'TeamTaskStatus',
declaration: 'export type TeamTaskStatus = \'pending\' | \'in_progress\' | \'completed\' | \'deleted\';',
@@ -5179,6 +5201,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [
name: 'TeamTaskView',
declaration: 'export interface TeamTaskView {\n readonly id: TeamTaskId;\n readonly revision: number;\n readonly subject: string;\n readonly description: string;\n readonly status: TeamTaskStatus;\n readonly blockedBy: TeamTaskId[];\n readonly writeScopes: string[];\n readonly ownerName?: string;\n readonly ready: boolean;\n readonly writeScopeWarnings: string[];\n}',
},
{
name: 'TeamView',
declaration: 'export interface TeamView {\n readonly members: TeamMemberView[];\n readonly tasks: TeamTaskView[];\n}',
},
{
name: 'TeamWaitResult',
declaration: 'export interface TeamWaitResult {\n readonly timedOut: boolean;\n}',
+80
View File
@@ -4591,6 +4591,9 @@ importers:
'@deepseek-ai/dsh-subagent-spawn-in-process':
specifier: workspace:^
version: link:../../subagent/subagent-spawn-in-process
'@deepseek-ai/dsh-typert-protocol':
specifier: workspace:^
version: link:../../typert/protocol
packages/experimental/agent-team-profile:
dependencies:
@@ -4614,6 +4617,83 @@ importers:
specifier: ^4.2.0
version: 4.3.1
packages/experimental/agent-team-web-profile:
dependencies:
'@deepseek-ai/dsh-experimental-client-ui-agent-team':
specifier: workspace:^
version: link:../client-ui-agent-team
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
version: link:../../../vendor/cordis
'@deepseek-ai/cordis-plugin-include':
specifier: workspace:^
version: link:../../../vendor/include
'@deepseek-ai/dsh-invariants':
specifier: workspace:^
version: link:../../runtime-diagnostics/invariants
js-yaml:
specifier: ^4.2.0
version: 4.3.1
packages/experimental/client-ui-agent-team:
dependencies:
react:
specifier: ^18.2.0
version: 18.3.1
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
version: link:../../../vendor/cordis
'@deepseek-ai/dsh-api-remotes':
specifier: workspace:^
version: link:../../api/remotes
'@deepseek-ai/dsh-api-session-controller':
specifier: workspace:^
version: link:../../api/session-controller
'@deepseek-ai/dsh-client-locale':
specifier: workspace:^
version: link:../../client/locale
'@deepseek-ai/dsh-client-test-runtime':
specifier: workspace:^
version: link:../../test-support/client-runtime
'@deepseek-ai/dsh-client-ui-conversation':
specifier: workspace:^
version: link:../../client/ui-conversation
'@deepseek-ai/dsh-client-ui-primitives':
specifier: workspace:^
version: link:../../client/ui-primitives
'@deepseek-ai/dsh-client-ui-renderer':
specifier: workspace:^
version: link:../../client/ui-renderer
'@deepseek-ai/dsh-client-ui-session':
specifier: workspace:^
version: link:../../client/ui-session
'@deepseek-ai/dsh-client-ui-slots':
specifier: workspace:^
version: link:../../client/ui-slots
'@deepseek-ai/dsh-experimental-agent-team':
specifier: workspace:^
version: link:../agent-team
'@deepseek-ai/dsh-invariants':
specifier: workspace:^
version: link:../../runtime-diagnostics/invariants
'@deepseek-ai/dsh-session':
specifier: workspace:^
version: link:../../core/session
'@deepseek-ai/dsh-typert-protocol':
specifier: workspace:^
version: link:../../typert/protocol
'@testing-library/react':
specifier: ^16.1.0
version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@types/react':
specifier: ~18.3.1
version: 18.3.31
react-dom:
specifier: ^18.2.0
version: 18.3.1(react@18.3.1)
packages/experimental/tool-agent-team:
dependencies:
'@deepseek-ai/schemastery':
+2
View File
@@ -495,8 +495,10 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
TeamId: 'agent-team.md',
TeamMemberView: 'agent-team.md',
TeamMembership: 'agent-team.md',
TeamTaskMutationResult: 'agent-team.md',
TeamTaskId: 'agent-team.md',
TeamTaskView: 'agent-team.md',
TeamView: 'agent-team.md',
TeamWaitResult: 'agent-team.md',
UpdateTeamTaskRequest: 'agent-team.md',
TokenMeasurement: 'token-meter.md',
+2 -2
View File
@@ -525,8 +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.',
consumers: ['experimental-tool-agent-team', 'experimental-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',
+1
View File
@@ -445,6 +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/tests/built-lib.e2e.ts',
]),
)
expect(subject.find(item => item.id === 'web-snapshot')).toMatchObject({
+1
View File
@@ -750,6 +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/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.
@@ -74,6 +74,8 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'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-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.' },
'packages/client/ui-sidebar': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-brand-official': { kind: 'none', reason: 'Browser-side presentation occupants; registers nothing model-facing.' },
+1
View File
@@ -245,6 +245,7 @@
"@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-experimental-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"],
"@deepseek-ai/dsh-client-ui-plan": ["./packages/client/ui-plan/src"],
+1
View File
@@ -76,6 +76,7 @@
{ "path": "./packages/client/ui-skill" },
{ "path": "./packages/client/ui-reference" },
{ "path": "./packages/client/ui-subagent" },
{ "path": "./packages/experimental/client-ui-agent-team" },
{ "path": "./packages/client/ui-jobs" },
{ "path": "./packages/client/ui-directory-picker-browse" },
{ "path": "./packages/client/ui-directory-picker-native" },
+2
View File
@@ -83,6 +83,7 @@
"apps/web/tests/skill-tool-row.e2e.ts",
"apps/web/tests/turn-tail-actions.e2e.ts",
"apps/web/tests/goal-multi-turn-actions.e2e.ts",
"apps/web/tests/agent-team-panel.e2e.ts",
"apps/web/tests/chat-scroll-fixture.ts",
"apps/web/tests/chat-scroll-contract.e2e.ts",
"apps/web/tests/chat-long-interactions.e2e.ts",
@@ -290,6 +291,7 @@
{ "path": "./packages/experimental/agent-team" },
{ "path": "./packages/experimental/agent-team-profile" },
{ "path": "./packages/experimental/tool-agent-team" },
{ "path": "./packages/experimental/agent-team-web-profile" },
{ "path": "./packages/workflow/workflow" },
{ "path": "./packages/workflow/workflow-worker-thread" },
{ "path": "./packages/workflow/tool-workflow" },
+3
View File
@@ -268,6 +268,9 @@ 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',
// The Team browser entry binds its source-covered mount lifecycle to
// the generated Team Remote contribution, which likewise exists only in lib.
'packages/experimental/client-ui-agent-team/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',