子智能体与协作任务
@deepseek-ai/dsh-experimental-agent-team 将一个会话组织成由 Lead 和具名 teammate 组成的小型协作团队,提供持久成员目录、可恢复的 peer 消息队列,以及带依赖和版本控制的共享任务板。它是实验性包,必须与持久会话存储及 dsh-experimental-tool-agent-team 一起使用,才能让模型实际操作团队能力。
Purpose and Scope
本文档覆盖 Agent Teams 服务本身:Team 身份与 roster、teammate 创建和恢复、持久 mailbox、共享任务板、等待/中断、配置限制、事件投影及运行时清理。重点是 TeamService 的服务入口和它组合的内部 owner,以及这些对象如何围绕 Lead Session 日志工作。
本文档不展开工具包如何向模型暴露工具,也不展开浏览器 UI 或通用 session persistence 的实现。需要模型可调用的工具时,请参阅 tool-agent-team;需要跨子系统的服务类型和 Web 投影说明时,请参阅 Agent Teams 子系统文档。
Overview
一个普通 runtime root 隐式充当 Team Lead,TeamId 等于 SessionId。Lead 可以创建名字永久保留的直接 teammate;teammate 可以使用 fresh 或 fork 模式启动。所有成员都可以向其他成员发送消息、创建任务和读取任务板,但只有 Lead 可以创建 teammate 或中断 teammate。
实现的核心取舍是“持久日志、派生状态”:成员、消息和任务事件追加到 Lead 的精确 Session 日志,roster、mailbox 和任务视图在读取/恢复时从日志重建。消息先持久化为 queued,再尝试即时投递;恢复时只重试尚未确认 delivered 的记录,并结合目标会话的去重状态,避免崩溃后重复消息。任务更新使用 expectedRevision compare-and-set,拒绝陈旧副本覆盖新状态。
该设计适合多个 agent 在同一进程、同一工作区中协调,并要求状态能够跨 reload、崩溃和中断保留。它不提供独立工作目录、跨进程共识、worktree/merge 或文件锁;writeScopes 仅是重叠写入提示,不是权限边界。
Architecture
TeamService 是 Cordis service facade,通过 static inject 依赖 agents、sessions、sessionPersistence、sessionProjections 和 subagents。它不直接实现 roster、消息或任务算法,而是在构造函数中把同一份配置和共享的 TeamJournal 注入各 domain owner。TeamJournal 将变更串行化到 Lead Session 日志;teamProjectionDefinition 则把 Team 事件投影为客户端可见的 agentTeam 状态。TeamActivity 负责一次性变更等待者,TeamRuntimeLifecycle 负责准入、恢复和有界 dispose。
Source: index.ts
核心实现
TeamService 的组装与准入
构造函数先将五个部署限制归一化为正的 safe integer,然后创建 activity、lifecycle、journal、roster、mailbox 和 task board。mailbox 与 roster 共享 lifecycle,确保 dispose 时不会继续接受新的创建或投递;mailbox 和 task board 则共享 journal,从而让不同领域的事件进入同一个 Lead 日志顺序。
服务通过 agent/created 为现有和新建 agent 排队 recovery,通过 session/event 让 mailbox 观察会话事件,并在 agent/status 时唤醒属于该 Team 的 activity waiter。Cordis effect 注册 Team projection,并把 runtime cleanup 绑定到 effect 的异步释放阶段。
1const DEFAULT_MAX_MEMBERS = 16
2const DEFAULT_MAX_TASKS = 256
3const DEFAULT_MAX_PENDING_MESSAGES = 64
4const DEFAULT_MAX_MESSAGE_BYTES = 65_536
5const DEFAULT_DISPOSAL_TIMEOUT_MS = 5_000
6
7export class TeamService extends Service {
8 static inject = ['agents', 'sessions', 'sessionPersistence', 'sessionProjections', 'subagents']
9
10 static Config: z<Config> = z.object({
11 maxMembers: z.number().step(1).min(1).default(DEFAULT_MAX_MEMBERS),
12 maxTasks: z.number().step(1).min(1).default(DEFAULT_MAX_TASKS),
13 maxPendingMessagesPerMember: z.number().step(1).min(1).default(DEFAULT_MAX_PENDING_MESSAGES),
14 maxMessageBytes: z.number().step(1).min(1).default(DEFAULT_MAX_MESSAGE_BYTES),
15 disposalTimeoutMs: z.number().step(1).min(1).default(DEFAULT_DISPOSAL_TIMEOUT_MS),
16 })
17}Source: index.ts
身份、权限与 roster
每个公开方法都接收确切的 live Agent,并通过 roster.membership(agent) 解析其 Team root、角色和模型侧名称。这使权限判断建立在运行时身份上,而不是调用方传入的字符串上。spawnTeammate() 将请求交给 TeamRoster.spawn;文档约定只有 Lead 能执行该操作,名字在第一次 provisioning 记录后永久保留,即使 child 创建失败也不会复用。
恢复流程会检查未终结的 provisioning 记录和 child 独立持久化会话:直接 parent、continuable descriptor 及初始用户消息都匹配时恢复为 active,否则为 failed。若同进程竞争导致另一方先完成,创建方接受终态或报告 TEAM_PROVISIONING_CONFLICT 并清理 child。
持久消息 mailbox
sendMessage() 先验证 peer 成员关系并将 team/message/queued flush 到日志,再尝试即时投递。live target 在步骤边界通过 Steer 收到消息;inactive target 在加载时启动轮次,冷 target 则恢复后投递。只有目标 pending inbox 或持久历史已经持有消息 id,才追加 delivered 确认。
恢复时以 queued 减去 delivered 的集合重新投递,并同时折叠 live 与持久目标状态,因此“inbox 已接受但模型尚未 claim”这类崩溃窗口不会复制消息。该保证依赖进程内重试和目标会话去重,不是跨进程 exactly-once;多个 harness 进程不应并发操作同一 Team。
版本化任务板
任务是完整版本化快照。创建任务从 revision 1 开始;后续更新必须携带 expectedRevision,陈旧版本会失败并报告 TEAM_TASK_STALE_REVISION。任务依赖形成 DAG,只有依赖全部完成后才 ready;writeScopes 被规范化为 workspace-relative 前缀,用于提示 in-progress 任务之间的重叠路径,但不会阻止 claim 或授予写权限。
删除任务会留下 tombstone,以便回放和保持任务 id 稳定;它不再出现在 listTasks(),也不占用 maxTasks。任务 id 的数字后缀必须是 safe integer,耗尽时报告 TEAM_TASK_LIMIT,而不是复用旧 id。
Core Flow
Source: README.zh.md
消息的关键顺序是先写 queued、后投递;这样即时投递失败不会丢失消息,而 dispose 或 recovery 可以继续处理已持久化的记录。TeamService.sendMessage 本身只负责把 exact caller 和请求交给 mailbox,消息验证、序列化、去重和状态事件都由 TeamMailbox 承担。
恢复和清理遵循同一原则:agent/created 触发 contained recovery pass;runtime dispose 先关闭准入、取消并等待已获准的创建和 dispatch,再释放 roster 中精确的 live direct child 及其后代。清理失败会显式使 dispose 失败,并受 disposalTimeoutMs 限制。
Usage Examples
最小组合配置
持久会话存储和两个 Team 包构成最小可用组合。配置字段是插件名列表,实际工具操作由兄弟 dsh-experimental-tool-agent-team 提供。
- name: '@deepseek-ai/dsh-session-persistence-jsonl'
- name: '@deepseek-ai/dsh-experimental-agent-team'
- name: '@deepseek-ai/dsh-experimental-tool-agent-team'Source: README.zh.md
通过服务入口创建 teammate、发送消息并操作任务
以下片段直接反映 TeamService 的公开 facade:调用方必须传入 exact live Agent;服务会把权限和 Team 归属解析委托给 roster,再把领域操作委托给对应 owner。
1async spawnTeammate(caller: Agent, request: SpawnTeammateRequest): Promise<SpawnTeammateResult> {
2 return await this.roster.spawn(caller, request)
3}
4
5async sendMessage(caller: Agent, request: SendTeamMessageRequest): Promise<SendTeamMessageResult> {
6 return await this.mailbox.send(caller, request)
7}
8
9async createTask(caller: Agent, request: CreateTeamTaskRequest): Promise<TeamTaskView> {
10 return await this.tasks.create(this.roster.membership(caller), request)
11}
12
13async updateTask(caller: Agent, request: UpdateTeamTaskRequest): Promise<TeamTaskView> {
14 return await this.tasks.update(caller, this.roster.membership(caller), request)
15}Source: index.ts
等待变化与中断
等待不是轮询 API:调用方给出 10 秒至 1 小时范围内的 timeout,并可用 AbortSignal 只取消本次等待。中断只接受 Lead caller,且保留目标的 pending inbox 和任务 owner。
1async waitForChange(caller: Agent, timeoutMs: number, signal: AbortSignal): Promise<TeamWaitResult> {
2 const membership = this.roster.membership(caller)
3 return await this.activity.wait(membership.id, timeoutMs, signal)
4}
5
6interrupt(caller: Agent, targetName: string): { previousStatus: 'running' | 'inactive' } {
7 return this.roster.interrupt(caller, targetName)
8}Source: index.ts
Configuration Options
所有选项都必须是正的 safe integer;schema 同时约束最小值为 1,构造函数再次通过 positiveLimit 校验。配置耗尽时不复用成员名、任务 id 或静默丢弃消息,而是报告类型化 Team 错误。
| 选项 | 类型 | 默认值 | 作用 |
|---|---|---|---|
maxMembers | number | 16 | 一支 Team 最多创建的 teammate 数,包括创建失败的成员。 |
maxTasks | number | 256 | 任务板上的活动任务上限。删除后的 tombstone 不计入活动列表。 |
maxPendingMessagesPerMember | number | 64 | 单个成员可排队的消息数。 |
maxMessageBytes | number | 65,536 | 单条发送消息的最大尺寸。 |
disposalTimeoutMs | number | 5,000 | dispose 清理允许的最长时间。 |
配置 schema 和默认常量来自服务入口;包文档还明确指出,只有持久会话存储存在时 Team 功能才会激活。
API Reference
membership(agent: Agent): TeamMembership
解析 exact live agent 的 Team root、Team 身份、角色和模型侧名称。对不属于 Team 或已失效的身份,使用 tryMembership 获取非抛错结果。
listMembers(agent: Agent): TeamMemberView[]
返回调用者可见、按创建顺序排列的 Lead 和 teammate runtime roster。
spawnTeammate(caller: Agent, request: SpawnTeammateRequest): Promise<SpawnTeammateResult>
创建 Lead 的一个具名、可持续 direct child。只有 Lead 可调用;名字唯一且不可复用。返回 active roster row,提供方失败则会留下持久 failed 成员记录。
sendMessage(caller: Agent, request: SendTeamMessageRequest): Promise<SendTeamMessageResult>
持久化一条 peer 消息后尝试即时投递。返回持久消息身份及投递观察结果;目标当前不可用时返回 queued 语义,而不是要求调用方重发。
createTask(caller: Agent, request: CreateTeamTaskRequest): Promise<TeamTaskView>
在 Lead 日志中创建无 owner 的 pending 任务,返回 revision-one 视图。请求可以包含依赖和 advisory write scopes。
getTask(caller: Agent, id: TeamTaskId): TeamTaskView
读取任务最新视图;即使任务已删除,也可读取其 tombstone。
listTasks(caller: Agent): TeamTaskView[]
返回当前未删除任务,按数字创建顺序排列。
updateTask(caller: Agent, request: UpdateTeamTaskRequest): Promise<TeamTaskView>
执行带授权检查的 compare-and-set 状态变更。请求中的 expectedRevision 必须匹配当前版本,否则以 TEAM_TASK_STALE_REVISION 拒绝。
waitForChange(caller: Agent, timeoutMs: number, signal: AbortSignal): Promise<TeamWaitResult>
等待下一次 roster、任务、mailbox 或成员实时状态变化。调用只返回一次变化或超时结果,运行时 dispose 会释放等待者。
interrupt(caller: Agent, targetName: string): { previousStatus: 'running' | 'inactive' }
由 Lead 中断一个 live teammate 的当前轮次;不清空 pending inbox,不释放任务 owner。
Failure Modes, Edge Cases & Concurrency
- 配置非法: 非 safe integer 或小于 1 的限制抛出
TEAM_INVALID_CONFIG。 - 成员上限: 超过
maxMembers时明确失败;失败创建的成员名字仍保留,因此不会通过名字复用掩盖 provisioning 问题。 - 目标不存在或不可投递: 消息先 durable queue,调用方看到
queued,恢复流程继续尝试;不能把 queued 消息当作需要手动重发的临时结果。 - 过期任务副本:
expectedRevision不匹配时拒绝更新,避免两个成员静默覆盖成果。 - 依赖未完成: 任务在所有依赖完成前不可 claim;任务依赖需要保持 DAG。
- 重叠写入:
writeScopes只产生警告,不提供文件锁或写权限隔离;共享 checkout 中成员的修改会立即可见。 - 等待取消: 取消原因若不是
Error,会转换为TEAM_WAIT_ABORTED;dispose 会解除当前等待。 - Provisioning 竞争: 恢复和创建同时处理同一 provisioning 记录时,可能报告
TEAM_PROVISIONING_CONFLICT,并 drain 多余 child。 - 进程边界: mailbox 的去重和重试是进程内语义,不是多个 harness 进程之间的 exactly-once 协议。
Performance and Operational Notes
持久化操作的正确性优先于低延迟:queued 消息在投递前 flush,Team 事件在报告成功或唤醒等待者前 flush。浏览器投影在 roster 或任务变化时广播完整 roster 和未删除任务板,较大的 Team 或频繁变更可能带来额外传输成本。
所有成员共享 cwd,当前实现不提供独立 worktree、远程 teammate、merge 或文件锁。Lead 需要协调任务 owner、查看最终 diff,并将 write scope 警告视为提示而非强制约束。不要让多个 harness 进程并发操作同一 Team。
Extension Points and Boundaries
扩展时应优先通过 TeamService facade 或 sibling tool package 使用现有 owner,而不是绕过 journal 直接修改会话状态。新的 Team 事件必须保持可投影、可恢复并纳入 dispose/lifecycle 准入;新的消息路径也必须保留持久 queued/delivered 语义和目标去重。
当前 roster 是扁平且不可变的:只有 Lead 创建直接 teammate,不支持嵌套 Team、重命名、删除或名字复用。任务 owner 不会因成员 inactive、interrupt、进程退出或工作失败而自动释放,运维或上层协调逻辑必须显式处理这一边界。
Related Links
- Agent Teams 子系统:跨包的持久 Team 类型与
ctx.agentTeams服务 API。 - tool-agent-team 包:面向模型的创建 teammate、发送消息和任务板工具。
- Agent Teams Agent Note:身份、mailbox、任务和共享 checkout 的设计决策。
- 实验包参考:实验包位置、发布和依赖隔离。