深入 OmO team-runtime:多 Agent 团队的创建、状态监控与优雅关闭生命周期引擎
【免费下载链接】oh-my-openagentOmO: Just type "mass ulw" keyword with your prompt. Now you are the master of graph engineering.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent
导读:OmO(oh-my-openagent)的 team-mode 支持并行协调多个 Agent 组成团队协同工作,而team-runtime正是承载这一能力生命周期的心脏——它负责team_create的成员派生、team_status的状态聚合、team_shutdown_request/team_approve_shutdown/team_reject_shutdown的关闭握手以及team_delete的整体拆除。本文以 team-runtime 的 AGENTS.md 为骨架,结合其源码实现,完整拆解"创建 → 运行 → 状态查询 → 关闭握手 → 删除回滚"的每个环节,读完你可以掌握该引擎的模块划分、关键约定(如 spawn-race 防护、部分失败回滚)与反模式红线。
一、team-runtime 在 team-mode 中的定位
在 OmO 的 team-mode 架构中(详见 team-mode 总览文档),存在 12 个team_*工具,其中生命周期类工具包括:
| 工具 | 作用 |
|---|---|
team_create | 根据命名或内联 TeamSpec 派生团队与成员会话 |
team_delete | 拆除状态、信箱、任务列表、工作树及可选的 tmux 布局 |
team_shutdown_request | 成员或 lead 请求自身关闭 |
team_approve_shutdown | lead 批准关闭 |
team_reject_shutdown | lead 附带原因拒绝关闭 |
team-runtime就是这些工具背后的生命周期引擎。工具层位于同级目录的 tools/,而team-runtime独占派生(spawning)、状态迁移(state transitions)、成员解析(member resolution)、布局激活(layout activation)与回滚(rollback)职责。
值得注意的边界划分:team-runtime的 barrel 文件 index.ts只导出resolve-member与shutdown两个模块,create/delete/status则被../tools/直接消费。这意味着 shutdown 与成员解析是可复用、可重导出的公共能力,而创建、删除、状态聚合是工具层专属的内部流程。
从代码组织看,registry、mailbox、tasklist、state、worktree、tmux-layout 等无 harness 依赖的基础原语已被抽取到packages/team-core/(团队模式的适配层只保留会话派生、hooks、工具与配置集成),team-runtime位于适配层内,直接调用这些原语的 adapter shim(如team-state-store、team-mailbox、team-worktree、team-layout-tmux)。
二、模块地图:生命周期各环节的源码落点
team-runtime目录下的文件分工明确,官方文档给出的任务定位表如下(路径已换算为仓库根目录相对路径):
| 任务 | 源码位置 |
|---|---|
| 创建一次团队运行 | create.ts(createTeamRun)——通过 BackgroundManager 派生成员,初始化信箱/任务列表/工作树,激活可选的 tmux 布局 |
| 状态查询 | status.ts |
| 关闭握手 | shutdown.ts + shutdown-helpers.ts、shutdown-test-fixtures.ts |
| 删除与后台取消 | delete-team.ts、delete-team-bg-cancel.ts |
| 资源清理/回滚 | cleanup-team-run-resources.ts |
| 成员解析 | resolve-member.ts、resolve-member-dependencies.ts、unresolved-team-members.ts(assertNoUnresolvedTeamMembers) |
| 布局激活 | activate-team-layout.ts(委托给../team-layout-tmux/) |
| 会话级清理注册表 | session-team-run-registry.ts(registerTeamRunForSessionCleanup)、session-cleanup.ts |
| 桶导出 | index.ts(仅导出resolve-member与shutdown) |
三、团队创建(createTeamRun):从 TeamSpec 到可运行团队的完整链路
createTeamRun是team_runtime中最复杂的函数(create.ts),其执行流程可分为六个阶段:
3.1 幂等复用与存量清理
创建开始前先做两件事:
- 查找已有运行:
findExistingRuntime遍历listActiveTeams,若存在同名且状态为creating/active、leadSessionId 一致、且没有未解析成员(!hasUnresolvedTeamMembers)的运行,直接返回,避免重复创建。 - 清理陈旧会话:
sweepStaleTeamSessions(activeRunIds)以 fire-and-forget 方式清除不存在的运行对应的残留 tmux 会话。
随后ensureBaseDirs(baseDir)确保基础目录存在,并用resolveSpecSource判定 TeamSpec 来源是project还是user作用域(项目作用域优先)。
3.2 运行状态创建与 spawn-race 防护
createRuntimeState在team-state-store中建立持久化运行状态,紧接着调用registerTeamRunForSessionCleanup(teamRunId)把本次运行登记进会话级清理注册表。
这是本模块最关键的约定之一:会话 ID 通过轮询获得(create.ts 中SESSION_ID_POLL_MS = 25,即每 25ms 轮询一次),一旦得知 sessionId,就必须同步调用registerTeamSession(sessionId, entry)注册会话,防止 hooks 在 spawn 竞态窗口内查不到会话。onSessionCreated回调中同样先注册再更新运行时状态。这一 spawn-race 规则源自父级 team-mode/AGENTS.md,是团队模式六大不变量之首。
3.3 调用者复用 lead 会话(可选)
当shouldReuseCallerLeadSession判定为真(见 resolve-caller-team-lead.ts)且 spec 指定了leadAgentId时,直接复用调用方的会话作为 lead:注册会话、把该成员的 sessionId 置为调用方会话并标记status: "running",不再为其派生新后台任务。
3.4 并行派生成员(worker 池)
核心派生逻辑采用固定 worker 池模式:workerCount = Math.min(config.max_parallel_members, spec.members.length)个并发 worker 通过nextMemberIndex++原子取号消费成员列表,直到全部派生完成或出现失败。
对每个成员依次执行:
- 工作树创建:若
member.worktreePath存在,则createMemberWorktree在项目根下递归创建(支持绝对路径或相对路径)。 - 成员解析:调用
resolveMember(详见下文第四节),得到实际使用的 agent、模型、fallback 链与系统提示词。 - 后台启动:通过
bgMgr.launch派生后台任务,注入buildMemberPrompt拼接的提示词(包含 Team 名、TeamRunId、Member 名、可选的 Worktree 路径、成员自定义 prompt 以及buildTeammateCommunicationAddendum生成的队友通信指引),同时传入解析出的 model、fallbackChain、skillContent 等,并以QUESTION_DENIED_SESSION_PERMISSION作为会话权限。若成员有工作树,则后台任务 cwd 指向工作树。 - 会话等待与注册:
waitForTaskSessionId以 25ms 为步长轮询任务 sessionId,超过max_wall_clock_minutes期限或任务进入 error/cancelled/interrupt 状态即抛错;拿到 sessionId 后再次registerTeamSession,并把解析出的模型参数(providerID、modelID、variant、reasoningEffort、temperature、top_p、maxTokens、thinking)持久化进运行时状态。
所有成员派生完成后,assertNoUnresolvedTeamMembers校验每个成员都已绑定 sessionId(否则抛错,因为"运行不能带着未解析成员进入 active 状态")。
3.5 布局激活与状态收尾
activateTeamLayout(launchedRuntimeState, config, ctx.directory, tmuxMgr)在tmux_visualization开启且提供 tmux manager 时,为所有非 lead 成员创建 pane 布局(focus pane + grid pane),并把tmuxLayout、各成员的tmuxPaneId/tmuxGridPaneId写入运行时状态(activate-team-layout.ts)。
最后通过transitionRuntimeState把状态从creating迁移到active,createTeamRun返回最终 RuntimeState。
3.6 部分失败回滚:TeamRunCreateError + cleanupReport
整个派生过程包裹在 try/catch 中。一旦任何一步失败,立即调用cleanupTeamRunResources执行回滚,并抛出携带cleanupReport的TeamRunCreateError(create.ts):
TeamRunCreateError └── cleanupReport ├── cancelledTaskIds: string[] // 已取消的后台任务 ├── removedLayout: boolean // 是否已移除 tmux 布局 ├── removedWorktrees: string[] // 已删除的工作树路径 └── errors: string[] // 回滚过程中个别步骤自身的错误回滚实现(cleanup-team-run-resources.ts)按逆序遍历已派生的资源:取消任务(skipNotification: true)、递归删除工作树、移除已创建的 tmux 布局、把运行时状态迁移到failed,最后注销会话与清理注册表。即使单个清理步骤出错也绝不中断——错误被收集进errors数组继续执行,这正是"绝不让失败的 create 半残留"这一反模式红线的代码体现。
四、成员解析(resolveMember):两种成员类型的落地差异
resolveMember(resolve-member.ts)根据 TeamSpec 中成员的kind走两条完全不同的解析路径:
kind: "subagent_type":直接解析为指定 agent(如sisyphus),通过resolveSubagentExecution确定 agentToUse、模型与 fallback 链,并允许allowSisyphusJuniorDirect与allowPrimaryAgentDelegation。kind: "category":路由到sisyphus-junior,通过resolveCategoryExecution按类别选择模型,prompt为必填。
这里有一个反直觉的实现细节(resolve-member.ts):解析前会剥离全局agents.sisyphus-junior.model覆盖(withoutSisyphusJuniorOverride)。注释解释了原因:resolveCategoryExecution会把该全局覆盖排在类别默认值之上——对普通task(category=…)这是正确的,但对团队模式却是错误的,会导致所有团队成员坍缩到同一个模型。
依赖项集中在 resolve-member-dependencies.ts,它只是从 delegate-task 工具层重导出resolveCategoryExecution、resolveSubagentExecution、buildSystemContent三个函数,说明成员解析与普通 delegate-task 复用同一套模型解析与系统提示词构建机制。解析失败时抛出的TeamMemberResolutionError会携带 memberName 便于定位。
五、状态聚合(aggregateStatus):team_status 背后的数据拼装
team_status工具的数据源是 status.ts 中的aggregateStatus,它把分散在多处的运行时数据拼装成一份结构化TeamStatus:
| 字段 | 来源与含义 |
|---|---|
teamName/teamRunId/status/createdAt/leadSessionId | 直接来自team-state-store的 RuntimeState |
members[] | 每个成员的状态、sessionId、颜色、worktreePath、tmux paneId,并附带未读消息数(通过listUnreadMessages统计信箱) |
tasks{} | 按pending / claimed / in_progress / completed / deleted / total六档统计共享任务列表(countTasks) |
shutdownRequests | 运行时状态中保存的关闭请求数组 |
concurrency{} | runningOnSameModel/queuedOnSameModel(基于 lead 会话的主模型键统计同模型并发),以及teamRunIdSpecific(该运行专属的后台任务数) |
bounds | 运行时记录的执行边界(来自配置的各类上限) |
staleLocks | 扫描任务认领目录claims/下的.lock文件,用detectStaleLock(lockPath, 300_000)检测超过 5 分钟未更新的陈旧锁 |
值得注意,未读消息数对每个成员都做了一次异步listUnreadMessages(并行Promise.all),而并发统计优先使用 BackgroundManager 的getConcurrencyCounts,不可用时退化为按 lead 会话的后台任务 status 估算——这种"优先精确、降级估算"的策略保证了查询在缺少 manager 注入时依然可用。
六、关闭握手:shutdown_request / approve / reject 三件套
关闭握手位于 shutdown.ts,由三个函数构成:
6.1 requestShutdownOfMember(发起请求)
- 校验目标成员与请求者都存在于运行时状态(
getRuntimeMember否则抛unknown member)。 - 去重:若存在同一
memberId + requesterName的未决请求(既未 approve 也未 reject),直接返回,不重复投递。 - 通过
sendMessage向目标成员投递shutdown_request类型的消息(消息结构见 shutdown-helpers.ts:version: 1、messageId为 UUID、from/to/kind/body/timestamp)。 - 在
transitionRuntimeState中追加一条{ memberId, requesterName, requestedAt }记录(内部同样做了去重保护)。
6.2 approveShutdown(批准)
- 按
memberName找到最新的关闭请求(findLatestShutdownRequestIndex从数组尾部向前扫描),不存在则抛错。 - 将目标成员状态迁移为
shutdown_approved(已completed/errored的成员跳过)。 - 在请求记录上写入
approvedAt,并向 lead 投递shutdown_approved消息。
6.3 rejectShutdown(拒绝)
- 找到最新请求后,若已拒绝且
rejectedReason与本次一致,则幂等返回。 - 向请求者(
shutdownRequest.requesterName)投递携带拒绝原因的shutdown_rejected消息。 - 在请求记录上写入
rejectedAt与rejectedReason。
三个函数全部遵循"先消息、后状态"的顺序,且状态写入都经过transitionRuntimeState的原子迁移(temp 文件 + rename),与 team-mode 的"原子写入"不变量一致。
七、删除与资源清理:team_delete 与后台取消
deleteTeam(delete-team.ts)是拆除整支团队运行的入口,包含两层防护与两条路径:
状态门槛:
- 普通删除只允许
active / shutdown_requested / deleting / deleted(DELETABLE_TEAM_STATUSES),并且所有非 lead 成员必须处于completed / shutdown_approved / errored(DELETABLE_MEMBER_STATUSES,定义于 shutdown-helpers.ts),否则抛members still active。 force: true额外允许creating / orphaned状态,并把pending / running / idle的成员强制标记为completed。
后台任务处理:通过 lead 会话找到属于本团队的后台任务(按teamRunId或team-create:${teamRunId}:前缀的 parentMessageId 过滤),非 force 模式下存在pending/running任务即拒绝删除;force 模式下则逐个cancelTask(source 为team-mode-delete)。
随后deleteTeamResources依次执行:迁移状态到deleting(creating/orphaned 且 force 时直接saveRuntimeState跳过 transition)→ 移除 tmux 布局(仅当tmux_visualization开启,收集非 lead 成员的 paneId 作为清理目标;force 模式下布局清理失败只记日志不中断)→removeWorktrees删除全部成员工作树 → 迁移状态到deleted→ 删除运行时状态目录本身 → 注销会话与清理注册表 → 再次 sweep 陈旧 tmux 会话。
与创建回滚不同,删除路径还联动 delete-team-bg-cancel.ts 处理更细粒度的后台取消逻辑(该文件配有独立测试 delete-team-bg-cancel.test.ts)。
八、关键约定(CONVENTIONS)与反模式(ANTI-PATTERNS)
本模块的三条核心约定
- 部分失败必回滚:
create.ts失败时抛出携带cleanupReport的TeamRunCreateError,cancelledTaskIds、removedLayout、removedWorktrees、errors四元组必须如实反映;创建过程中获取的每一个资源都必须出现在失败报告中。 - 会话轮询注册:会话 ID 以
SESSION_ID_POLL_MS = 25(毫秒)轮询直到已知,随后同步调用registerTeamSession()——这是源自父级 AGENTS.md 的 spawn-race 规则。 - 调用者 lead 复用:是否复用调用方会话作为 lead,由 resolve-caller-team-lead.ts 的
shouldReuseCallerLeadSession决定。
三条反模式红线
- 绝不未经注册就派生成员:必须在
../team-session-registry.ts注册会话之后才能派生,否则 hooks 会在 spawn 竞态窗口内找不到会话。 - 绝不让失败的 create 半残留:即使个别清理步骤出错,回滚路径也必须执行到底(错误进
errors数组,不中断整体回滚)。 - 不在本模块直接写持久化状态:持久化状态必须经由
../team-state-store/的原子锁机制写入,禁止绕过。
九、测试佐证与进一步阅读
team-runtime的每个核心能力都有配套测试,可作为行为契约阅读:
- 创建链路:create.test.ts(覆盖幂等复用、并行派生、失败回滚)
- 状态聚合:status.test.ts
- 关闭握手:shutdown.test.ts(含 shutdown-test-fixtures.ts 提供的夹具)
- 成员解析:resolve-member.test.ts
- 布局激活:activate-team-layout.test.ts
- 回滚清理:cleanup-team-run-resources.test.ts
- 会话级清理:session-cleanup.test.ts
想继续深入,建议按以下顺序阅读:
- 团队模式总览与配置项:team-mode/AGENTS.md(含
team_mode完整配置 schema、12 工具清单、Agent 准入三级判定、存储布局、六大不变量); - 用户视角的实战文档:docs/guide/team-mode.md;
- 无 harness 依赖的基础原语:packages/team-core(registry/mailbox/tasklist/state/worktree/tmux-layout 的通用实现);
- 生命周期工具的对外封装:tools/lifecycle.ts 与 tools/query.ts。
需要特别提醒的运行时前提:team-mode 默认关闭,需在.omo/omo.jsonc中设置team_mode.enabled: true并重启 OpenCode 才生效;team-runtime的所有派生、状态迁移与布局激活行为都受该配置(max_parallel_members、max_wall_clock_minutes、tmux_visualization等)约束。理解这份 AGENTS.md 与源码的对应关系,你就能在排查团队生命周期问题时,从"工具报错"快速定位到"引擎的哪一层"。
【免费下载链接】oh-my-openagentOmO: Just type "mass ulw" keyword with your prompt. Now you are the master of graph engineering.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考